From 209ce7056e9bf3bc213dc17209b379adf13b73c9 Mon Sep 17 00:00:00 2001 From: William May Date: Tue, 10 Dec 2024 12:20:38 +0000 Subject: [PATCH] FS-4805 - Centralising authentication route decoration Instead of having login_required and check_internal_user added above each and every route, we can have a central @app.before_request decorator to add the decorators to every route, except those that require public access - static, healthcheck, login and index. --- app/blueprints/dev/routes.py | 17 ---------------- app/blueprints/fund_builder/routes.py | 29 --------------------------- app/blueprints/self_serve/routes.py | 17 ---------------- app/blueprints/templates/routes.py | 7 ------- app/create_app.py | 24 ++++++++++++++++++++++ 5 files changed, 24 insertions(+), 70 deletions(-) diff --git a/app/blueprints/dev/routes.py b/app/blueprints/dev/routes.py index aa57abca..b0396367 100644 --- a/app/blueprints/dev/routes.py +++ b/app/blueprints/dev/routes.py @@ -3,9 +3,6 @@ from flask import redirect from flask import request from flask import url_for -from fsd_utils.authentication.decorators import SupportedApp -from fsd_utils.authentication.decorators import check_internal_user -from fsd_utils.authentication.decorators import login_required from app.blueprints.self_serve.data.data_access import clear_all_responses from app.blueprints.self_serve.data.data_access import get_all_components @@ -25,8 +22,6 @@ @dev_bp.route("/responses") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_responses(): """ Retrieves all responses received from a 'Save per page' callback when a form is in preview mode. @@ -37,8 +32,6 @@ def view_responses(): @dev_bp.route("/responses/clear") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def clear_responses(): """ Clears all responses received from a 'Save per page' callback when a form is in preview mode. @@ -48,8 +41,6 @@ def clear_responses(): @dev_bp.route("/save", methods=["PUT"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def save_per_page(): """ Mock version of the 'save per page' route in application store - used to save and enable viewing of save @@ -75,32 +66,24 @@ def save_per_page(): @dev_bp.route("/forms") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_forms(): forms = get_saved_forms() return forms @dev_bp.route("/pages") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_pages(): forms = get_all_pages() return forms @dev_bp.route("/sections") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_sections(): forms = get_all_sections() return forms @dev_bp.route("/questions") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_questions(): forms = get_all_components() return forms diff --git a/app/blueprints/fund_builder/routes.py b/app/blueprints/fund_builder/routes.py index 8f92dc66..28e59632 100644 --- a/app/blueprints/fund_builder/routes.py +++ b/app/blueprints/fund_builder/routes.py @@ -17,10 +17,7 @@ from flask import request from flask import send_file from flask import url_for -from fsd_utils.authentication.decorators import SupportedApp -from fsd_utils.authentication.decorators import check_internal_user from fsd_utils.authentication.decorators import login_requested -from fsd_utils.authentication.decorators import login_required from app.all_questions.metadata_utils import generate_print_data_for_sections from app.blueprints.fund_builder.forms.fund import FundForm @@ -92,15 +89,11 @@ def login(): @build_fund_bp.route("/dashboard", methods=["GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def dashboard(): return render_template("index.html") @build_fund_bp.route("/fund/round//section", methods=["GET", "POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def section(round_id): round_obj = get_round_by_id(round_id) fund_obj = get_fund_by_id(round_obj.fund_id) @@ -163,8 +156,6 @@ def section(round_id): @build_fund_bp.route("/fund/round//section//forms", methods=["POST", "GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def configure_forms_in_section(round_id, section_id): if request.method == "GET": if request.args.get("action") == "remove": @@ -193,8 +184,6 @@ def all_funds_as_govuk_select_items(all_funds: list) -> list: @build_fund_bp.route("/fund/view", methods=["GET", "POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_fund(): """ Renders a template providing a drop down list of funds. If a fund is selected, renders its config info @@ -218,8 +207,6 @@ def view_fund(): @build_fund_bp.route("/fund/round//application_config") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def build_application(round_id): """ Renders a template displaying application configuration info for the chosen round @@ -235,8 +222,6 @@ def build_application(round_id): @build_fund_bp.route("/fund//round//clone") -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def clone_round(round_id, fund_id): cloned = clone_single_round( round_id=round_id, new_fund_id=fund_id, new_short_name=f"R-C{randint(0, 999)}" # nosec B311 @@ -248,8 +233,6 @@ def clone_round(round_id, fund_id): @build_fund_bp.route("/fund", methods=["GET", "POST"]) @build_fund_bp.route("/fund/", methods=["GET", "POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def fund(fund_id=None): """ Renders a template to allow a user to add or update a fund, when saved validates the form data and saves to DB @@ -315,8 +298,6 @@ def fund(fund_id=None): @build_fund_bp.route("/round", methods=["GET", "POST"]) @build_fund_bp.route("/round/", methods=["GET", "POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def round(round_id=None): """ Renders a template to select a fund and add or update a round to that fund. If saved, validates the round form data @@ -572,8 +553,6 @@ def create_new_round(form): @build_fund_bp.route("/preview/", methods=["GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def preview_form(form_id): """ Generates the form json for a chosen form, does not persist this, but publishes it to the form runner using the @@ -595,8 +574,6 @@ def preview_form(form_id): @build_fund_bp.route("/download/", methods=["GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def download_form_json(form_id): """ Generates form json for the selected form and returns it as a file download @@ -612,8 +589,6 @@ def download_form_json(form_id): @build_fund_bp.route("/fund/round//all_questions", methods=["GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_all_questions(round_id): """ Generates the form data for all sections in the selected round, then uses that to generate the 'All Questions' @@ -642,8 +617,6 @@ def view_all_questions(round_id): @build_fund_bp.route("/fund/round//all_questions/", methods=["GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_form_questions(round_id, form_id): """ Generates the form data for this form, then uses that to generate the 'All Questions' @@ -679,8 +652,6 @@ def create_export_zip(directory_to_zip, zip_file_name, random_post_fix) -> str: @build_fund_bp.route("/create_export_files/", methods=["GET"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def create_export_files(round_id): round_short_name = get_round_by_id(round_id).short_name # Construct the path to the output directory relative to this file's location diff --git a/app/blueprints/self_serve/routes.py b/app/blueprints/self_serve/routes.py index 188c18bd..5b254854 100644 --- a/app/blueprints/self_serve/routes.py +++ b/app/blueprints/self_serve/routes.py @@ -8,9 +8,6 @@ from flask import render_template from flask import request from flask import url_for -from fsd_utils.authentication.decorators import SupportedApp -from fsd_utils.authentication.decorators import check_internal_user -from fsd_utils.authentication.decorators import login_required from app.all_questions.metadata_utils import generate_print_data_for_sections from app.blueprints.self_serve.data.data_access import get_all_components @@ -41,8 +38,6 @@ @self_serve_bp.route("/download_json", methods=["POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def generate_json(): form_json = generate_form_config_from_request()["form_json"] @@ -71,8 +66,6 @@ def generate_form_config_from_request(): @self_serve_bp.route("/form_questions", methods=["POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_form_questions(): form_config = generate_form_config_from_request() print_data = generate_print_data_for_sections( @@ -89,8 +82,6 @@ def view_form_questions(): @self_serve_bp.route("/section_questions", methods=["POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_section_questions(): # form_config = generate_form_config_from_request() # print_data = generate_print_data_for_sections( @@ -112,8 +103,6 @@ def view_section_questions(): # Create routes @self_serve_bp.route("section", methods=["GET", "POST", "PUT", "DELETE"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def section(): # TODO: Create frontend routes and connect to middleware if request.method == "PUT": @@ -142,8 +131,6 @@ def section(): @self_serve_bp.route("/form", methods=["GET", "POST", "PUT", "DELETE"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def form(): # TODO: Create frontend routes and connect to middleware if request.method == "PUT": @@ -182,8 +169,6 @@ def form(): @self_serve_bp.route("/page", methods=["GET", "POST", "PUT", "DELETE"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def page(): # TODO: Create frontend routes and connect to middleware if request.method == "PUT": @@ -216,8 +201,6 @@ def page(): @self_serve_bp.route("/question", methods=["GET", "PUT", "POST", "DELETE"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def question(): # TODO: Create frontend routes and connect to middleware if request.method == "PUT": diff --git a/app/blueprints/templates/routes.py b/app/blueprints/templates/routes.py index 2a50aed4..ab24556d 100644 --- a/app/blueprints/templates/routes.py +++ b/app/blueprints/templates/routes.py @@ -5,9 +5,6 @@ from flask import render_template from flask import request from flask import url_for -from fsd_utils.authentication.decorators import SupportedApp -from fsd_utils.authentication.decorators import check_internal_user -from fsd_utils.authentication.decorators import login_required from werkzeug.utils import secure_filename from app.blueprints.fund_builder.forms.templates import TemplateFormForm @@ -57,8 +54,6 @@ def _build_rows(forms: list[Form]) -> list[dict]: @template_bp.route("/all", methods=["GET", "POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def view_templates(): sections = get_all_template_sections() forms = get_all_template_forms() @@ -97,8 +92,6 @@ def view_templates(): @template_bp.route("/forms/", methods=["GET", "POST"]) -@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) -@check_internal_user def edit_form_template(form_id): template_form = TemplateFormForm() params = { diff --git a/app/create_app.py b/app/create_app.py index 5028f4bb..3fa34098 100644 --- a/app/create_app.py +++ b/app/create_app.py @@ -1,6 +1,9 @@ from flask import Flask from flask import render_template from flask_assets import Environment +from fsd_utils.authentication.decorators import SupportedApp +from fsd_utils.authentication.decorators import check_internal_user +from fsd_utils.authentication.decorators import login_required from fsd_utils.logging import logging from jinja2 import ChoiceLoader from jinja2 import PackageLoader @@ -15,6 +18,25 @@ from app.db.models import Round # noqa:F401 +PUBLIC_ROUTES = [ + "static", + "build_fund_bp.healthcheck", + "build_fund_bp.index", + "build_fund_bp.login", +] + + +def protect_private_routes(flask_app: Flask) -> Flask: + for endpoint, view_func in flask_app.view_functions.items(): + if endpoint in PUBLIC_ROUTES: + continue + flask_app.view_functions[endpoint] = login_required( + check_internal_user(view_func), + return_app=SupportedApp.FUND_APPLICATION_BUILDER + ) + return flask_app + + def create_app() -> Flask: flask_app = Flask("__name__", static_url_path="/assets") @@ -23,6 +45,8 @@ def create_app() -> Flask: flask_app.register_blueprint(build_fund_bp) flask_app.register_blueprint(template_bp) + protect_private_routes(flask_app) + flask_app.config.from_object("config.Config") flask_app.static_folder = "app/static/dist"