Beispiel #1
0
def create_app(config: 'SDConfig') -> Flask:
    app = Flask(__name__,
                template_folder=config.JOURNALIST_TEMPLATES_DIR,
                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))

    app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS)
    app.session_interface = JournalistInterfaceSessionInterface()

    csrf = CSRFProtect(app)
    Environment(app)

    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.config['SQLALCHEMY_DATABASE_URI'] = config.DATABASE_URI
    db.init_app(app)

    v2_enabled = path.exists(
        path.join(config.SECUREDROP_DATA_ROOT, 'source_v2_url'))
    v3_enabled = path.exists(
        path.join(config.SECUREDROP_DATA_ROOT, 'source_v3_url'))
    app.config.update(V2_ONION_ENABLED=v2_enabled, V3_ONION_ENABLED=v3_enabled)

    # TODO: Attaching a Storage dynamically like this disables all type checking (and
    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
    app.storage = Storage(config.STORE_DIR, config.TEMP_DIR,
                          config.JOURNALIST_KEY)

    # TODO: Attaching a CryptoUtil dynamically like this disables all type checking (and
    # breaks code analysis tools) for code that uses current_app.storage; it should be refactored
    app.crypto_util = CryptoUtil(
        scrypt_params=config.SCRYPT_PARAMS,
        scrypt_id_pepper=config.SCRYPT_ID_PEPPER,
        scrypt_gpg_pepper=config.SCRYPT_GPG_PEPPER,
        securedrop_root=config.SECUREDROP_ROOT,
        word_list=config.WORD_LIST,
        nouns_file=config.NOUNS,
        adjectives_file=config.ADJECTIVES,
        gpg_key_dir=config.GPG_KEY_DIR,
    )

    @app.errorhandler(CSRFError)
    def handle_csrf_error(e: CSRFError) -> 'Response':
        # render the message first to ensure it's localized.
        msg = gettext('You have been logged out due to inactivity.')
        session.clear()
        flash(msg, 'error')
        return redirect(url_for('main.login'))

    def _handle_http_exception(
        error: 'HTTPException'
    ) -> 'Tuple[Union[Response, str], Optional[int]]':
        # Workaround for no blueprint-level 404/5 error handlers, see:
        # https://github.com/pallets/flask/issues/503#issuecomment-71383286
        handler = list(app.error_handler_spec['api'][error.code].values())[0]
        if request.path.startswith('/api/') and handler:
            return handler(error)

        return render_template('error.html', error=error), error.code

    for code in default_exceptions:
        app.errorhandler(code)(_handle_http_exception)

    i18n.setup_app(config, app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals['version'] = version.__version__
    app.jinja_env.filters['rel_datetime_format'] = \
        template_filters.rel_datetime_format
    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat

    @app.before_first_request
    def expire_blacklisted_tokens() -> None:
        cleanup_expired_revoked_tokens()

    @app.before_request
    def load_instance_config() -> None:
        app.instance_config = InstanceConfig.get_current()

    @app.before_request
    def setup_g() -> 'Optional[Response]':
        """Store commonly used values in Flask's special g object"""
        if 'expires' in session and datetime.utcnow() >= session['expires']:
            session.clear()
            flash(gettext('You have been logged out due to inactivity.'),
                  'error')

        uid = session.get('uid', None)
        if uid:
            user = Journalist.query.get(uid)
            if user and 'nonce' in session and \
               session['nonce'] != user.session_nonce:
                session.clear()
                flash(
                    gettext('You have been logged out due to password change'),
                    'error')

        session['expires'] = datetime.utcnow() + \
            timedelta(minutes=getattr(config,
                                      'SESSION_EXPIRATION_MINUTES',
                                      120))

        # Work around https://github.com/lepture/flask-wtf/issues/275
        # -- after upgrading from Python 2 to Python 3, any existing
        # session's csrf_token value will be retrieved as bytes,
        # causing a TypeError. This simple fix, deleting the existing
        # token, was suggested in the issue comments. This code will
        # be safe to remove after Python 2 reaches EOL in 2020, and no
        # supported SecureDrop installations can still have this
        # problem.
        if sys.version_info.major > 2 and type(
                session.get('csrf_token')) is bytes:
            del session['csrf_token']

        uid = session.get('uid', None)
        if uid:
            g.user = Journalist.query.get(uid)

        g.locale = i18n.get_locale(config)
        g.text_direction = i18n.get_text_direction(g.locale)
        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
        g.locales = i18n.get_locale2name()

        if not app.config['V3_ONION_ENABLED'] or app.config['V2_ONION_ENABLED']:
            g.show_v2_onion_eol_warning = True

        if request.path.split('/')[1] == 'api':
            pass  # We use the @token_required decorator for the API endpoints
        else:  # We are not using the API
            if request.endpoint not in _insecure_views and not logged_in():
                return redirect(url_for('main.login'))

        if request.method == 'POST':
            filesystem_id = request.form.get('filesystem_id')
            if filesystem_id:
                g.filesystem_id = filesystem_id
                g.source = get_source(filesystem_id)

        return None

    app.register_blueprint(main.make_blueprint(config))
    app.register_blueprint(account.make_blueprint(config),
                           url_prefix='/account')
    app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')
    app.register_blueprint(col.make_blueprint(config), url_prefix='/col')
    api_blueprint = api.make_blueprint(config)
    app.register_blueprint(api_blueprint, url_prefix='/api/v1')
    csrf.exempt(api_blueprint)

    return app
Beispiel #2
0
def create_app(config):
    app = Flask(__name__,
                template_folder=config.JOURNALIST_TEMPLATES_DIR,
                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))

    app.config.from_object(config.JournalistInterfaceFlaskConfig)

    CSRFProtect(app)
    Environment(app)

    @app.errorhandler(CSRFError)
    def handle_csrf_error(e):
        # render the message first to ensure it's localized.
        msg = gettext('You have been logged out due to inactivity')
        session.clear()
        flash(msg, 'error')
        return redirect(url_for('main.login'))

    i18n.setup_app(config, app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals['version'] = version.__version__
    if hasattr(config, 'CUSTOM_HEADER_IMAGE'):
        app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
        app.jinja_env.globals['use_custom_header_image'] = True
    else:
        app.jinja_env.globals['header_image'] = 'logo.png'
        app.jinja_env.globals['use_custom_header_image'] = False

    app.jinja_env.filters['rel_datetime_format'] = \
        template_filters.rel_datetime_format
    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat

    @app.template_filter('autoversion')
    def autoversion_filter(filename):
        """Use this template filter for cache busting"""
        absolute_filename = path.join(config.SECUREDROP_ROOT, filename[1:])
        if path.exists(absolute_filename):
            timestamp = str(path.getmtime(absolute_filename))
        else:
            return filename
        versioned_filename = "{0}?v={1}".format(filename, timestamp)
        return versioned_filename

    @app.teardown_appcontext
    def shutdown_session(exception=None):
        """Automatically remove database sessions at the end of the request, or
        when the application shuts down"""
        db_session.remove()

    @app.before_request
    def setup_g():
        """Store commonly used values in Flask's special g object"""
        if 'expires' in session and datetime.utcnow() >= session['expires']:
            session.clear()
            flash(gettext('You have been logged out due to inactivity'),
                  'error')

        session['expires'] = datetime.utcnow() + \
            timedelta(minutes=getattr(config,
                                      'SESSION_EXPIRATION_MINUTES',
                                      120))

        uid = session.get('uid', None)
        if uid:
            g.user = Journalist.query.get(uid)

        g.locale = i18n.get_locale(config)
        g.text_direction = i18n.get_text_direction(g.locale)
        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
        g.locales = i18n.get_locale2name()

        if request.endpoint not in _insecure_views and not logged_in():
            return redirect(url_for('main.login'))

        if request.method == 'POST':
            filesystem_id = request.form.get('filesystem_id')
            if filesystem_id:
                g.filesystem_id = filesystem_id
                g.source = get_source(filesystem_id)

    app.register_blueprint(main.make_blueprint(config))
    app.register_blueprint(account.make_blueprint(config),
                           url_prefix='/account')
    app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')
    app.register_blueprint(col.make_blueprint(config), url_prefix='/col')

    return app
Beispiel #3
0
def create_app(config):
    app = Flask(__name__,
                template_folder=config.JOURNALIST_TEMPLATES_DIR,
                static_folder=path.join(config.SECUREDROP_ROOT, 'static'))

    app.config.from_object(config.JournalistInterfaceFlaskConfig)

    CSRFProtect(app)
    Environment(app)

    @app.errorhandler(CSRFError)
    def handle_csrf_error(e):
        # render the message first to ensure it's localized.
        msg = gettext('You have been logged out due to inactivity')
        session.clear()
        flash(msg, 'error')
        return redirect(url_for('main.login'))

    i18n.setup_app(app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals['version'] = version.__version__
    if hasattr(config, 'CUSTOM_HEADER_IMAGE'):
        app.jinja_env.globals['header_image'] = config.CUSTOM_HEADER_IMAGE
        app.jinja_env.globals['use_custom_header_image'] = True
    else:
        app.jinja_env.globals['header_image'] = 'logo.png'
        app.jinja_env.globals['use_custom_header_image'] = False

    app.jinja_env.filters['rel_datetime_format'] = \
        template_filters.rel_datetime_format
    app.jinja_env.filters['filesizeformat'] = template_filters.filesizeformat

    @app.teardown_appcontext
    def shutdown_session(exception=None):
        """Automatically remove database sessions at the end of the request, or
        when the application shuts down"""
        db_session.remove()

    @app.before_request
    def setup_g():
        """Store commonly used values in Flask's special g object"""
        if 'expires' in session and datetime.utcnow() >= session['expires']:
            session.clear()
            flash(gettext('You have been logged out due to inactivity'),
                  'error')

        session['expires'] = datetime.utcnow() + \
            timedelta(minutes=getattr(config,
                                      'SESSION_EXPIRATION_MINUTES',
                                      120))

        uid = session.get('uid', None)
        if uid:
            g.user = Journalist.query.get(uid)

        g.locale = i18n.get_locale()
        g.text_direction = i18n.get_text_direction(g.locale)
        g.html_lang = i18n.locale_to_rfc_5646(g.locale)
        g.locales = i18n.get_locale2name()

        if request.endpoint not in _insecure_views and not logged_in():
            return redirect(url_for('main.login'))

        if request.method == 'POST':
            filesystem_id = request.form.get('filesystem_id')
            if filesystem_id:
                g.filesystem_id = filesystem_id
                g.source = get_source(filesystem_id)

    app.register_blueprint(main.make_blueprint(config))
    app.register_blueprint(account.make_blueprint(config),
                           url_prefix='/account')
    app.register_blueprint(admin.make_blueprint(config), url_prefix='/admin')
    app.register_blueprint(col.make_blueprint(config), url_prefix='/col')

    return app
Beispiel #4
0
def create_app(config: "SDConfig") -> Flask:
    app = Flask(
        __name__,
        template_folder=config.JOURNALIST_TEMPLATES_DIR,
        static_folder=path.join(config.SECUREDROP_ROOT, "static"),
    )

    app.config.from_object(config.JOURNALIST_APP_FLASK_CONFIG_CLS)
    app.session_interface = JournalistInterfaceSessionInterface()

    csrf = CSRFProtect(app)

    app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
    app.config["SQLALCHEMY_DATABASE_URI"] = config.DATABASE_URI
    db.init_app(app)

    class JSONEncoder(json.JSONEncoder):
        """Custom JSON encoder to use our preferred timestamp format"""

        def default(self, obj: "Any") -> "Any":
            if isinstance(obj, datetime):
                return obj.strftime(API_DATETIME_FORMAT)
            super(JSONEncoder, self).default(obj)

    app.json_encoder = JSONEncoder

    # TODO: enable type checking once upstream Flask fix is available. See:
    # https://github.com/pallets/flask/issues/4295
    @app.errorhandler(CSRFError)  # type: ignore
    def handle_csrf_error(e: CSRFError) -> "Response":
        app.logger.error("The CSRF token is invalid.")
        session.clear()
        msg = gettext("You have been logged out due to inactivity.")
        flash(msg, "error")
        return redirect(url_for("main.login"))

    def _handle_http_exception(
        error: "HTTPException",
    ) -> "Tuple[Union[Response, str], Optional[int]]":
        # Workaround for no blueprint-level 404/5 error handlers, see:
        # https://github.com/pallets/flask/issues/503#issuecomment-71383286
        # TODO: clean up API error handling such that all except 404/5s are
        # registered in the blueprint and 404/5s are handled at the application
        # level.
        handler = list(app.error_handler_spec["api"][error.code].values())[0]
        if request.path.startswith("/api/") and handler:
            return handler(error)  # type: ignore

        return render_template("error.html", error=error), error.code

    for code in default_exceptions:
        app.errorhandler(code)(_handle_http_exception)

    i18n.configure(config, app)

    app.jinja_env.trim_blocks = True
    app.jinja_env.lstrip_blocks = True
    app.jinja_env.globals["version"] = version.__version__
    app.jinja_env.filters["rel_datetime_format"] = template_filters.rel_datetime_format
    app.jinja_env.filters["filesizeformat"] = template_filters.filesizeformat
    app.jinja_env.filters["html_datetime_format"] = template_filters.html_datetime_format
    app.jinja_env.add_extension("jinja2.ext.do")

    @app.before_first_request
    def expire_blacklisted_tokens() -> None:
        cleanup_expired_revoked_tokens()

    @app.before_request
    def update_instance_config() -> None:
        InstanceConfig.get_default(refresh=True)

    @app.before_request
    def setup_g() -> "Optional[Response]":
        """Store commonly used values in Flask's special g object"""
        if "expires" in session and datetime.now(timezone.utc) >= session["expires"]:
            session.clear()
            flash(gettext("You have been logged out due to inactivity."), "error")

        uid = session.get("uid", None)
        if uid:
            user = Journalist.query.get(uid)
            if user and "nonce" in session and session["nonce"] != user.session_nonce:
                session.clear()
                flash(gettext("You have been logged out due to password change"), "error")

        session["expires"] = datetime.now(timezone.utc) + timedelta(
            minutes=getattr(config, "SESSION_EXPIRATION_MINUTES", 120)
        )

        uid = session.get("uid", None)
        if uid:
            g.user = Journalist.query.get(uid)  # pylint: disable=assigning-non-slot

        i18n.set_locale(config)

        if InstanceConfig.get_default().organization_name:
            g.organization_name = (  # pylint: disable=assigning-non-slot
                InstanceConfig.get_default().organization_name
            )
        else:
            g.organization_name = gettext("SecureDrop")  # pylint: disable=assigning-non-slot

        try:
            g.logo = get_logo_url(app)  # pylint: disable=assigning-non-slot
        except FileNotFoundError:
            app.logger.error("Site logo not found.")

        if request.path.split("/")[1] == "api":
            pass  # We use the @token_required decorator for the API endpoints
        else:  # We are not using the API
            if request.endpoint not in _insecure_views and not logged_in():
                return redirect(url_for("main.login"))

        if request.method == "POST":
            filesystem_id = request.form.get("filesystem_id")
            if filesystem_id:
                g.filesystem_id = filesystem_id  # pylint: disable=assigning-non-slot
                g.source = get_source(filesystem_id)  # pylint: disable=assigning-non-slot

        return None

    app.register_blueprint(main.make_blueprint(config))
    app.register_blueprint(account.make_blueprint(config), url_prefix="/account")
    app.register_blueprint(admin.make_blueprint(config), url_prefix="/admin")
    app.register_blueprint(col.make_blueprint(config), url_prefix="/col")
    api_blueprint = api.make_blueprint(config)
    app.register_blueprint(api_blueprint, url_prefix="/api/v1")
    csrf.exempt(api_blueprint)

    return app