From dcccc5a86c3a20d658bcbfc97096e12c18cdb6dd Mon Sep 17 00:00:00 2001 From: William May Date: Fri, 29 Nov 2024 14:48:39 +0000 Subject: [PATCH] FS-4805 - Adding authentication to FAB --- app/blueprints/fund_builder/routes.py | 30 +++++++++++++++++++ .../fund_builder/templates/login.html | 21 +++++++++++++ config/envs/default.py | 10 +++++++ config/envs/development.py | 9 ++++++ pyproject.toml | 2 +- uv.lock | 10 +++---- 6 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 app/blueprints/fund_builder/templates/login.html diff --git a/app/blueprints/fund_builder/routes.py b/app/blueprints/fund_builder/routes.py index f2d7bdbc..6aa96ca2 100644 --- a/app/blueprints/fund_builder/routes.py +++ b/app/blueprints/fund_builder/routes.py @@ -11,11 +11,15 @@ from flask import Response from flask import after_this_request from flask import flash +from flask import g from flask import redirect from flask import render_template 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 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 @@ -67,11 +71,26 @@ @build_fund_bp.route("/") +@login_requested def index(): + if not g.is_authenticated: + return redirect(url_for("build_fund_bp.login")) + return redirect(url_for("build_fund_bp.dashboard")) + + +@build_fund_bp.route("/login", methods=["GET"]) +def login(): + return render_template("login.html") + + +@build_fund_bp.route("/dashboard", methods=["GET"]) +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) +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) def section(round_id): round_obj = get_round_by_id(round_id) fund_obj = get_fund_by_id(round_obj.fund_id) @@ -134,6 +153,7 @@ def section(round_id): @build_fund_bp.route("/fund/round//section//forms", methods=["POST", "GET"]) +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) def configure_forms_in_section(round_id, section_id): if request.method == "GET": if request.args.get("action") == "remove": @@ -162,6 +182,7 @@ 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) def view_fund(): """ Renders a template providing a drop down list of funds. If a fund is selected, renders its config info @@ -185,6 +206,7 @@ def view_fund(): @build_fund_bp.route("/fund/round//application_config") +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) def build_application(round_id): """ Renders a template displaying application configuration info for the chosen round @@ -200,6 +222,7 @@ def build_application(round_id): @build_fund_bp.route("/fund//round//clone") +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) 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 @@ -211,6 +234,7 @@ 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) 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 @@ -276,6 +300,7 @@ 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) 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 @@ -531,6 +556,7 @@ def create_new_round(form): @build_fund_bp.route("/preview/", methods=["GET"]) +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) 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 @@ -552,6 +578,7 @@ def preview_form(form_id): @build_fund_bp.route("/download/", methods=["GET"]) +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) def download_form_json(form_id): """ Generates form json for the selected form and returns it as a file download @@ -567,6 +594,7 @@ def download_form_json(form_id): @build_fund_bp.route("/fund/round//all_questions", methods=["GET"]) +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) 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' @@ -595,6 +623,7 @@ def view_all_questions(round_id): @build_fund_bp.route("/fund/round//all_questions/", methods=["GET"]) +@login_required(return_app=SupportedApp.FUND_APPLICATION_BUILDER) def view_form_questions(round_id, form_id): """ Generates the form data for this form, then uses that to generate the 'All Questions' @@ -630,6 +659,7 @@ 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) 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/fund_builder/templates/login.html b/app/blueprints/fund_builder/templates/login.html new file mode 100644 index 00000000..4f217e67 --- /dev/null +++ b/app/blueprints/fund_builder/templates/login.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{%- from 'govuk_frontend_jinja/components/button/macro.html' import govukButton -%} + +{# Override main width to two thirds #} +{% set mainClasses = "govuk-!-width-two-thirds" %} + +{% block content %} +
+

Sign in to use FAB

+

+ Sign in using the Microsoft account for your work email address. +

+ + {{ govukButton({ + "text": "Sign in using Microsoft", + "href": config['AUTHENTICATOR_HOST'] + "/sso/login?return_app=fund-application-builder", + "classes": "login-button" + }) }} +
+{% endblock content %} diff --git a/config/envs/default.py b/config/envs/default.py index 6253fc94..2e80cf39 100644 --- a/config/envs/default.py +++ b/config/envs/default.py @@ -1,3 +1,4 @@ +import base64 import logging from os import environ from os import getenv @@ -12,6 +13,7 @@ class DefaultConfig(object): # Logging FSD_LOG_LEVEL = logging.WARNING + FLASK_ENV = CommonConfig.FLASK_ENV SECRET_KEY = CommonConfig.SECRET_KEY FAB_HOST = getenv("FAB_HOST", "fab:8080/") @@ -22,3 +24,11 @@ class DefaultConfig(object): TEMP_FILE_PATH = Path("/tmp") GENERATE_LOCAL_CONFIG = False + + FSD_USER_TOKEN_COOKIE_NAME = "fsd_user_token" + AUTHENTICATOR_HOST = getenv("AUTHENTICATOR_HOST", "http://authenticator.levellingup.gov.localhost:3004") + + # RSA 256 Keys + RSA256_PUBLIC_KEY_BASE64 = getenv("RSA256_PUBLIC_KEY_BASE64") + if RSA256_PUBLIC_KEY_BASE64: + RSA256_PUBLIC_KEY: str = base64.b64decode(RSA256_PUBLIC_KEY_BASE64).decode() diff --git a/config/envs/development.py b/config/envs/development.py index 81aa652f..2e40584e 100644 --- a/config/envs/development.py +++ b/config/envs/development.py @@ -20,3 +20,12 @@ class DevelopmentConfig(Config): TEMP_FILE_PATH = Path("app") / "export_config" / "output" GENERATE_LOCAL_CONFIG = True + + DEBUG_USER_ON = True + DEBUG_USER = { + "full_name": "Development User", + "email": "dev@communities.gov.uk", + "roles": [], + "highest_role_map": {}, + } + DEBUG_USER_ACCOUNT_ID = "00000000-0000-0000-0000-000000000000" diff --git a/pyproject.toml b/pyproject.toml index 395c5922..531e71d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "flask-talisman==1.1.0", "flask-wtf==1.2.1", "flask==3.0.3", - "funding-service-design-utils==5.1.5", + "funding-service-design-utils==5.2.0", "govuk-frontend-jinja==3.4.0", "jsmin==3.0.1", "jsonschema==4.23.0", diff --git a/uv.lock b/uv.lock index 6f648964..a604aaa3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = "==3.10.*" +requires-python = ">=3.10.0, <3.11" resolution-markers = [ "(platform_machine == 'AMD64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32') or (platform_machine == 'WIN32' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32') or (platform_machine == 'amd64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32') or (platform_machine == 'ppc64le' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32') or (platform_machine == 'win32' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform == 'win32')", "(platform_machine == 'AMD64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32') or (platform_machine == 'WIN32' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32') or (platform_machine == 'amd64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32') or (platform_machine == 'ppc64le' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32') or (platform_machine == 'win32' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and platform_system == 'Windows' and sys_platform != 'win32')", @@ -616,7 +616,7 @@ requires-dist = [ { name = "flask-sqlalchemy", specifier = "==3.1.1" }, { name = "flask-talisman", specifier = "==1.1.0" }, { name = "flask-wtf", specifier = "==1.2.1" }, - { name = "funding-service-design-utils", specifier = "==5.1.5" }, + { name = "funding-service-design-utils", specifier = "==5.2.0" }, { name = "govuk-frontend-jinja", specifier = "==3.4.0" }, { name = "jsmin", specifier = "==3.0.1" }, { name = "jsonschema", specifier = "==4.23.0" }, @@ -650,7 +650,7 @@ dev = [ [[package]] name = "funding-service-design-utils" -version = "5.1.5" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -671,9 +671,9 @@ dependencies = [ { name = "sentry-sdk", extra = ["flask"] }, { name = "sqlalchemy-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/23/eeb6917cf0afbc55596a1d3f8f65ebcdbe843bf0092d1ad48e8be8b7c3a1/funding_service_design_utils-5.1.5.tar.gz", hash = "sha256:e5bdd17ed323316d1ab3ff4cd50e67f3998590e25da72d064d018dadf6eec6a9", size = 66862 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/f6/683e686a975db8074930e7c65203fafbe5e18c460e385e1072eae884301f/funding_service_design_utils-5.2.0.tar.gz", hash = "sha256:06bccd7814647ff1ee226eeaf323aaa1c284188bc03bbfdaa313ca0f0b8898ba", size = 67461 } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/f9/a252776eb791af4c2656b61e085a8a797b759b6158f70fc0f389687a4880/funding_service_design_utils-5.1.5-py3-none-any.whl", hash = "sha256:174692a111b35607baffb8630f361f71cb4cf32ac688903addbeb793e7a03f55", size = 80613 }, + { url = "https://files.pythonhosted.org/packages/41/6b/a6c4dc52793049e7bf13432881997f822a1ba13c9f30c35dc07be0e51296/funding_service_design_utils-5.2.0-py3-none-any.whl", hash = "sha256:773d64ec9a06b6c73f057871a5a16b048e513bdee770d0b7f5d757610e801c38", size = 81221 }, ] [[package]]