From eba29fa62d5b32778002f0566a78fb256d7a4fd6 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 | 3 +- uv.lock | 12 +++----- 6 files changed, 76 insertions(+), 9 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 5ec12f94..1da2ab45 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 @@ -66,11 +70,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) @@ -133,6 +152,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": @@ -161,6 +181,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 @@ -184,6 +205,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 @@ -199,6 +221,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 @@ -210,6 +233,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 @@ -275,6 +299,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 @@ -532,6 +557,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 @@ -553,6 +579,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 @@ -568,6 +595,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' @@ -596,6 +624,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' @@ -631,6 +660,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..048c6185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "flask-talisman==1.1.0", "flask-wtf==1.2.1", "flask==3.0.3", - "funding-service-design-utils==5.1.5", + # TODO: Change back to funding-service-design-utils==5.1.5 before merging to main + "funding-service-design-utils @ git+https://github.com/communitiesuk/funding-service-design-utils.git@FS-4805_FAB-authentication", "govuk-frontend-jinja==3.4.0", "jsmin==3.0.1", "jsonschema==4.23.0", diff --git a/uv.lock b/uv.lock index 6f648964..d9e7b3bf 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", git = "https://github.com/communitiesuk/funding-service-design-utils.git?rev=FS-4805_FAB-authentication" }, { name = "govuk-frontend-jinja", specifier = "==3.4.0" }, { name = "jsmin", specifier = "==3.0.1" }, { name = "jsonschema", specifier = "==4.23.0" }, @@ -650,8 +650,8 @@ dev = [ [[package]] name = "funding-service-design-utils" -version = "5.1.5" -source = { registry = "https://pypi.org/simple" } +version = "5.1.8" +source = { git = "https://github.com/communitiesuk/funding-service-design-utils.git?rev=FS-4805_FAB-authentication#627cf2dac5281c8dd485912dbec0f28f33578b35" } dependencies = [ { name = "beautifulsoup4" }, { name = "boto3" }, @@ -671,10 +671,6 @@ 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 } -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 }, -] [[package]] name = "govuk-frontend-jinja"