##// END OF EJS Templates
chore(logs): report warning on missing filestore object
super-admin -
r5507:9ad352d3 default
parent child Browse files
Show More
@@ -1,200 +1,200 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
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
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
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/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 import logging
18 import logging
19
19
20
20
21 from pyramid.response import FileResponse
21 from pyramid.response import FileResponse
22 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
22 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
23
23
24 from rhodecode.apps._base import BaseAppView
24 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps.file_store import utils
25 from rhodecode.apps.file_store import utils
26 from rhodecode.apps.file_store.exceptions import (
26 from rhodecode.apps.file_store.exceptions import (
27 FileNotAllowedException, FileOverSizeException)
27 FileNotAllowedException, FileOverSizeException)
28
28
29 from rhodecode.lib import helpers as h
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
32 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
33 LoginRequired)
33 LoginRequired)
34 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
34 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
35 from rhodecode.model.db import Session, FileStore, UserApiKeys
35 from rhodecode.model.db import Session, FileStore, UserApiKeys
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class FileStoreView(BaseAppView):
40 class FileStoreView(BaseAppView):
41 upload_key = 'store_file'
41 upload_key = 'store_file'
42
42
43 def load_default_context(self):
43 def load_default_context(self):
44 c = self._get_local_tmpl_context()
44 c = self._get_local_tmpl_context()
45 self.storage = utils.get_file_storage(self.request.registry.settings)
45 self.storage = utils.get_file_storage(self.request.registry.settings)
46 return c
46 return c
47
47
48 def _guess_type(self, file_name):
48 def _guess_type(self, file_name):
49 """
49 """
50 Our own type guesser for mimetypes using the rich DB
50 Our own type guesser for mimetypes using the rich DB
51 """
51 """
52 if not hasattr(self, 'db'):
52 if not hasattr(self, 'db'):
53 self.db = get_mimetypes_db()
53 self.db = get_mimetypes_db()
54 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
54 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
55 return _content_type, _encoding
55 return _content_type, _encoding
56
56
57 def _serve_file(self, file_uid):
57 def _serve_file(self, file_uid):
58 if not self.storage.exists(file_uid):
58 if not self.storage.exists(file_uid):
59 store_path = self.storage.store_path(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`',
60 log.warning('File with FID:%s not found in the store under `%s`',
61 file_uid, store_path)
61 file_uid, store_path)
62 raise HTTPNotFound()
62 raise HTTPNotFound()
63
63
64 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
64 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
65 if not db_obj:
65 if not db_obj:
66 raise HTTPNotFound()
66 raise HTTPNotFound()
67
67
68 # private upload for user
68 # private upload for user
69 if db_obj.check_acl and db_obj.scope_user_id:
69 if db_obj.check_acl and db_obj.scope_user_id:
70 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
70 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
71 db_obj.scope_user_id)
71 db_obj.scope_user_id)
72 user = db_obj.user
72 user = db_obj.user
73 if self._rhodecode_db_user.user_id != user.user_id:
73 if self._rhodecode_db_user.user_id != user.user_id:
74 log.warning('Access to file store object forbidden')
74 log.warning('Access to file store object forbidden')
75 raise HTTPNotFound()
75 raise HTTPNotFound()
76
76
77 # scoped to repository permissions
77 # scoped to repository permissions
78 if db_obj.check_acl and db_obj.scope_repo_id:
78 if db_obj.check_acl and db_obj.scope_repo_id:
79 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
79 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
80 db_obj.scope_repo_id)
80 db_obj.scope_repo_id)
81 repo = db_obj.repo
81 repo = db_obj.repo
82 perm_set = ['repository.read', 'repository.write', 'repository.admin']
82 perm_set = ['repository.read', 'repository.write', 'repository.admin']
83 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
83 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
84 if not has_perm:
84 if not has_perm:
85 log.warning('Access to file store object `%s` forbidden', file_uid)
85 log.warning('Access to file store object `%s` forbidden', file_uid)
86 raise HTTPNotFound()
86 raise HTTPNotFound()
87
87
88 # scoped to repository group permissions
88 # scoped to repository group permissions
89 if db_obj.check_acl and db_obj.scope_repo_group_id:
89 if db_obj.check_acl and db_obj.scope_repo_group_id:
90 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
90 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
91 db_obj.scope_repo_group_id)
91 db_obj.scope_repo_group_id)
92 repo_group = db_obj.repo_group
92 repo_group = db_obj.repo_group
93 perm_set = ['group.read', 'group.write', 'group.admin']
93 perm_set = ['group.read', 'group.write', 'group.admin']
94 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
94 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
95 if not has_perm:
95 if not has_perm:
96 log.warning('Access to file store object `%s` forbidden', file_uid)
96 log.warning('Access to file store object `%s` forbidden', file_uid)
97 raise HTTPNotFound()
97 raise HTTPNotFound()
98
98
99 FileStore.bump_access_counter(file_uid)
99 FileStore.bump_access_counter(file_uid)
100
100
101 file_path = self.storage.store_path(file_uid)
101 file_path = self.storage.store_path(file_uid)
102 content_type = 'application/octet-stream'
102 content_type = 'application/octet-stream'
103 content_encoding = None
103 content_encoding = None
104
104
105 _content_type, _encoding = self._guess_type(file_path)
105 _content_type, _encoding = self._guess_type(file_path)
106 if _content_type:
106 if _content_type:
107 content_type = _content_type
107 content_type = _content_type
108
108
109 # For file store we don't submit any session data, this logic tells the
109 # For file store we don't submit any session data, this logic tells the
110 # Session lib to skip it
110 # Session lib to skip it
111 setattr(self.request, '_file_response', True)
111 setattr(self.request, '_file_response', True)
112 response = FileResponse(
112 response = FileResponse(
113 file_path, request=self.request,
113 file_path, request=self.request,
114 content_type=content_type, content_encoding=content_encoding)
114 content_type=content_type, content_encoding=content_encoding)
115
115
116 file_name = db_obj.file_display_name
116 file_name = db_obj.file_display_name
117
117
118 response.headers["Content-Disposition"] = (
118 response.headers["Content-Disposition"] = (
119 f'attachment; filename="{str(file_name)}"'
119 f'attachment; filename="{str(file_name)}"'
120 )
120 )
121 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
121 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
122 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
122 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
123 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
123 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
124 return response
124 return response
125
125
126 @LoginRequired()
126 @LoginRequired()
127 @NotAnonymous()
127 @NotAnonymous()
128 @CSRFRequired()
128 @CSRFRequired()
129 def upload_file(self):
129 def upload_file(self):
130 self.load_default_context()
130 self.load_default_context()
131 file_obj = self.request.POST.get(self.upload_key)
131 file_obj = self.request.POST.get(self.upload_key)
132
132
133 if file_obj is None:
133 if file_obj is None:
134 return {'store_fid': None,
134 return {'store_fid': None,
135 'access_path': None,
135 'access_path': None,
136 'error': f'{self.upload_key} data field is missing'}
136 'error': f'{self.upload_key} data field is missing'}
137
137
138 if not hasattr(file_obj, 'filename'):
138 if not hasattr(file_obj, 'filename'):
139 return {'store_fid': None,
139 return {'store_fid': None,
140 'access_path': None,
140 'access_path': None,
141 'error': 'filename cannot be read from the data field'}
141 'error': 'filename cannot be read from the data field'}
142
142
143 filename = file_obj.filename
143 filename = file_obj.filename
144
144
145 metadata = {
145 metadata = {
146 'user_uploaded': {'username': self._rhodecode_user.username,
146 'user_uploaded': {'username': self._rhodecode_user.username,
147 'user_id': self._rhodecode_user.user_id,
147 'user_id': self._rhodecode_user.user_id,
148 'ip': self._rhodecode_user.ip_addr}}
148 'ip': self._rhodecode_user.ip_addr}}
149 try:
149 try:
150 store_uid, metadata = self.storage.save_file(
150 store_uid, metadata = self.storage.save_file(
151 file_obj.file, filename, extra_metadata=metadata)
151 file_obj.file, filename, extra_metadata=metadata)
152 except FileNotAllowedException:
152 except FileNotAllowedException:
153 return {'store_fid': None,
153 return {'store_fid': None,
154 'access_path': None,
154 'access_path': None,
155 'error': f'File {filename} is not allowed.'}
155 'error': f'File {filename} is not allowed.'}
156
156
157 except FileOverSizeException:
157 except FileOverSizeException:
158 return {'store_fid': None,
158 return {'store_fid': None,
159 'access_path': None,
159 'access_path': None,
160 'error': f'File {filename} is exceeding allowed limit.'}
160 'error': f'File {filename} is exceeding allowed limit.'}
161
161
162 try:
162 try:
163 entry = FileStore.create(
163 entry = FileStore.create(
164 file_uid=store_uid, filename=metadata["filename"],
164 file_uid=store_uid, filename=metadata["filename"],
165 file_hash=metadata["sha256"], file_size=metadata["size"],
165 file_hash=metadata["sha256"], file_size=metadata["size"],
166 file_description='upload attachment',
166 file_description='upload attachment',
167 check_acl=False, user_id=self._rhodecode_user.user_id
167 check_acl=False, user_id=self._rhodecode_user.user_id
168 )
168 )
169 Session().add(entry)
169 Session().add(entry)
170 Session().commit()
170 Session().commit()
171 log.debug('Stored upload in DB as %s', entry)
171 log.debug('Stored upload in DB as %s', entry)
172 except Exception:
172 except Exception:
173 log.exception('Failed to store file %s', filename)
173 log.exception('Failed to store file %s', filename)
174 return {'store_fid': None,
174 return {'store_fid': None,
175 'access_path': None,
175 'access_path': None,
176 'error': f'File {filename} failed to store in DB.'}
176 'error': f'File {filename} failed to store in DB.'}
177
177
178 return {'store_fid': store_uid,
178 return {'store_fid': store_uid,
179 'access_path': h.route_path('download_file', fid=store_uid)}
179 'access_path': h.route_path('download_file', fid=store_uid)}
180
180
181 # ACL is checked by scopes, if no scope the file is accessible to all
181 # ACL is checked by scopes, if no scope the file is accessible to all
182 def download_file(self):
182 def download_file(self):
183 self.load_default_context()
183 self.load_default_context()
184 file_uid = self.request.matchdict['fid']
184 file_uid = self.request.matchdict['fid']
185 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
185 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
186 return self._serve_file(file_uid)
186 return self._serve_file(file_uid)
187
187
188 # in addition to @LoginRequired ACL is checked by scopes
188 # in addition to @LoginRequired ACL is checked by scopes
189 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
189 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
190 @NotAnonymous()
190 @NotAnonymous()
191 def download_file_by_token(self):
191 def download_file_by_token(self):
192 """
192 """
193 Special view that allows to access the download file by special URL that
193 Special view that allows to access the download file by special URL that
194 is stored inside the URL.
194 is stored inside the URL.
195
195
196 http://example.com/_file_store/token-download/TOKEN/FILE_UID
196 http://example.com/_file_store/token-download/TOKEN/FILE_UID
197 """
197 """
198 self.load_default_context()
198 self.load_default_context()
199 file_uid = self.request.matchdict['fid']
199 file_uid = self.request.matchdict['fid']
200 return self._serve_file(file_uid)
200 return self._serve_file(file_uid)
General Comments 0
You need to be logged in to leave comments. Login now