##// END OF EJS Templates
tests: added some more artifact access tests.
marcink -
r4008:786403b1 default
parent child Browse files
Show More
@@ -1,126 +1,261 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import os
21 21 import pytest
22 22
23 23 from rhodecode.lib.ext_json import json
24 from rhodecode.model.db import Session, FileStore
24 from rhodecode.model.auth_token import AuthTokenModel
25 from rhodecode.model.db import Session, FileStore, Repository, User
25 26 from rhodecode.tests import TestController
26 27 from rhodecode.apps.file_store import utils, config_keys
27 28
28 29
29 30 def route_path(name, params=None, **kwargs):
30 31 import urllib
31 32
32 33 base_url = {
33 34 'upload_file': '/_file_store/upload',
34 35 'download_file': '/_file_store/download/{fid}',
36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
35 37
36 38 }[name].format(**kwargs)
37 39
38 40 if params:
39 41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 42 return base_url
41 43
42 44
43 45 class TestFileStoreViews(TestController):
44 46
45 47 @pytest.mark.parametrize("fid, content, exists", [
46 48 ('abcde-0.jpg', "xxxxx", True),
47 49 ('abcde-0.exe', "1234567", True),
48 50 ('abcde-0.jpg', "xxxxx", False),
49 51 ])
50 52 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
51 53 user = self.log_user()
52 54 user_id = user['user_id']
53 55 repo_id = user_util.create_repo().repo_id
54 56 store_path = self.app._pyramid_settings[config_keys.store_path]
55 57 store_uid = fid
56 58
57 59 if exists:
58 60 status = 200
59 61 store = utils.get_file_storage({config_keys.store_path: store_path})
60 62 filesystem_file = os.path.join(str(tmpdir), fid)
61 63 with open(filesystem_file, 'wb') as f:
62 64 f.write(content)
63 65
64 66 with open(filesystem_file, 'rb') as f:
65 67 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
66 68
67 69 entry = FileStore.create(
68 70 file_uid=store_uid, filename=metadata["filename"],
69 71 file_hash=metadata["sha256"], file_size=metadata["size"],
70 72 file_display_name='file_display_name',
71 73 file_description='repo artifact `{}`'.format(metadata["filename"]),
72 74 check_acl=True, user_id=user_id,
73 75 scope_repo_id=repo_id
74 76 )
75 77 Session().add(entry)
76 78 Session().commit()
77 79
78 80 else:
79 81 status = 404
80 82
81 83 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
82 84
83 85 if exists:
84 86 assert response.text == content
85 87 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
86 88 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
87 89 assert os.path.exists(metadata_file)
88 90 with open(metadata_file, 'rb') as f:
89 91 json_data = json.loads(f.read())
90 92
91 93 assert json_data
92 94 assert 'size' in json_data
93 95
94 96 def test_upload_files_without_content_to_store(self):
95 97 self.log_user()
96 98 response = self.app.post(
97 99 route_path('upload_file'),
98 100 params={'csrf_token': self.csrf_token},
99 101 status=200)
100 102
101 103 assert response.json == {
102 104 u'error': u'store_file data field is missing',
103 105 u'access_path': None,
104 106 u'store_fid': None}
105 107
106 108 def test_upload_files_bogus_content_to_store(self):
107 109 self.log_user()
108 110 response = self.app.post(
109 111 route_path('upload_file'),
110 112 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
111 113 status=200)
112 114
113 115 assert response.json == {
114 116 u'error': u'filename cannot be read from the data field',
115 117 u'access_path': None,
116 118 u'store_fid': None}
117 119
118 120 def test_upload_content_to_store(self):
119 121 self.log_user()
120 122 response = self.app.post(
121 123 route_path('upload_file'),
122 124 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
123 125 params={'csrf_token': self.csrf_token},
124 126 status=200)
125 127
126 128 assert response.json['store_fid']
129
130 @pytest.fixture()
131 def create_artifact_factory(self, tmpdir):
132 def factory(user_id, content):
133 store_path = self.app._pyramid_settings[config_keys.store_path]
134 store = utils.get_file_storage({config_keys.store_path: store_path})
135 fid = 'example.txt'
136
137 filesystem_file = os.path.join(str(tmpdir), fid)
138 with open(filesystem_file, 'wb') as f:
139 f.write(content)
140
141 with open(filesystem_file, 'rb') as f:
142 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143
144 entry = FileStore.create(
145 file_uid=store_uid, filename=metadata["filename"],
146 file_hash=metadata["sha256"], file_size=metadata["size"],
147 file_display_name='file_display_name',
148 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 check_acl=True, user_id=user_id,
150 )
151 Session().add(entry)
152 Session().commit()
153 return entry
154 return factory
155
156 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 user = self.log_user()
158 user_id = user['user_id']
159 content = 'HELLO MY NAME IS ARTIFACT !'
160
161 artifact = create_artifact_factory(user_id, content)
162 file_uid = artifact.file_uid
163 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 assert response.text == content
165
166 # log-in to new user and test download again
167 user = user_util.create_user(password='qweqwe')
168 self.log_user(user.username, 'qweqwe')
169 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 assert response.text == content
171
172 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 user = self.log_user()
174 user_id = user['user_id']
175 content = 'HELLO MY NAME IS ARTIFACT !'
176
177 artifact = create_artifact_factory(user_id, content)
178 # bind to repo
179 repo = user_util.create_repo()
180 repo_id = repo.repo_id
181 artifact.scope_repo_id = repo_id
182 Session().add(artifact)
183 Session().commit()
184
185 file_uid = artifact.file_uid
186 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 assert response.text == content
188
189 # log-in to new user and test download again
190 user = user_util.create_user(password='qweqwe')
191 self.log_user(user.username, 'qweqwe')
192 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 assert response.text == content
194
195 # forbid user the rights to repo
196 repo = Repository.get(repo_id)
197 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 self.app.get(route_path('download_file', fid=file_uid), status=404)
199
200 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 user = self.log_user()
202 user_id = user['user_id']
203 content = 'HELLO MY NAME IS ARTIFACT !'
204
205 artifact = create_artifact_factory(user_id, content)
206 # bind to user
207 user = user_util.create_user(password='qweqwe')
208
209 artifact.scope_user_id = user.user_id
210 Session().add(artifact)
211 Session().commit()
212
213 # artifact creator doesn't have access since it's bind to another user
214 file_uid = artifact.file_uid
215 self.app.get(route_path('download_file', fid=file_uid), status=404)
216
217 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 self.log_user(user.username, 'qweqwe')
219 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 assert response.text == content
221
222 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 user_id = User.get_first_super_admin().user_id
224 content = 'HELLO MY NAME IS ARTIFACT !'
225
226 artifact = create_artifact_factory(user_id, content)
227 # bind to repo
228 repo = user_util.create_repo()
229 repo_id = repo.repo_id
230 artifact.scope_repo_id = repo_id
231 Session().add(artifact)
232 Session().commit()
233
234 file_uid = artifact.file_uid
235 self.app.get(route_path('download_file_by_token',
236 _auth_token='bogus', fid=file_uid), status=302)
237
238 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 user = User.get_first_super_admin()
240 AuthTokenModel().create(user, 'test artifact token',
241 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242
243 user = User.get_first_super_admin()
244 artifact_token = user.artifact_token
245
246 user_id = User.get_first_super_admin().user_id
247 content = 'HELLO MY NAME IS ARTIFACT !'
248
249 artifact = create_artifact_factory(user_id, content)
250 # bind to repo
251 repo = user_util.create_repo()
252 repo_id = repo.repo_id
253 artifact.scope_repo_id = repo_id
254 Session().add(artifact)
255 Session().commit()
256
257 file_uid = artifact.file_uid
258 response = self.app.get(
259 route_path('download_file_by_token',
260 _auth_token=artifact_token, fid=file_uid), status=200)
261 assert response.text == content
@@ -1,166 +1,174 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import logging
21 21
22 22 from pyramid.view import view_config
23 23 from pyramid.response import FileResponse
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.exceptions import (
29 29 FileNotAllowedException, FileOverSizeException)
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.auth import (
34 34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 35 LoginRequired)
36 36 from rhodecode.model.db import Session, FileStore, UserApiKeys
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class FileStoreView(BaseAppView):
42 42 upload_key = 'store_file'
43 43
44 44 def load_default_context(self):
45 45 c = self._get_local_tmpl_context()
46 46 self.storage = utils.get_file_storage(self.request.registry.settings)
47 47 return c
48 48
49 49 @LoginRequired()
50 50 @NotAnonymous()
51 51 @CSRFRequired()
52 52 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
53 53 def upload_file(self):
54 54 self.load_default_context()
55 55 file_obj = self.request.POST.get(self.upload_key)
56 56
57 57 if file_obj is None:
58 58 return {'store_fid': None,
59 59 'access_path': None,
60 60 'error': '{} data field is missing'.format(self.upload_key)}
61 61
62 62 if not hasattr(file_obj, 'filename'):
63 63 return {'store_fid': None,
64 64 'access_path': None,
65 65 'error': 'filename cannot be read from the data field'}
66 66
67 67 filename = file_obj.filename
68 68
69 69 metadata = {
70 70 'user_uploaded': {'username': self._rhodecode_user.username,
71 71 'user_id': self._rhodecode_user.user_id,
72 72 'ip': self._rhodecode_user.ip_addr}}
73 73 try:
74 74 store_uid, metadata = self.storage.save_file(
75 75 file_obj.file, filename, extra_metadata=metadata)
76 76 except FileNotAllowedException:
77 77 return {'store_fid': None,
78 78 'access_path': None,
79 79 'error': 'File {} is not allowed.'.format(filename)}
80 80
81 81 except FileOverSizeException:
82 82 return {'store_fid': None,
83 83 'access_path': None,
84 84 'error': 'File {} is exceeding allowed limit.'.format(filename)}
85 85
86 86 try:
87 87 entry = FileStore.create(
88 88 file_uid=store_uid, filename=metadata["filename"],
89 89 file_hash=metadata["sha256"], file_size=metadata["size"],
90 file_description='upload attachment',
90 file_description=u'upload attachment',
91 91 check_acl=False, user_id=self._rhodecode_user.user_id
92 92 )
93 93 Session().add(entry)
94 94 Session().commit()
95 95 log.debug('Stored upload in DB as %s', entry)
96 96 except Exception:
97 97 log.exception('Failed to store file %s', filename)
98 98 return {'store_fid': None,
99 99 'access_path': None,
100 100 'error': 'File {} failed to store in DB.'.format(filename)}
101 101
102 102 return {'store_fid': store_uid,
103 103 'access_path': h.route_path('download_file', fid=store_uid)}
104 104
105 105 def _serve_file(self, file_uid):
106 106
107 107 if not self.storage.exists(file_uid):
108 108 store_path = self.storage.store_path(file_uid)
109 109 log.debug('File with FID:%s not found in the store under `%s`',
110 110 file_uid, store_path)
111 111 raise HTTPNotFound()
112 112
113 113 db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
114 114 if not db_obj:
115 115 raise HTTPNotFound()
116 116
117 117 # private upload for user
118 118 if db_obj.check_acl and db_obj.scope_user_id:
119 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
120 db_obj.scope_user_id)
119 121 user = db_obj.user
120 122 if self._rhodecode_db_user.user_id != user.user_id:
121 123 log.warning('Access to file store object forbidden')
122 124 raise HTTPNotFound()
123 125
124 126 # scoped to repository permissions
125 127 if db_obj.check_acl and db_obj.scope_repo_id:
128 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
129 db_obj.scope_repo_id)
126 130 repo = db_obj.repo
127 131 perm_set = ['repository.read', 'repository.write', 'repository.admin']
128 132 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
129 133 if not has_perm:
130 134 log.warning('Access to file store object `%s` forbidden', file_uid)
131 135 raise HTTPNotFound()
132 136
133 137 # scoped to repository group permissions
134 138 if db_obj.check_acl and db_obj.scope_repo_group_id:
139 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
140 db_obj.scope_repo_group_id)
135 141 repo_group = db_obj.repo_group
136 142 perm_set = ['group.read', 'group.write', 'group.admin']
137 143 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
138 144 if not has_perm:
139 145 log.warning('Access to file store object `%s` forbidden', file_uid)
140 146 raise HTTPNotFound()
141 147
142 148 FileStore.bump_access_counter(file_uid)
143 149
144 150 file_path = self.storage.store_path(file_uid)
145 151 return FileResponse(file_path)
146 152
147 153 # ACL is checked by scopes, if no scope the file is accessible to all
148 154 @view_config(route_name='download_file')
149 155 def download_file(self):
150 156 self.load_default_context()
151 157 file_uid = self.request.matchdict['fid']
152 158 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
153 159 return self._serve_file(file_uid)
154 160
161 # in addition to @LoginRequired ACL is checked by scopes
155 162 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
163 @NotAnonymous()
156 164 @view_config(route_name='download_file_by_token')
157 165 def download_file_by_token(self):
158 166 """
159 167 Special view that allows to access the download file by special URL that
160 168 is stored inside the URL.
161 169
162 170 http://example.com/_file_store/token-download/TOKEN/FILE_UID
163 171 """
164 172 self.load_default_context()
165 173 file_uid = self.request.matchdict['fid']
166 174 return self._serve_file(file_uid)
General Comments 0
You need to be logged in to leave comments. Login now