Server: CRM-Domain mit REST-API, Autorisierung und CLI

Neues crm-Paket mit Datenmodell, Geschäftslogik und REST-Endpunkten
für die Entität Kunde (/xhr/kunde/{list,get,create,update,delete}).

authorisation_required-Decorator: app-spezifische Rollenprüfung via
has_permission(appid, ...). SysAdmin systemweit, AppAdmin implizit in
seiner Anwendung, User (Mitarbeiter) als Basis-Rolle, Zusatz-Rollen
wie KundenManager additiv.

Flask CLI: init-db (Schema + Musterdaten + App-Rollen) und reset-db
(Bestätigung, behält SysAdmin).

config.ini: Neuer appid-Eintrag (KBPlusCRM), getrennt vom technischen
PROJEKTNAME. checkdb: appid aus config, Migration _db_V01_02 für
bestehende DBs.

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-03 16:12:13 +02:00
1 parent 65f81a0ee8
commit 13eaaec9c1
12 files changed
+627 -8

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
"""
Decoratoren zur Authentifizierungs- und Autorisierungsprüfung.
Der Decorator authorisation_required hebt die Rollenprüfung an die Route-Ebene,
analog zu flask_login.login_required. Authentifizierung (login_required) ist
eingebettet, sodass pro Endpunkt nur ein Decorator nötig ist.
:author: Mistral Vibe <vibe@mistral.ai>
:date: 2026-09-02
:version: 1.0
"""
from functools import wraps
from flask import current_app
from flask_login import current_user, login_required
from auth.model import APP_ADMIN
from auth.model.errors import AutorisationError
def authorisation_required(allowed_roles=[]):
"""
Decorator, der sicherstellt, dass der angemeldete Benutzer für den
Endpunkt autorisiert ist. Authentifizierung (login_required) ist eingebettet,
sodass kein zusätzlicher @login_required-Aufruf nötig ist.
Aspekte der Autorisierung (Prüfreihenfolge):
1. SysAdmin (systemweite Super-Rolle):
Der SysAdmin darf alles, unabhängig von der Anwendung. Er braucht in
keiner Application eine Rolle zugewiesen zu haben. Die Prüfung erfolgt
über User.is_SysAdmin() (app-übergreifend). Dies ist die einzige Rolle,
die applikationsübergreifend gilt — sie ist eine Systemrolle, keine
App-Rolle.
2. AppAdmin (anwendungsbezogene Verwaltungsrolle):
Der AppAdmin darf innerhalb der Anwendung alles, der er zugeordnet ist.
Er braucht die Rolle AppAdmin nur in der aktuellen Anwendung. Der
Decorator ergänzt allowed_roles intern um APP_ADMIN, sodass der AppAdmin
jeden Endpunkt ohne explizite Nennung in allowed_roles passiert. Die
Prüfung erfolgt über User.has_permission(appid, [..., APP_ADMIN]) —
app-spezifisch, nicht systemweit.
3. Benutzer mit Basis-Rolle 'User' (Mitarbeiter):
Jeder Anwender hat zwingend die Basis-Rolle 'User' (Mitarbeiter), damit
er das Login-Gate in auth/routes.checklogin passiert. Diese Rolle allein
berechtigt nur zu Lese-Endpunkten, bei denen MITARBEITER in allowed_roles
steht. Für Schreib-Endpunkte reicht 'User' nicht.
4. Benutzer mit Zusatz-Rollen (z.B. KundenManager, später ProjektManager):
Zusätzlich zur Basis-Rolle 'User' können einem Benutzer weitere Rollen
zugewiesen werden. Diese gewähren weitergehende Berechtigungen für
bestimmte Entitäten. Die Prüfung erfolgt app-spezifisch über
User.has_permission(appid, allowed_roles). Der Benutzer muss die
Zusatz-Rolle in der aktuellen Anwendung haben, nicht in einer anderen.
Die appid der aktuellen Anwendung wird aus app.config["APPID"] gelesen
(gesetzt in approot.py aus config.ini, Eintrag [APP] appid). Sie ist der
lesbare Anwendungsname (z.B. 'KBPlusCRM') und dient der Zuordnung in der
Tabelle 'applications'. Sie ist nicht identisch mit PROJEKTNAME, der nur
für Datei- und Verzeichnisnamen verwendet wird.
- allowed_roles: List[str]
- roleids (nicht ids) der Rollen, die den Endpunkt zusätzlich zu
AppAdmin/SysAdmin erlauben. AppAdmin wird intern ergänzt, muss also
nicht explizit genannt werden. Der Benutzer muss die Rolle in der
aktuellen Anwendung haben (geprüft via User.has_permission).
"""
def decorator(f):
@wraps(f)
@login_required
def wrapper(*args, **kwargs):
# appid der aktuellen Anwendung aus der Konfiguration
appid = current_app.config["APPID"]
# 1. SysAdmin darf systemweit alles (keine app-spezifische Prüfung)
if current_user.is_SysAdmin():
return f(*args, **kwargs)
# 2.-4. AppAdmin und alle anderen Rollen: app-spezifische Prüfung.
# AppAdmin wird implizit zu allowed_roles hinzugefügt, damit der
# AppAdmin in seiner Anwendung jeden Endpunkt ohne explizite
# Nennung in allowed_roles nutzen kann.
effective_roles = list(allowed_roles) + [APP_ADMIN]
if not current_user.has_permission(appid, effective_roles):
# Rollen des Users in der aktuellen Anwendung für die Fehlermeldung
user_roles = current_user.get_userapproles(appid)
raise AutorisationError(
allowed_roles=allowed_roles, user_roles=user_roles
)
return f(*args, **kwargs)
return wrapper
return decorator
+23 -2
View File
@@ -3,6 +3,7 @@ from auth import model
from sqlalchemy import text
from sqlalchemy.exc import OperationalError
from datetime import datetime
from flask import current_app
from db import dbauth
session = dbauth.session
@@ -48,8 +49,11 @@ def _db_V01_01():
application = model.Application()
application.id = 1
application.appid = "Project01"
application.name = "Project01 (Name ändern)"
# appid und name aus der Konfiguration übernehmen (config.ini [APP] appid).
# Die appid ist der lesbare Anwendungsname, der zur Autorisierung verwendet
# wird; nicht identisch mit PROJEKTNAME (technischer Bezeichner).
application.appid = current_app.config["APPID"]
application.name = current_app.config["APPID"]
session.add(application)
userapp = model.Userapp()
@@ -61,6 +65,23 @@ def _db_V01_01():
_updatesysteminfo('01.01')
session.commit()
def _db_V01_02():
"""
Migriert bestehende Application-Datensätze, deren appid noch den alten
Platzhalter 'Project01' enthält, auf die appid aus der aktuellen Konfiguration
(config.ini [APP] appid). Wird nach reset-db nicht benötigt (da _db_V01_01
direkt den korrekten Wert setzt), greift aber bei Upgrades bestehender DBs.
"""
appid = current_app.config["APPID"]
application: model.Application = model.Application.query.filter(
model.Application.appid == "Project01"
).first()
if application is not None:
application.appid = appid
application.name = appid
_updatesysteminfo('01.02')
session.commit()
def _updatesysteminfo(versionnr):
s: model.Systeminfo = model.Systeminfo.query.get(1)
s.version = versionnr