diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -284,6 +284,13 @@ labs_settings_active = true ## This is used to store exception from RhodeCode in shared directory #exception_tracker.store_path = +## File store configuration. This is used to store and serve uploaded files +file_store.enabled = true +## backend, only available one is local +file_store.backend = local +## path to store the uploaded binaries +file_store.storage_path = %(here)s/data/file_store + #################################### ### CELERY CONFIG #### diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -259,6 +259,13 @@ labs_settings_active = true ## This is used to store exception from RhodeCode in shared directory #exception_tracker.store_path = +## File store configuration. This is used to store and serve uploaded files +file_store.enabled = true +## backend, only available one is local +file_store.backend = local +## path to store the uploaded binaries +file_store.storage_path = %(here)s/data/file_store + #################################### ### CELERY CONFIG #### diff --git a/rhodecode/apps/upload_store/__init__.py b/rhodecode/apps/upload_store/__init__.py new file mode 100755 --- /dev/null +++ b/rhodecode/apps/upload_store/__init__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +import os +from rhodecode.apps.upload_store import config_keys +from rhodecode.config.middleware import _bool_setting, _string_setting + + +def _sanitize_settings_and_apply_defaults(settings): + """ + Set defaults, convert to python types and validate settings. + """ + _bool_setting(settings, config_keys.enabled, 'true') + + _string_setting(settings, config_keys.backend, 'local') + + default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store') + _string_setting(settings, config_keys.store_path, default_store) + + +def includeme(config): + settings = config.registry.settings + _sanitize_settings_and_apply_defaults(settings) + + config.add_route( + name='upload_file', + pattern='/_file_store/upload') + config.add_route( + name='download_file', + pattern='/_file_store/download/{fid}') + + # Scan module for configuration decorators. + config.scan('.views', ignore='.tests') diff --git a/rhodecode/apps/upload_store/config_keys.py b/rhodecode/apps/upload_store/config_keys.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/upload_store/config_keys.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +# Definition of setting keys used to configure this module. Defined here to +# avoid repetition of keys throughout the module. + +enabled = 'file_store.enabled' +backend = 'file_store.backend' +store_path = 'file_store.storage_path' diff --git a/rhodecode/apps/upload_store/exceptions.py b/rhodecode/apps/upload_store/exceptions.py new file mode 100755 --- /dev/null +++ b/rhodecode/apps/upload_store/exceptions.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +class FileNotAllowedException(Exception): + """ + Thrown if file does not have an allowed extension. + """ + + +class FileOverSizeException(Exception): + """ + Thrown if file is over the set limit. + """ diff --git a/rhodecode/apps/upload_store/extensions.py b/rhodecode/apps/upload_store/extensions.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/upload_store/extensions.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +ANY = [] +TEXT_EXT = ['txt', 'md', 'rst', 'log'] +DOCUMENTS_EXT = ['pdf', 'rtf', 'odf', 'ods', 'gnumeric', 'abw', 'doc', 'docx', 'xls', 'xlsx'] +IMAGES_EXT = ['jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tiff'] +AUDIO_EXT = ['wav', 'mp3', 'aac', 'ogg', 'oga', 'flac'] +VIDEO_EXT = ['mpeg', '3gp', 'avi', 'divx', 'dvr', 'flv', 'mp4', 'wmv'] +DATA_EXT = ['csv', 'ini', 'json', 'plist', 'xml', 'yaml', 'yml'] +SCRIPTS_EXT = ['js', 'php', 'pl', 'py', 'rb', 'sh', 'go', 'c', 'h'] +ARCHIVES_EXT = ['gz', 'bz2', 'zip', 'tar', 'tgz', 'txz', '7z'] +EXECUTABLES_EXT = ['so', 'exe', 'dll'] + + +DEFAULT = DOCUMENTS_EXT + TEXT_EXT + IMAGES_EXT + DATA_EXT + +GROUPS = dict(( + ('any', ANY), + ('text', TEXT_EXT), + ('documents', DOCUMENTS_EXT), + ('images', IMAGES_EXT), + ('audio', AUDIO_EXT), + ('video', VIDEO_EXT), + ('data', DATA_EXT), + ('scripts', SCRIPTS_EXT), + ('archives', ARCHIVES_EXT), + ('executables', EXECUTABLES_EXT), + ('default', DEFAULT), +)) + + +def resolve_extensions(extensions, groups=None): + """ + Calculate allowed extensions based on a list of extensions provided, and optional + groups of extensions from the available lists. + + :param extensions: a list of extensions e.g ['py', 'txt'] + :param groups: additionally groups to extend the extensions. + """ + groups = groups or [] + valid_exts = set([x.lower() for x in extensions]) + + for group in groups: + if group in GROUPS: + valid_exts.update(GROUPS[group]) + + return valid_exts diff --git a/rhodecode/apps/upload_store/local_store.py b/rhodecode/apps/upload_store/local_store.py new file mode 100755 --- /dev/null +++ b/rhodecode/apps/upload_store/local_store.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os +import shutil + +from rhodecode.lib.ext_json import json +from rhodecode.apps.upload_store import utils +from rhodecode.apps.upload_store.extensions import resolve_extensions +from rhodecode.apps.upload_store.exceptions import FileNotAllowedException + + +class LocalFileStorage(object): + + @classmethod + def resolve_name(cls, name, directory): + """ + Resolves a unique name and the correct path. If a filename + for that path already exists then a numeric prefix with values > 0 will be + added, for example test.jpg -> test-1.jpg etc. initially file would have 0 prefix. + + :param name: base name of file + :param directory: absolute directory path + """ + + basename, ext = os.path.splitext(name) + counter = 0 + while True: + name = '%s-%d%s' % (basename, counter, ext) + path = os.path.join(directory, name) + if not os.path.exists(path): + return name, path + counter += 1 + + def __init__(self, base_path, extension_groups=None): + + """ + Local file storage + + :param base_path: the absolute base path where uploads are stored + :param extension_groups: extensions string + """ + + extension_groups = extension_groups or ['any'] + self.base_path = base_path + self.extensions = resolve_extensions([], groups=extension_groups) + + def store_path(self, filename): + """ + Returns absolute file path of the filename, joined to the + base_path. + + :param filename: base name of file + """ + return os.path.join(self.base_path, filename) + + def delete(self, filename): + """ + Deletes the filename. Filename is resolved with the + absolute path based on base_path. If file does not exist, + returns **False**, otherwise **True** + + :param filename: base name of file + """ + if self.exists(filename): + os.remove(self.store_path(filename)) + return True + return False + + def exists(self, filename): + """ + Checks if file exists. Resolves filename's absolute + path based on base_path. + + :param filename: base name of file + """ + return os.path.exists(self.store_path(filename)) + + def filename_allowed(self, filename, extensions=None): + """Checks if a filename has an allowed extension + + :param filename: base name of file + :param extensions: iterable of extensions (or self.extensions) + """ + _, ext = os.path.splitext(filename) + return self.extension_allowed(ext, extensions) + + def extension_allowed(self, ext, extensions=None): + """ + Checks if an extension is permitted. Both e.g. ".jpg" and + "jpg" can be passed in. Extension lookup is case-insensitive. + + :param extensions: iterable of extensions (or self.extensions) + """ + + extensions = extensions or self.extensions + if not extensions: + return True + if ext.startswith('.'): + ext = ext[1:] + return ext.lower() in extensions + + def save_file(self, file_obj, filename, directory=None, extensions=None, + metadata=None, **kwargs): + """ + Saves a file object to the uploads location. + Returns the resolved filename, i.e. the directory + + the (randomized/incremented) base name. + + :param file_obj: **cgi.FieldStorage** object (or similar) + :param filename: original filename + :param directory: relative path of sub-directory + :param extensions: iterable of allowed extensions, if not default + :param metadata: JSON metadata to store next to the file with .meta suffix + :returns: modified filename + """ + + extensions = extensions or self.extensions + + if not self.filename_allowed(filename, extensions): + raise FileNotAllowedException() + + if directory: + dest_directory = os.path.join(self.base_path, directory) + else: + dest_directory = self.base_path + + if not os.path.exists(dest_directory): + os.makedirs(dest_directory) + + filename = utils.uid_filename(filename) + + filename, path = self.resolve_name(filename, dest_directory) + filename_meta = filename + '.meta' + + file_obj.seek(0) + + with open(path, "wb") as dest: + shutil.copyfileobj(file_obj, dest) + + if metadata: + size = os.stat(path).st_size + metadata.update({'size': size}) + with open(os.path.join(dest_directory, filename_meta), "wb") as dest_meta: + dest_meta.write(json.dumps(metadata)) + + if directory: + filename = os.path.join(directory, filename) + + return filename diff --git a/rhodecode/apps/upload_store/tests/__init__.py b/rhodecode/apps/upload_store/tests/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/upload_store/tests/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + diff --git a/rhodecode/apps/upload_store/tests/test_upload_file.py b/rhodecode/apps/upload_store/tests/test_upload_file.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/upload_store/tests/test_upload_file.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +import os +import pytest + +from rhodecode.lib.ext_json import json +from rhodecode.tests import TestController +from rhodecode.apps.upload_store import utils, config_keys + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'upload_file': '/_file_store/upload', + 'download_file': '/_file_store/download/{fid}', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestFileStoreViews(TestController): + + @pytest.mark.parametrize("fid, content, exists", [ + ('abcde-0.jpg', "xxxxx", True), + ('abcde-0.exe', "1234567", True), + ('abcde-0.jpg', "xxxxx", False), + ]) + def test_get_files_from_store(self, fid, content, exists, tmpdir): + self.log_user() + store_path = self.app._pyramid_settings[config_keys.store_path] + + if exists: + status = 200 + store = utils.get_file_storage({config_keys.store_path: store_path}) + filesystem_file = os.path.join(str(tmpdir), fid) + with open(filesystem_file, 'wb') as f: + f.write(content) + + with open(filesystem_file, 'rb') as f: + fid = store.save_file(f, fid, metadata={'filename': fid}) + + else: + status = 404 + + response = self.app.get(route_path('download_file', fid=fid), status=status) + + if exists: + assert response.text == content + metadata = os.path.join(store_path, fid + '.meta') + assert os.path.exists(metadata) + with open(metadata, 'rb') as f: + json_data = json.loads(f.read()) + + assert json_data + assert 'size' in json_data + + def test_upload_files_without_content_to_store(self): + self.log_user() + response = self.app.post( + route_path('upload_file'), + params={'csrf_token': self.csrf_token}, + status=200) + + assert response.json == { + u'error': u'store_file data field is missing', + u'access_path': None, + u'store_fid': None} + + def test_upload_files_bogus_content_to_store(self): + self.log_user() + response = self.app.post( + route_path('upload_file'), + params={'csrf_token': self.csrf_token, 'store_file': 'bogus'}, + status=200) + + assert response.json == { + u'error': u'filename cannot be read from the data field', + u'access_path': None, + u'store_fid': None} + + def test_upload_content_to_store(self): + self.log_user() + response = self.app.post( + route_path('upload_file'), + upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')], + params={'csrf_token': self.csrf_token}, + status=200) + + assert response.json['store_fid'] diff --git a/rhodecode/apps/upload_store/utils.py b/rhodecode/apps/upload_store/utils.py new file mode 100755 --- /dev/null +++ b/rhodecode/apps/upload_store/utils.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +import os +import uuid + + +def get_file_storage(settings): + from rhodecode.apps.upload_store.local_store import LocalFileStorage + from rhodecode.apps.upload_store import config_keys + store_path = settings.get(config_keys.store_path) + return LocalFileStorage(base_path=store_path) + + +def uid_filename(filename, randomized=True): + """ + Generates a randomized or stable (uuid) filename, + preserving the original extension. + + :param filename: the original filename + :param randomized: define if filename should be stable (sha1 based) or randomized + """ + _, ext = os.path.splitext(filename) + if randomized: + uid = uuid.uuid4() + else: + hash_key = '{}.{}'.format(filename, 'store') + uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key) + return str(uid) + ext.lower() diff --git a/rhodecode/apps/upload_store/views.py b/rhodecode/apps/upload_store/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/upload_store/views.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2019 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ +import logging + +from pyramid.view import view_config +from pyramid.response import FileResponse +from pyramid.httpexceptions import HTTPFound, HTTPNotFound + +from rhodecode.apps._base import BaseAppView +from rhodecode.apps.upload_store import utils +from rhodecode.apps.upload_store.exceptions import ( + FileNotAllowedException,FileOverSizeException) + +from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.auth import (CSRFRequired, NotAnonymous) + +log = logging.getLogger(__name__) + + +class FileStoreView(BaseAppView): + upload_key = 'store_file' + + def load_default_context(self): + c = self._get_local_tmpl_context() + self.storage = utils.get_file_storage(self.request.registry.settings) + return c + + @NotAnonymous() + @CSRFRequired() + @view_config(route_name='upload_file', request_method='POST', renderer='json_ext') + def upload_file(self): + self.load_default_context() + file_obj = self.request.POST.get(self.upload_key) + + if file_obj is None: + return {'store_fid': None, + 'access_path': None, + 'error': '{} data field is missing'.format(self.upload_key)} + + if not hasattr(file_obj, 'filename'): + return {'store_fid': None, + 'access_path': None, + 'error': 'filename cannot be read from the data field'} + + filename = file_obj.filename + + metadata = { + 'filename': filename, + 'size': '', # filled by save_file + 'user_uploaded': {'username': self._rhodecode_user.username, + 'user_id': self._rhodecode_user.user_id, + 'ip': self._rhodecode_user.ip_addr}} + try: + store_fid = self.storage.save_file(file_obj.file, filename, + metadata=metadata) + except FileNotAllowedException: + return {'store_fid': None, + 'access_path': None, + 'error': 'File {} is not allowed.'.format(filename)} + + except FileOverSizeException: + return {'store_fid': None, + 'access_path': None, + 'error': 'File {} is exceeding allowed limit.'.format(filename)} + + return {'store_fid': store_fid, + 'access_path': h.route_path('download_file', fid=store_fid)} + + @view_config(route_name='download_file') + def download_file(self): + self.load_default_context() + file_uid = self.request.matchdict['fid'] + log.debug('Requesting FID:%s from store %s', file_uid, self.storage) + if not self.storage.exists(file_uid): + log.debug('File with FID:%s not found in the store', file_uid) + raise HTTPNotFound() + + file_path = self.storage.store_path(file_uid) + return FileResponse(file_path) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -281,6 +281,7 @@ def includeme(config): config.include('rhodecode.apps.ops') config.include('rhodecode.apps.admin') config.include('rhodecode.apps.channelstream') + config.include('rhodecode.apps.upload_store') config.include('rhodecode.apps.login') config.include('rhodecode.apps.home') config.include('rhodecode.apps.journal') diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -136,6 +136,8 @@ function registerRCRoutes() { pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []); pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []); pyroutes.register('channelstream_proxy', '/_channelstream', []); + pyroutes.register('upload_file', '/_file_store/upload', []); + pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']); pyroutes.register('logout', '/_admin/logout', []); pyroutes.register('reset_password', '/_admin/password_reset', []); pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []); diff --git a/rhodecode/tests/utils.py b/rhodecode/tests/utils.py --- a/rhodecode/tests/utils.py +++ b/rhodecode/tests/utils.py @@ -101,8 +101,7 @@ class CustomTestResponse(TestResponse): """ from pyramid_beaker import session_factory_from_settings - session = session_factory_from_settings( - self.test_app.app.config.get_settings()) + session = session_factory_from_settings(self.test_app._pyramid_settings) return session(self.request) @@ -140,6 +139,14 @@ class CustomTestApp(TestApp): def csrf_token(self): return self.rc_login_data['csrf_token'] + @property + def _pyramid_registry(self): + return self.app.config.registry + + @property + def _pyramid_settings(self): + return self._pyramid_registry.settings + def set_anonymous_access(enabled): """(Dis)allows anonymous access depending on parameter `enabled`"""