diff --git a/storage_s3/indico_storage_s3/__init__.py b/storage_s3/indico_storage_s3/__init__.py index 9eaa627..7d4e5ea 100644 --- a/storage_s3/indico_storage_s3/__init__.py +++ b/storage_s3/indico_storage_s3/__init__.py @@ -17,6 +17,10 @@ from __future__ import unicode_literals from indico.core import signals +from indico.util.i18n import make_bound_gettext + + +_ = make_bound_gettext('search_cern') @signals.import_tasks.connect diff --git a/storage_s3/indico_storage_s3/blueprint.py b/storage_s3/indico_storage_s3/blueprint.py new file mode 100644 index 0000000..5d3b7ac --- /dev/null +++ b/storage_s3/indico_storage_s3/blueprint.py @@ -0,0 +1,25 @@ +# This file is part of Indico. +# Copyright (C) 2002 - 2018 European Organization for Nuclear Research (CERN). +# +# Indico is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Indico 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 General Public License +# along with Indico; if not, see . + +from __future__ import unicode_literals + +from indico.core.plugins import IndicoPluginBlueprint + +from indico_storage_s3.controllers import RHBuckets + + +blueprint = IndicoPluginBlueprint('storage_s3', __name__, url_prefix='/api/plugin/s3') +blueprint.add_url_rule('/buckets', 'buckets', RHBuckets) diff --git a/storage_s3/indico_storage_s3/controllers.py b/storage_s3/indico_storage_s3/controllers.py new file mode 100644 index 0000000..1c1ae27 --- /dev/null +++ b/storage_s3/indico_storage_s3/controllers.py @@ -0,0 +1,77 @@ +# This file is part of Indico. +# Copyright (C) 2002 - 2018 European Organization for Nuclear Research (CERN). +# +# Indico is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Indico 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 General Public License +# along with Indico; if not, see . + +from __future__ import unicode_literals + +from flask import current_app, jsonify, request +from werkzeug.exceptions import NotFound, Unauthorized + +from indico.core.config import config +from indico.core.db import db +from indico.core.storage import StoredFileMixin +from indico.core.storage.backend import ReadOnlyStorageMixin, get_storage +from indico.web.rh import RH + +from indico_storage_s3.storage import DynamicS3Storage, S3Storage, S3StorageBase + + +class RHBuckets(RH): + """Provide information on used S3 buckets""" + + def _check_access(self): + from indico_storage_s3.plugin import S3StoragePlugin + auth = request.authorization + if not S3StoragePlugin.settings.get('bucket_info_enabled'): + raise NotFound + username = S3StoragePlugin.settings.get('username') + password = S3StoragePlugin.settings.get('password') + if not auth or not auth.password or auth.username != username or auth.password != password: + response = current_app.response_class('Authorization required', 401, + {'WWW-Authenticate': 'Basic realm="Indico - S3 Buckets"'}) + raise Unauthorized(response=response) + + def _get_static_info(self, storage): + return { + 'bucket': storage.bucket_name, + 'dynamic': False, + } + + def _get_dynamic_info(self, backend_name, storage): + buckets = set() + for model in StoredFileMixin.__subclasses__(): + query = (db.session.query(db.func.split_part(model.storage_file_id, '//', 1)) + .filter(model.storage_file_id.isnot(None), model.storage_backend == backend_name)) + buckets.update(bucket for bucket, in query) + + return { + 'buckets': sorted(buckets), + 'dynamic': True, + 'template': storage.bucket_name_template, + } + + def _process(self): + data = {} + for key in config.STORAGE_BACKENDS: + storage = get_storage(key) + if not isinstance(storage, S3StorageBase): + continue + readonly = isinstance(storage, ReadOnlyStorageMixin) + data[key] = {'readonly': readonly} + if isinstance(storage, S3Storage): + data[key].update(self._get_static_info(storage)) + elif isinstance(storage, DynamicS3Storage): + data[key].update(self._get_dynamic_info(key, storage)) + return jsonify(data) diff --git a/storage_s3/indico_storage_s3/plugin.py b/storage_s3/indico_storage_s3/plugin.py index ece4766..f7b0526 100644 --- a/storage_s3/indico_storage_s3/plugin.py +++ b/storage_s3/indico_storage_s3/plugin.py @@ -19,23 +19,56 @@ from __future__ import unicode_literals import sys import click +from markupsafe import Markup +from wtforms.fields import BooleanField, StringField +from wtforms.validators import DataRequired from indico.cli.core import cli_command from indico.core import signals from indico.core.config import config -from indico.core.plugins import IndicoPlugin +from indico.core.plugins import IndicoPlugin, url_for_plugin from indico.core.storage.backend import ReadOnlyStorageMixin, get_storage +from indico.web.forms.base import IndicoForm +from indico.web.forms.fields import IndicoPasswordField +from indico.web.forms.validators import HiddenUnless +from indico.web.forms.widgets import SwitchWidget +from indico_storage_s3 import _ +from indico_storage_s3.blueprint import blueprint from indico_storage_s3.storage import (DynamicS3Storage, ReadOnlyDynamicS3Storage, ReadOnlyS3Storage, S3Storage, S3StorageBase) +class SettingsForm(IndicoForm): + bucket_info_enabled = BooleanField(_("Bucket info API"), widget=SwitchWidget()) + username = StringField(_("Username"), [HiddenUnless('bucket_info_enabled', preserve_data=True), DataRequired()], + description=_("The username to access the S3 bucket info endpoint")) + password = IndicoPasswordField(_('Password'), + [HiddenUnless('bucket_info_enabled', preserve_data=True), DataRequired()], + toggle=True, + description=_("The password to access the S3 bucket info endpoint")) + + def __init__(self, *args, **kwargs): + super(SettingsForm, self).__init__(*args, **kwargs) + url = Markup('{}').format(url_for_plugin('storage_s3.buckets')) + self.bucket_info_enabled.description = _("Enables an API on {url} that returns information on all S3 buckets " + "currently in use, including dynamically-named ones.").format(url=url) + + class S3StoragePlugin(IndicoPlugin): """S3 Storage Provides S3 storage backends. """ + configurable = True + settings_form = SettingsForm + default_settings = { + 'bucket_info_enabled': False, + 'username': '', + 'password': '' + } + def init(self): super(S3StoragePlugin, self).init() self.connect(signals.get_storage_backends, self._get_storage_backends) @@ -47,6 +80,9 @@ class S3StoragePlugin(IndicoPlugin): yield ReadOnlyS3Storage yield ReadOnlyDynamicS3Storage + def get_blueprints(self): + return blueprint + def _extend_indico_cli(self, sender, **kwargs): @cli_command() @click.option('--storage', default=None, metavar='NAME', help='Storage backend to create bucket for')