##// END OF EJS Templates
file-store: don't response with cookies on file-store download.
marcink -
r4236:a948e8d8 stable
parent child Browse files
Show More
@@ -1,174 +1,177 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 def _serve_file(self, file_uid):
50 50
51 51 if not self.storage.exists(file_uid):
52 52 store_path = self.storage.store_path(file_uid)
53 53 log.debug('File with FID:%s not found in the store under `%s`',
54 54 file_uid, store_path)
55 55 raise HTTPNotFound()
56 56
57 57 db_obj = FileStore().query().filter(FileStore.file_uid == file_uid).scalar()
58 58 if not db_obj:
59 59 raise HTTPNotFound()
60 60
61 61 # private upload for user
62 62 if db_obj.check_acl and db_obj.scope_user_id:
63 63 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
64 64 db_obj.scope_user_id)
65 65 user = db_obj.user
66 66 if self._rhodecode_db_user.user_id != user.user_id:
67 67 log.warning('Access to file store object forbidden')
68 68 raise HTTPNotFound()
69 69
70 70 # scoped to repository permissions
71 71 if db_obj.check_acl and db_obj.scope_repo_id:
72 72 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
73 73 db_obj.scope_repo_id)
74 74 repo = db_obj.repo
75 75 perm_set = ['repository.read', 'repository.write', 'repository.admin']
76 76 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
77 77 if not has_perm:
78 78 log.warning('Access to file store object `%s` forbidden', file_uid)
79 79 raise HTTPNotFound()
80 80
81 81 # scoped to repository group permissions
82 82 if db_obj.check_acl and db_obj.scope_repo_group_id:
83 83 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
84 84 db_obj.scope_repo_group_id)
85 85 repo_group = db_obj.repo_group
86 86 perm_set = ['group.read', 'group.write', 'group.admin']
87 87 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
88 88 if not has_perm:
89 89 log.warning('Access to file store object `%s` forbidden', file_uid)
90 90 raise HTTPNotFound()
91 91
92 92 FileStore.bump_access_counter(file_uid)
93 93
94 94 file_path = self.storage.store_path(file_uid)
95 95 return FileResponse(file_path)
96 # For file store we don't submit any session data, this logic tells the
97 # Session lib to skip it
98 setattr(self.request, '_file_response', True)
96 99
97 100 @LoginRequired()
98 101 @NotAnonymous()
99 102 @CSRFRequired()
100 103 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
101 104 def upload_file(self):
102 105 self.load_default_context()
103 106 file_obj = self.request.POST.get(self.upload_key)
104 107
105 108 if file_obj is None:
106 109 return {'store_fid': None,
107 110 'access_path': None,
108 111 'error': '{} data field is missing'.format(self.upload_key)}
109 112
110 113 if not hasattr(file_obj, 'filename'):
111 114 return {'store_fid': None,
112 115 'access_path': None,
113 116 'error': 'filename cannot be read from the data field'}
114 117
115 118 filename = file_obj.filename
116 119
117 120 metadata = {
118 121 'user_uploaded': {'username': self._rhodecode_user.username,
119 122 'user_id': self._rhodecode_user.user_id,
120 123 'ip': self._rhodecode_user.ip_addr}}
121 124 try:
122 125 store_uid, metadata = self.storage.save_file(
123 126 file_obj.file, filename, extra_metadata=metadata)
124 127 except FileNotAllowedException:
125 128 return {'store_fid': None,
126 129 'access_path': None,
127 130 'error': 'File {} is not allowed.'.format(filename)}
128 131
129 132 except FileOverSizeException:
130 133 return {'store_fid': None,
131 134 'access_path': None,
132 135 'error': 'File {} is exceeding allowed limit.'.format(filename)}
133 136
134 137 try:
135 138 entry = FileStore.create(
136 139 file_uid=store_uid, filename=metadata["filename"],
137 140 file_hash=metadata["sha256"], file_size=metadata["size"],
138 141 file_description=u'upload attachment',
139 142 check_acl=False, user_id=self._rhodecode_user.user_id
140 143 )
141 144 Session().add(entry)
142 145 Session().commit()
143 146 log.debug('Stored upload in DB as %s', entry)
144 147 except Exception:
145 148 log.exception('Failed to store file %s', filename)
146 149 return {'store_fid': None,
147 150 'access_path': None,
148 151 'error': 'File {} failed to store in DB.'.format(filename)}
149 152
150 153 return {'store_fid': store_uid,
151 154 'access_path': h.route_path('download_file', fid=store_uid)}
152 155
153 156 # ACL is checked by scopes, if no scope the file is accessible to all
154 157 @view_config(route_name='download_file')
155 158 def download_file(self):
156 159 self.load_default_context()
157 160 file_uid = self.request.matchdict['fid']
158 161 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
159 162 return self._serve_file(file_uid)
160 163
161 164 # in addition to @LoginRequired ACL is checked by scopes
162 165 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
163 166 @NotAnonymous()
164 167 @view_config(route_name='download_file_by_token')
165 168 def download_file_by_token(self):
166 169 """
167 170 Special view that allows to access the download file by special URL that
168 171 is stored inside the URL.
169 172
170 173 http://example.com/_file_store/token-download/TOKEN/FILE_UID
171 174 """
172 175 self.load_default_context()
173 176 file_uid = self.request.matchdict['fid']
174 177 return self._serve_file(file_uid)
@@ -1,200 +1,204 b''
1 1 # Copyright (c) 2010 Agendaless Consulting and Contributors.
2 2 # (http://www.agendaless.com), All Rights Reserved
3 3 # License: BSD-derived (http://www.repoze.org/LICENSE.txt)
4 4 # With Patches from RhodeCode GmBH
5 5
6 6
7 7 import os
8 8
9 9 from beaker import cache
10 10 from beaker.session import SessionObject
11 11 from beaker.util import coerce_cache_params
12 12 from beaker.util import coerce_session_params
13 13
14 14 from pyramid.interfaces import ISession
15 15 from pyramid.settings import asbool
16 16 from zope.interface import implementer
17 17
18 18 from binascii import hexlify
19 19
20 20
21 21 def BeakerSessionFactoryConfig(**options):
22 22 """ Return a Pyramid session factory using Beaker session settings
23 23 supplied directly as ``**options``"""
24 24
25 25 class PyramidBeakerSessionObject(SessionObject):
26 26 _options = options
27 27 _cookie_on_exception = _options.pop('cookie_on_exception', True)
28 28 _constant_csrf_token = _options.pop('constant_csrf_token', False)
29 29
30 30 def __init__(self, request):
31 31 SessionObject.__init__(self, request.environ, **self._options)
32 32
33 33 def session_callback(request, response):
34 34 exception = getattr(request, 'exception', None)
35 if (exception is None or self._cookie_on_exception) and self.accessed():
35 file_response = getattr(request, '_file_response', None)
36
37 if file_response is None \
38 and (exception is None or self._cookie_on_exception) \
39 and self.accessed():
36 40 self.persist()
37 41 headers = self.__dict__['_headers']
38 if headers['set_cookie'] and headers['cookie_out']:
42 if headers.get('set_cookie') and headers.get('cookie_out'):
39 43 response.headerlist.append(('Set-Cookie', headers['cookie_out']))
40 44 request.add_response_callback(session_callback)
41 45
42 46 # ISession API
43 47
44 48 @property
45 49 def id(self):
46 50 # this is as inspected in SessionObject.__init__
47 51 if self.__dict__['_params'].get('type') != 'cookie':
48 52 return self._session().id
49 53 return None
50 54
51 55 @property
52 56 def new(self):
53 57 return self.last_accessed is None
54 58
55 59 changed = SessionObject.save
56 60
57 61 # modifying dictionary methods
58 62
59 63 @call_save
60 64 def clear(self):
61 65 return self._session().clear()
62 66
63 67 @call_save
64 68 def update(self, d, **kw):
65 69 return self._session().update(d, **kw)
66 70
67 71 @call_save
68 72 def setdefault(self, k, d=None):
69 73 return self._session().setdefault(k, d)
70 74
71 75 @call_save
72 76 def pop(self, k, d=None):
73 77 return self._session().pop(k, d)
74 78
75 79 @call_save
76 80 def popitem(self):
77 81 return self._session().popitem()
78 82
79 83 __setitem__ = call_save(SessionObject.__setitem__)
80 84 __delitem__ = call_save(SessionObject.__delitem__)
81 85
82 86 # Flash API methods
83 87 def flash(self, msg, queue='', allow_duplicate=True):
84 88 storage = self.setdefault('_f_' + queue, [])
85 89 if allow_duplicate or (msg not in storage):
86 90 storage.append(msg)
87 91
88 92 def pop_flash(self, queue=''):
89 93 storage = self.pop('_f_' + queue, [])
90 94 return storage
91 95
92 96 def peek_flash(self, queue=''):
93 97 storage = self.get('_f_' + queue, [])
94 98 return storage
95 99
96 100 # CSRF API methods
97 101 def new_csrf_token(self):
98 102 token = (self._constant_csrf_token
99 103 or hexlify(os.urandom(20)).decode('ascii'))
100 104 self['_csrft_'] = token
101 105 return token
102 106
103 107 def get_csrf_token(self):
104 108 token = self.get('_csrft_', None)
105 109 if token is None:
106 110 token = self.new_csrf_token()
107 111 return token
108 112
109 113 return implementer(ISession)(PyramidBeakerSessionObject)
110 114
111 115
112 116 def call_save(wrapped):
113 117 """ By default, in non-auto-mode beaker badly wants people to
114 118 call save even though it should know something has changed when
115 119 a mutating method is called. This hack should be removed if
116 120 Beaker ever starts to do this by default. """
117 121 def save(session, *arg, **kw):
118 122 value = wrapped(session, *arg, **kw)
119 123 session.save()
120 124 return value
121 125 save.__doc__ = wrapped.__doc__
122 126 return save
123 127
124 128
125 129 def session_factory_from_settings(settings):
126 130 """ Return a Pyramid session factory using Beaker session settings
127 131 supplied from a Paste configuration file"""
128 132 prefixes = ('session.', 'beaker.session.')
129 133 options = {}
130 134
131 135 # Pull out any config args meant for beaker session. if there are any
132 136 for k, v in settings.items():
133 137 for prefix in prefixes:
134 138 if k.startswith(prefix):
135 139 option_name = k[len(prefix):]
136 140 if option_name == 'cookie_on_exception':
137 141 v = asbool(v)
138 142 options[option_name] = v
139 143
140 144 options = coerce_session_params(options)
141 145 return BeakerSessionFactoryConfig(**options)
142 146
143 147
144 148 def set_cache_regions_from_settings(settings):
145 149 """ Add cache support to the Pylons application.
146 150
147 151 The ``settings`` passed to the configurator are used to setup
148 152 the cache options. Cache options in the settings should start
149 153 with either 'beaker.cache.' or 'cache.'.
150 154
151 155 """
152 156 cache_settings = {'regions': []}
153 157 for key in settings.keys():
154 158 for prefix in ['beaker.cache.', 'cache.']:
155 159 if key.startswith(prefix):
156 160 name = key.split(prefix)[1].strip()
157 161 cache_settings[name] = settings[key].strip()
158 162
159 163 if ('expire' in cache_settings
160 164 and isinstance(cache_settings['expire'], basestring)
161 165 and cache_settings['expire'].lower() in ['none', 'no']):
162 166 cache_settings['expire'] = None
163 167
164 168 coerce_cache_params(cache_settings)
165 169
166 170 if 'enabled' not in cache_settings:
167 171 cache_settings['enabled'] = True
168 172
169 173 regions = cache_settings['regions']
170 174 if regions:
171 175 for region in regions:
172 176 if not region:
173 177 continue
174 178
175 179 region_settings = {
176 180 'data_dir': cache_settings.get('data_dir'),
177 181 'lock_dir': cache_settings.get('lock_dir'),
178 182 'expire': cache_settings.get('expire', 60),
179 183 'enabled': cache_settings['enabled'],
180 184 'key_length': cache_settings.get('key_length', 250),
181 185 'type': cache_settings.get('type'),
182 186 'url': cache_settings.get('url'),
183 187 }
184 188 region_prefix = '%s.' % region
185 189 region_len = len(region_prefix)
186 190 for key in list(cache_settings.keys()):
187 191 if key.startswith(region_prefix):
188 192 region_settings[key[region_len:]] = cache_settings.pop(key)
189 193
190 194 if (isinstance(region_settings['expire'], basestring)
191 195 and region_settings['expire'].lower() in ['none', 'no']):
192 196 region_settings['expire'] = None
193 197 coerce_cache_params(region_settings)
194 198 cache.cache_regions[region] = region_settings
195 199
196 200
197 201 def includeme(config):
198 202 session_factory = session_factory_from_settings(config.registry.settings)
199 203 config.set_session_factory(session_factory)
200 204 set_cache_regions_from_settings(config.registry.settings)
General Comments 0
You need to be logged in to leave comments. Login now