Files
Inventarsystem/Legal/MULTITENANT_PYTHON_API.md
T
Aiirondev_dev 0c27d7ac86 feat: Add Excel, PDF export, and user generation modules
- Implemented `excel_export.py` for generating library item exports in Excel format.
- Created `pdf_export.py` for generating audit reports compliant with DIN 5008 standards, including detailed event tables and signature blocks.
- Developed `generate_user.py` for interactive user creation with validation for usernames and passwords.
- Introduced `module_registry.py` for managing module states and path matching.
- Added a basic `__init__.py` in the `terminplaner` module for initialization.
2026-05-20 15:30:23 +02:00

5.1 KiB
Raw Blame History

Multi-Tenant Python Management API

This document explains how the multi-tenant architecture isolates data within Python, what the return values are, and how developers can build internal administrative scripts using native Python instead of the Docker CLI.

1. Architectural Concept

In the system, each "tenant" is essentially a dedicated MongoDB database identified by a dynamically generated string based on a subdomain or header (inventar_<tenant_id>). App containers share a connection pool using pymongo.MongoClient, and requests are routed to specific databases dynamically based on the current Flask g.tenant_context.

All MongoDB administrative tasks (creating tenants, restarting apps, fetching lists) are done via standard MongoDB Python drivers because the core multi-tenancy happens at the database level.

2. Managing Tenants via Python

If you want to perform multi-tenant administrative operations without traversing through manage-tenant.sh, you can execute native Python scripts connecting to the system's MongoClient.

Basic Connection Boilerplate

Whenever automating an administrative task in Python, you simply need to connect to MongoDB using the properties defined in settings.py.

import sys
import os

# Append Web folder so we can access configuration
sys.path.insert(0, '/app/Web')
import settings
from pymongo import MongoClient

# Establish connection pooling
client = MongoClient(settings.MONGODB_HOST, int(settings.MONGODB_PORT))

A. Adding a New Tenant (Database Initialization)

A new tenant database isnt provisioned until the first actual data insert happens. We trigger this manually by creating an admin user for them.

Operation:

def create_tenant(tenant_id, admin_password="hashed_password_here"):
    db_name = f"{settings.MONGODB_DB}_{tenant_id}"
    db = client[db_name]
    
    # MongoDB creates the DB automatically on first insert
    result = db.users.insert_one({
        'username': 'admin',
        'password': admin_password,
        'role': 'admin'
    })
    return result.inserted_id # Returns the BSON ObjectId of the new user

B. List Active Tenants

To find out how many isolated tenants have active databases, you query the raw MongoClient for all databases and search for your configured MongoDB prefix (default: inventar_).

Operation:

def list_tenants():
    prefix = f"{settings.MONGODB_DB}_"
    
    # Returns a Python list of string database names
    all_dbs = client.list_database_names() 
    
    # Filter and strip the prefix to return just the tenant_ids
    active_tenants = [d.replace(prefix, "") for d in all_dbs if d.startswith(prefix)]
    
    return active_tenants # e.g., ['schule1', 'schule2', 'test']

C. Soft-Restarting a Tenant (Invalidating Sessions)

"Restarting" a single tenant means signing out all of their users and forcing an application refresh. Because Session data is coupled to the tenant database, dropping their sessions collection achieves an instant sign-out.

Operation:

def restart_tenant(tenant_id):
    db_name = f"{settings.MONGODB_DB}_{tenant_id}"
    db = client[db_name]
    
    # Drops the collection. All active user cookies immediately become invalid.
    result = db.sessions.drop() 
    
    return result # Returns None. Raises PyMongoError if connection fails.

D. Removing a Tenant Completely (Wipe Data)

If a tenant is removed from the service or their lease expires, you can permanently obliterate their data container footprint.

Operation:

def remove_tenant(tenant_id):
    db_name = f"{settings.MONGODB_DB}_{tenant_id}"
    
    # Erases the isolated database. Can't be undone.
    client.drop_database(db_name) 
    
    return True # Returns True. Raises PyMongoError if connection fails.

3. Resolving Context Inside Flask (app.py)

If you are building custom application endpoints inside Web/app.py, you shouldn't use the direct MongoDB client manually. Instead, you rely on the built-in Flask context manager (Web/tenant.py) to give you the correct isolated scope.

The get_tenant_db() function

Every route must use get_tenant_db(client) to ensure users can only ever access their own school/domain's database.

from pymongo import MongoClient
import settings
from tenant import get_tenant_db

# Example Route
@app.route('/api/items')
def get_items():
    # 1. Establish/reuse pooling connection
    client = MongoClient(settings.MONGODB_HOST, settings.MONGODB_PORT)
    
    # 2. Get the dynamically routed DB for THIS user 
    # (Based on Nginx Subdomain or X-Tenant-Id header)
    db = get_tenant_db(client) 
    
    # 3. Runs query solely on `inventar_schule1.items`
    items = list(db.items.find())
    
    return items # List of BSON Dictionaries

What it returns internally: The get_tenant_db function queries g.tenant_context inside Flask, calculates the database name from the subdomain, and returns a live pymongo.database.Database object.

This ensures that scaling is extremely cheap on resources because 1 Application Container connects to 100 separate Tenant Databases using just 1 shared MongoClient pool.