diff --git a/application/single_app/app.py b/application/single_app/app.py index 2354b1b5..d58eeed5 100644 --- a/application/single_app/app.py +++ b/application/single_app/app.py @@ -487,6 +487,16 @@ def markdown_filter(text): # Add the filter to the Jinja environment app.jinja_env.filters['markdown'] = markdown_filter +# Register a custom Jinja filter for nl2br (newline to
) +def nl2br_filter(value): + """Escape HTML then convert newline characters to
tags.""" + from markupsafe import escape, Markup + if not value: + return Markup('') + return Markup(str(escape(value)).replace('\n', '
\n')) + +app.jinja_env.filters['nl2br'] = nl2br_filter + # =================== Default Routes ===================== @app.route('/') @swagger_route(security=get_auth_security()) diff --git a/application/single_app/functions_settings.py b/application/single_app/functions_settings.py index 8176939d..89367065 100644 --- a/application/single_app/functions_settings.py +++ b/application/single_app/functions_settings.py @@ -260,6 +260,9 @@ def get_settings(use_cosmos=False): 'max_file_size_mb': 150, 'conversation_history_limit': 10, 'default_system_prompt': '', + # Access denied message shown on the home page for signed-in users who lack required roles. + # Default is hard-coded; admins can override via Admin Settings (persisted in Cosmos DB). + 'access_denied_message': 'You are logged in but do not have the required permissions to access this application.\nPlease contact an administrator for access.', 'enable_file_processing_logs': True, 'file_processing_logs_timer_enabled': False, 'file_timer_value': 1, diff --git a/application/single_app/route_frontend_admin_settings.py b/application/single_app/route_frontend_admin_settings.py index 578e1545..2fc5abc8 100644 --- a/application/single_app/route_frontend_admin_settings.py +++ b/application/single_app/route_frontend_admin_settings.py @@ -869,6 +869,7 @@ def is_valid_url(url): 'max_file_size_mb': max_file_size_mb, 'conversation_history_limit': conversation_history_limit, 'default_system_prompt': form_data.get('default_system_prompt', '').strip(), + 'access_denied_message': form_data.get('access_denied_message', settings.get('access_denied_message', '')).strip(), # Video file settings with Azure Video Indexer Settings 'video_indexer_endpoint': form_data.get('video_indexer_endpoint', video_indexer_endpoint).strip(), diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 7d01f7da..f8c8b623 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -1428,6 +1428,12 @@
+
+ + Shown to signed-in users who lack the required roles. Use Enter for line breaks. + +
diff --git a/application/single_app/templates/index.html b/application/single_app/templates/index.html index 7a146e0d..c3c2abc6 100644 --- a/application/single_app/templates/index.html +++ b/application/single_app/templates/index.html @@ -62,8 +62,7 @@ {% else %} {% if session.get('user') %}

- You are logged in but do not have the required permissions to access this application. - Please submit a ticket to request access. + {{ app_settings.access_denied_message | nl2br }}

{% else %}
diff --git a/functional_tests/test_access_denied_message_feature.py b/functional_tests/test_access_denied_message_feature.py new file mode 100644 index 00000000..4a0a2194 --- /dev/null +++ b/functional_tests/test_access_denied_message_feature.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# test_access_denied_message_feature.py +""" +Functional regression test for admin-configurable access denied message. + +Version: 0.239.002 +Implemented in: 0.239.002 + +This test ensures that: +1. The Admin Settings template exposes a textarea with name="access_denied_message". +2. route_frontend_admin_settings.py reads the field from form_data and falls back + to the existing stored value (not '') when the field is absent -- preventing + silent data loss from cached/older form submissions. +3. index.html renders app_settings.access_denied_message through the nl2br filter + without a redundant hardcoded fallback string. +4. functions_settings.py defines a non-empty default for access_denied_message so + the field is always present after get_settings() deep-merges defaults. +""" + +import sys +import os +import re + +# Resolve paths relative to repo root +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +ADMIN_TEMPLATE = os.path.join(REPO_ROOT, "application", "single_app", "templates", "admin_settings.html") +INDEX_TEMPLATE = os.path.join(REPO_ROOT, "application", "single_app", "templates", "index.html") +ROUTE_FILE = os.path.join(REPO_ROOT, "application", "single_app", "route_frontend_admin_settings.py") +SETTINGS_FILE = os.path.join(REPO_ROOT, "application", "single_app", "functions_settings.py") + + +# --------------------------------------------------------------------------- +# Test 1 – Admin Settings template has the access_denied_message field +# --------------------------------------------------------------------------- + +def test_admin_template_has_field(): + """Admin Settings template must expose a textarea named access_denied_message.""" + print("Testing admin_settings.html contains access_denied_message field...") + errors = [] + + with open(ADMIN_TEMPLATE, encoding="utf-8") as f: + content = f.read() + + # textarea with correct name attribute + if 'name="access_denied_message"' not in content: + errors.append("No