##// 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]
@@ -54,7 +54,7 b' syntax: regexp'
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.*$
@@ -1,12 +1,49 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 '{}' ';'
@@ -14,14 +51,14 b' clean:'
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 '{}' ';'
@@ -29,34 +66,36 b' test-clean:'
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"
@@ -64,25 +103,9 b' web-build:'
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
@@ -95,14 +118,14 b' dev-sh:'
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/
@@ -114,7 +137,7 b' dev-env:'
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
@@ -124,49 +147,12 b' sh:'
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 }'
@@ -257,6 +257,13 b' license_token ='
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
@@ -281,15 +288,56 b' labs_settings_active = true'
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
@@ -624,7 +672,8 b' vcs.scm_app_implementation = http'
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.
@@ -647,6 +696,12 b' vcs.connection_timeout = 3600'
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
@@ -716,7 +771,8 b' ssh.authorized_keys_file_path = /etc/rho'
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
@@ -13,6 +13,7 b' 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
@@ -37,17 +38,41 b" accesslog = '-'"
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
@@ -225,6 +225,13 b' license_token ='
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
@@ -249,15 +256,56 b' labs_settings_active = true'
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
@@ -592,7 +640,8 b' vcs.scm_app_implementation = http'
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.
@@ -615,6 +664,12 b' vcs.connection_timeout = 3600'
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
@@ -684,7 +739,8 b' ssh.authorized_keys_file_path = /etc/rho'
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
@@ -22,6 +22,12 b' RUN apt-get update \\'
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
@@ -147,10 +147,6 b' Peer-to-peer Failover Support'
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 -------------------
@@ -13,7 +13,7 b' This method simply enables SAML authenti'
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
@@ -46,6 +46,8 b' From available options pick only one and'
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
@@ -62,13 +64,13 b' Enter in the ishell prompt'
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 = ''
@@ -76,7 +78,7 b' Enter in the ishell prompt'
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
@@ -32,62 +32,118 b' 4. Check the `enabled` check box in the '
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.
@@ -15,5 +15,6 b' Please check for reference two example p'
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
@@ -32,62 +32,117 b' 4. Check the `enabled` check box in the '
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
@@ -29,6 +29,7 b' administrator greater control over how u'
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
@@ -4,237 +4,8 b''
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 ^^^^^^^^^^
@@ -37,6 +37,18 b' and commit files and |repos| while manag'
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
@@ -66,6 +66,7 b' Output should look similar to this:'
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
@@ -76,6 +77,7 b' At this point you should be able to acce'
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:
@@ -85,7 +87,6 b' At this point you should be able to acce'
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/
@@ -1,23 +1,12 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 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -42,6 +42,7 b' Newer Operating system locales'
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
@@ -93,3 +94,24 b' example to pass the correct locale infor'
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
@@ -10,20 +10,20 b' Release Date'
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.
@@ -39,21 +39,20 b' 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.
@@ -9,7 +9,9 b' Release Notes'
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
@@ -4,7 +4,7 b' 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
@@ -5,7 +5,7 b' alembic==1.13.1'
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
@@ -18,8 +18,8 b' celery==5.3.6'
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
@@ -33,7 +33,7 b' 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
@@ -46,7 +46,7 b' channelstream==0.7.1'
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
@@ -61,7 +61,7 b' channelstream==0.7.1'
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
@@ -87,32 +87,31 b' dogpile.cache==1.3.3'
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
@@ -127,7 +126,7 b' nbconvert==7.7.3'
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
@@ -135,15 +134,15 b' nbconvert==7.7.3'
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
@@ -151,9 +150,9 b' nbconvert==7.7.3'
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
@@ -161,20 +160,20 b' nbconvert==7.7.3'
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
@@ -191,33 +190,6 b' 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
@@ -229,13 +201,27 b' pyramid-mailer==0.15.1'
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
@@ -243,20 +229,20 b' python-ldap==3.4.3'
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
@@ -283,7 +269,7 b' s3fs==2024.6.0'
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
@@ -293,7 +279,7 b' sshpubkeys==3.3.1'
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
@@ -9,6 +9,7 b' pympler'
9 9 ipdb
10 10 ipython
11 11 rich
12 pyramid-debugtoolbar
12 13
13 14 # format
14 15 flake8
@@ -4,38 +4,38 b' 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
@@ -1,1 +1,1 b''
1 5.1.2 No newline at end of file
1 5.2.0
@@ -40,6 +40,7 b' 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
@@ -186,7 +187,6 b' def request_view(request):'
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
@@ -33,7 +33,7 b' 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
@@ -328,8 +328,8 b' def get_method(request, apiuser, pattern'
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()
@@ -43,7 +43,38 b' def admin_routes(config):'
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')
@@ -75,14 +75,21 b' class AdminSettingsView(BaseAppView):'
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:
@@ -92,6 +99,7 b' class AdminSettingsView(BaseAppView):'
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
@@ -164,7 +172,6 b' class AdminSettingsView(BaseAppView):'
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,11 +171,17 b' class AdminSystemInfoSettingsView(BaseAp'
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
@@ -16,7 +16,8 b''
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
@@ -24,18 +25,48 b' def _sanitize_settings_and_apply_default'
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
@@ -20,6 +20,38 b''
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
@@ -16,3 +16,42 b''
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
@@ -15,13 +15,16 b''
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
@@ -29,27 +32,61 b' from rhodecode.tests.routes import route'
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"],
@@ -69,14 +106,10 b' class TestFileStoreViews(TestController)'
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()
@@ -112,32 +145,6 b' class TestFileStoreViews(TestController)'
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']
@@ -19,21 +19,84 b''
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.
@@ -46,10 +109,37 b' def uid_filename(filename, randomized=Tr'
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
@@ -17,12 +17,11 b''
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
@@ -31,6 +30,7 b' 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
@@ -42,7 +42,7 b' class FileStoreView(BaseAppView):'
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):
@@ -55,10 +55,10 b' class FileStoreView(BaseAppView):'
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)
@@ -98,28 +98,25 b' class FileStoreView(BaseAppView):'
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
@@ -147,8 +144,8 b' class FileStoreView(BaseAppView):'
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,
@@ -182,7 +179,7 b' class FileStoreView(BaseAppView):'
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
@@ -601,26 +601,26 b' class RepoCommitsView(RepoAppView):'
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(
@@ -48,7 +48,7 b' 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
@@ -820,7 +820,7 b' class RepoFilesView(RepoAppView):'
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(
@@ -29,7 +29,7 b' 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
@@ -136,6 +136,9 b' class RepoSettingsAdvancedView(RepoAppVi'
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)
@@ -158,9 +161,6 b' class RepoSettingsAdvancedView(RepoAppVi'
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.')
@@ -171,9 +171,6 b' class RepoSettingsAdvancedView(RepoAppVi'
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(
@@ -184,6 +181,16 b' class RepoSettingsAdvancedView(RepoAppVi'
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`')
@@ -37,7 +37,7 b' def _sanitize_settings_and_apply_default'
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')
@@ -23,7 +23,7 b" generate_authorized_keyfile = 'ssh.gener"
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
@@ -157,7 +157,7 b' class SshVcsServer(object):'
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)
@@ -32,7 +32,7 b' class GitServerCreator(object):'
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'
@@ -31,7 +31,7 b' class MercurialServerCreator(object):'
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'
@@ -29,7 +29,7 b' class SubversionServerCreator(object):'
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'
@@ -52,6 +52,7 b' class AuthnRootResource(AuthnResourceBas'
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()
@@ -52,7 +52,8 b' def sanitize_settings_and_apply_defaults'
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
@@ -95,6 +96,11 b' def sanitize_settings_and_apply_defaults'
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')
@@ -106,7 +112,7 b' def sanitize_settings_and_apply_defaults'
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', '')
@@ -116,6 +122,9 b' def sanitize_settings_and_apply_defaults'
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
@@ -152,6 +161,10 b' def sanitize_settings_and_apply_defaults'
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
@@ -202,7 +215,7 b' def sanitize_settings_and_apply_defaults'
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')
@@ -16,8 +16,8 b''
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
@@ -30,6 +30,21 b' from rhodecode.lib.vcs import connect_vc'
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()
@@ -75,11 +90,8 b' def load_pyramid_environment(global_conf'
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))
@@ -35,7 +35,7 b' from pyramid.renderers import render_to_'
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
@@ -50,7 +50,7 b' from rhodecode.lib.utils2 import Attribu'
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__)
@@ -99,6 +99,7 b' def make_pyramid_app(global_config, **se'
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
@@ -114,6 +115,9 b' def make_pyramid_app(global_config, **se'
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
@@ -396,7 +400,8 b' def includeme(config, auth_resources=Non'
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')
@@ -158,3 +158,10 b' def inspect_getargspec():'
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)
@@ -35,7 +35,7 b' def configure_vcs(config):'
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']
@@ -31,9 +31,11 b' 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):
@@ -58,7 +58,7 b' class S3Shard(BaseShard):'
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
@@ -27,10 +27,11 b' Celery loader, run with::'
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
@@ -1,4 +1,4 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
@@ -64,8 +64,9 b' def send_email(recipients, subject, body'
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):
@@ -86,8 +87,8 b' def send_email(recipients, subject, body'
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')),
@@ -207,7 +208,7 b' def create_repo(form_data, cur_user):'
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)
@@ -298,7 +299,7 b' def create_repo_fork(form_data, cur_user'
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)
@@ -390,7 +391,7 b' def sync_last_update_for_objects(*args, '
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:
@@ -570,7 +570,6 b' class DbManage(object):'
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', '/'),
@@ -35,16 +35,19 b' def downgrade(migrate_engine):'
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...')
@@ -80,6 +80,10 b' class AttachedPullRequestsError(Exceptio'
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
@@ -98,6 +102,11 b' class HTTPRequirementError(HTTPClientErr'
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
@@ -81,7 +81,7 b' from rhodecode.lib.action_parser import '
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
@@ -1416,62 +1416,14 b' class InitialsGravatar(object):'
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):
@@ -66,12 +66,12 b' class Hooks(object):'
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
@@ -30,7 +30,7 b' 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
@@ -64,6 +64,18 b' def is_shadow_repo(extras):'
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('.'):
@@ -108,6 +120,7 b' def pre_push(extras):'
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]):
@@ -129,6 +142,8 b' def pre_push(extras):'
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:
@@ -180,6 +195,7 b' def pre_pull(extras):'
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
@@ -46,7 +46,7 b' class RequestWrapperTween(object):'
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
@@ -63,7 +63,7 b' class RequestWrapperTween(object):'
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,
@@ -53,25 +53,31 b' class SimpleHg(simplevcs.SimpleVCS):'
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
@@ -293,7 +293,7 b' class SimpleVCS(object):'
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(
@@ -339,21 +339,6 b' class SimpleVCS(object):'
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')
@@ -373,12 +358,6 b' class SimpleVCS(object):'
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
@@ -159,11 +159,18 b' def detect_vcs_request(environ, backends'
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",
@@ -178,12 +185,6 b' def detect_vcs_request(environ, backends'
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',
@@ -193,11 +194,14 b' def detect_vcs_request(environ, backends'
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
@@ -38,9 +38,9 b' from dogpile.cache.backends import redis'
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
@@ -198,6 +198,13 b' class FileNamespaceBackend(PickleSeriali'
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 = ''
@@ -289,7 +296,7 b' class RedisMsgPackBackend(MsgPackSeriali'
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"""
@@ -26,9 +26,9 b' 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
@@ -91,15 +91,14 b' def command(ini_path, filename, file_pat'
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')
@@ -108,11 +108,10 b' def command(ini_path, force_yes, user, e'
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
@@ -181,3 +181,7 b' def splitnewlines(text: bytes):'
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')
@@ -396,17 +396,18 b' def storage_inodes():'
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
@@ -415,7 +416,8 b' def storage_archives():'
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:
@@ -425,8 +427,44 b' def storage_archives():'
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
@@ -798,6 +836,7 b' def get_system_info(environ):'
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
@@ -42,6 +42,8 b' from webhelpers2.text import collapse, s'
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
@@ -84,8 +86,39 b' def adopt_for_celery(func):'
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
@@ -361,32 +394,39 b' ui_sections = ['
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,
@@ -415,7 +455,7 b' def make_db_config(clear_session=True, r'
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
@@ -582,7 +622,7 b' def repo2db_mapper(initial_repo_list, re'
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', '')
@@ -2568,10 +2568,10 b' class Repository(Base, BaseModel):'
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::
@@ -2610,6 +2610,14 b' class Repository(Base, BaseModel):'
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']):
@@ -2636,6 +2644,7 b' class Repository(Base, BaseModel):'
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:
@@ -2650,6 +2659,7 b' class Repository(Base, BaseModel):'
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',
@@ -5839,8 +5849,7 b' class FileStore(Base, BaseModel):'
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
@@ -5939,7 +5948,7 b' class FileStoreMetadata(Base, BaseModel)'
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):
@@ -129,6 +129,20 b' def TOTPForm(localizer, user, allow_reco'
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 []
@@ -454,13 +468,6 b' 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')
@@ -117,14 +117,16 b' class NotificationModel(BaseModel):'
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,
@@ -31,7 +31,7 b' from zope.cachedescriptors.property impo'
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
@@ -736,7 +736,7 b' class RepoModel(BaseModel):'
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
@@ -745,6 +745,7 b' class RepoModel(BaseModel):'
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:
@@ -767,6 +768,13 b' class RepoModel(BaseModel):'
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:
@@ -486,7 +486,6 b' class VcsSettingsModel(object):'
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'),
@@ -496,12 +495,10 b' class VcsSettingsModel(object):'
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):
@@ -666,18 +663,16 b' class VcsSettingsModel(object):'
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='',
@@ -697,26 +692,17 b' class VcsSettingsModel(object):'
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_)
@@ -557,10 +557,10 b' class UserModel(BaseModel):'
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
@@ -86,6 +86,7 b' function registerRCRoutes() {'
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']);
@@ -111,9 +111,11 b' def scan_repositories_if_enabled(event):'
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
@@ -205,7 +207,7 b' def write_usage_data(event):'
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())
@@ -216,10 +218,9 b' def write_usage_data(event):'
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')
@@ -314,6 +315,28 b' def write_js_routes_if_enabled(event):'
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.
@@ -26,8 +26,13 b''
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>
@@ -50,6 +50,13 b''
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'}
@@ -215,18 +215,35 b''
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
@@ -114,6 +114,7 b''
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>
@@ -5,22 +5,7 b''
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']:
@@ -75,17 +60,6 b''
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>
@@ -127,17 +101,6 b''
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
@@ -117,7 +117,7 b' class TestSanitizeVcsSettings(object):'
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', ''),
@@ -305,7 +305,7 b' class Fixture(object):'
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):
@@ -110,7 +110,7 b' def ini_config(request, tmpdir_factory, '
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',
@@ -120,7 +120,6 b' def test_get_config(user_util, baseapp, '
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', '/'),
@@ -239,7 +239,6 b' class TestShadowRepoExposure(object):'
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
@@ -267,7 +266,6 b' class TestShadowRepoExposure(object):'
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
@@ -291,7 +289,6 b' class TestShadowRepoExposure(object):'
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',)
@@ -399,7 +396,7 b' class TestGenerateVcsResponse(object):'
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()
@@ -371,7 +371,7 b' class TestMakeDbConfig(object):'
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)
@@ -381,8 +381,8 b' class TestMakeDbConfig(object):'
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),
@@ -398,7 +398,7 b' class TestConfigDataFromDb(object):'
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)
@@ -407,7 +407,8 b' class TestConfigDataFromDb(object):'
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
@@ -578,21 +578,9 b' class TestCreateOrUpdateRepoHgSettings(o'
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 }
@@ -605,7 +593,6 b' class TestCreateOrUpdateGlobalHgSettings'
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=''),
@@ -632,7 +619,6 b' class TestCreateOrUpdateGlobalHgSettings'
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):
@@ -643,7 +629,6 b' class TestCreateOrUpdateGlobalGitSetting'
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
@@ -1001,9 +986,7 b' class TestCreateOrUpdateRepoSettings(obj'
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,
@@ -449,7 +449,7 b' class TestPullRequestModel(object):'
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,
@@ -36,7 +36,7 b' port = 10020'
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
@@ -249,15 +249,56 b' labs_settings_active = true'
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
@@ -593,6 +634,7 b' vcs.scm_app_implementation = http'
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
@@ -626,6 +668,10 b' vcs.methods.cache = false'
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
@@ -681,7 +727,8 b' ssh.authorized_keys_file_path = %(here)s'
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
@@ -189,6 +189,7 b' setup('
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 ],
@@ -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