feat: Implement tenant-aware configuration system

- Added tenant verification checklist in TENANT_VERIFICATION.md for system validation.
- Created tenant_config.py to manage tenant-specific configurations with global defaults and per-tenant overrides.
- Introduced tenant_guards.py for route protection based on module availability.
- Developed tenant_resolver.py for identifying active tenants from requests using subdomains and headers.
- Added tenant_templates.py for Jinja2 template helpers to check module visibility.
- Created error_403.html for custom 403 error handling.
- Defined tenants.json for tenant configurations and module management.
- Implemented unit tests in test_tenant_system.py to ensure functionality of tenant system components.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-24 09:27:46 +02:00
parent 7e93e0a6a9
commit 257cafcb21
16 changed files with 4425 additions and 1 deletions
+46 -1
View File
@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session, send_file, after_this_request
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, get_flashed_messages, session, send_file, after_this_request, g
import os
import json
import atexit
@@ -22,6 +22,12 @@ from pymongo.errors import PyMongoError
from bson.objectid import ObjectId
import user as user_store
# Tenant-aware configuration imports
import tenant_resolver
import tenant_config
import tenant_guards
import tenant_templates
app = Flask(__name__)
app.secret_key = "ASDfhbsdfseiufhgildsrfrjg874368546987s6e8468f4!?FAUS/&s"
app.config["SESSION_COOKIE_HTTPONLY"] = True
@@ -30,6 +36,45 @@ app.config["SESSION_COOKIE_SECURE"] = os.environ.get("SESSION_COOKIE_SECURE", "0
app.config["PREFERRED_URL_SCHEME"] = "https" if os.environ.get("SESSION_COOKIE_SECURE") == "1" else "http"
# ============================================================================
# TENANT-AWARE CONFIGURATION SETUP
# ============================================================================
# Register Jinja2 context processor for tenant-aware template helpers
app.context_processor(tenant_templates.inject_tenant_context)
@app.before_request
def resolve_tenant_context():
"""
Resolve the active tenant for the current request and store in g.
This runs before every request, making the tenant available to all handlers.
"""
parent_domain = os.environ.get("INSTANCE_PARENT_DOMAIN", "meine-domain")
tenant_id = tenant_resolver.resolve_tenant(parent_domain)
g.tenant_id = tenant_id
@app.errorhandler(403)
def forbidden_error(error):
"""
Custom error handler for 403 Forbidden responses.
Provides tenant-aware error messages for disabled modules.
"""
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
return jsonify({
'error': 'Access denied',
'message': 'This resource is not available for your organization.'
}), 403
return render_template('error_403.html',
tenant_id=g.get('tenant_id', 'default')), 403
# ============================================================================
@app.after_request
def set_security_headers(response):
response.headers["X-Content-Type-Options"] = "nosniff"