====== 4.1 DB-Funktionen — Auth Controller ====== //Stand: 2026-03-05// Übergeordnet: [[de:int:wvdsshell:notes:01-siam:auth-database|4. Datenschicht — DB-Schemas]] Verwandt: [[de:int:wvdsshell:notes:01-siam:auth-gateway|3. Gateway.Service]] Dieses Dokument beschreibt alle gespeicherten Prozeduren, Funktionen und Views in **ENIVERSSIAM**, die ''WvdS.Data.Gateway.Controller.Auth'' auf Datenbankebene unterstützen. ===== Endpoint → DB-Zuordnung ===== ^ Endpoint ^ DB-Objekte ^ | POST ''/auth/kerberos'' | ''fn_user_lookup'', ''usp_login_success'', ''usp_device_session_create'', ''fn_user_roles'' | | POST ''/auth/external'' | ''fn_user_lookup'', ''usp_password_failure'', ''usp_mfa_session_create'' | | POST ''/auth/totp/verify'' | ''usp_mfa_session_consume'', ''usp_totp_failure'', ''usp_login_success'', ''usp_device_session_create'', ''fn_user_roles'' | | POST ''/auth/token/refresh'' | ''usp_refresh_token_rotate'' | | GET ''/auth/totp/enroll'' | ''usp_totp_enrollment_begin'' | | DELETE ''/auth/totp/enroll'' | ''usp_totp_reset'' | | GET ''/features'' | ''fn_get_features'' | | Middleware: Replay-Schutz | ''usp_nonce_check_insert'' | | Middleware: Rate-Limiting | ''usp_rate_limit_record'', ''fn_is_rate_limited'' | | Middleware: Autorisierung | ''fn_get_user_id'', ''fn_is_user_member_of'' | | Hintergrundaufgabe (periodisch) | ''usp_expired_cleanup'' | ===== Naming-Konvention ===== ^ Präfix ^ Typ ^ Beispiel ^ | ''usp_'' | Stored Procedure (mit DML) | ''auth.usp_login_success'' | | ''fn_'' | Scalar / Table-Valued Function | ''auth.fn_user_lookup'', ''auth.fn_is_rate_limited'' | | ''v_'' | View | ''auth.v_user_auth'' | > **Faustregel:** Reine Lesezugriffe mit tabellarischem Ergebnis → TVF (''fn_''), verwendbar in ''FROM''/''JOIN''/''CROSS APPLY''. > Operationen mit DML (''INSERT'', ''UPDATE'') oder ''OUTPUT''-Parametern → Stored Procedure (''usp_''). Identifier: lowercase snake_case (konsistent mit den Tabellennamen in ENIVERSSIAM). ===== Stored Procedures ===== ==== auth.usp_login_success ==== **Zweck:** Erfolgreichen Login protokollieren — ''last_login_at'' aktualisieren, Passwort-Fehlversuche zurücksetzen. CREATE OR ALTER PROCEDURE auth.usp_login_success @user_id INT, @ip_address VARCHAR(45) AS BEGIN SET NOCOUNT ON; UPDATE auth.[user] SET last_login_at = SYSUTCDATETIME(), password_failed_attempts = 0, password_locked_until = NULL, updated_at = SYSUTCDATETIME() WHERE id = @user_id; END; | Tabellen | ''auth.[user]'' | | Aufgerufen von | POST ''/auth/kerberos'', POST ''/auth/totp/verify'' | ==== auth.usp_password_failure ==== **Zweck:** Fehlgeschlagenen Passwort-Versuch registrieren. Setzt Lockout, wenn der Schwellwert erreicht ist. Gibt die verbleibenden Versuche und ggf. den Lockout-Zeitpunkt zurück. CREATE OR ALTER PROCEDURE auth.usp_password_failure @user_id INT, @max_attempts INT = 5, @lockout_minutes INT = 15, @attempts_remaining INT OUTPUT, @locked_until DATETIME2 OUTPUT AS BEGIN SET NOCOUNT ON; UPDATE auth.[user] SET password_failed_attempts = password_failed_attempts + 1, password_locked_until = CASE WHEN password_failed_attempts + 1 >= @max_attempts THEN DATEADD(MINUTE, @lockout_minutes, SYSUTCDATETIME()) ELSE NULL END, updated_at = SYSUTCDATETIME() WHERE id = @user_id; SELECT @attempts_remaining = CASE WHEN password_failed_attempts >= @max_attempts THEN 0 ELSE @max_attempts - password_failed_attempts END, @locked_until = password_locked_until FROM auth.[user] WHERE id = @user_id; END; | Tabellen | ''auth.[user]'' | | Aufgerufen von | POST ''/auth/external'' | ==== auth.usp_mfa_session_create ==== **Zweck:** TOTP-Challenge-Session anlegen. Gibt eine neue ''challenge_id'' zurück, die der Gateway im JSON-Response an den Client sendet. CREATE OR ALTER PROCEDURE auth.usp_mfa_session_create @user_id INT, @reason VARCHAR(50), @lifetime_seconds INT = 300, @challenge_id VARCHAR(64) OUTPUT, @expires_at DATETIME2 OUTPUT AS BEGIN SET NOCOUNT ON; -- challenge_id: Gateway übergibt eine bereits generierte UUID -- (NEWID() wäre akzeptabel, aber der Gateway-Layer kontrolliert die Entropie) SET @expires_at = DATEADD(SECOND, @lifetime_seconds, SYSUTCDATETIME()); INSERT INTO auth.mfa_session (mfa_session_id, user_id, reason, expires_at) VALUES (@challenge_id, @user_id, @reason, @expires_at); END; | Tabellen | ''auth.mfa_session'' | | Aufgerufen von | POST ''/auth/external'' | | Hinweis | ''@challenge_id'' wird vom Gateway generiert (kryptografisch zufällig) und als INPUT übergeben. | ==== auth.usp_mfa_session_consume ==== **Zweck:** TOTP-Challenge atomar prüfen und verbrauchen. Inkrementiert den Versuchs-Counter; sperrt die Challenge bei Überschreitung von ''@max_attempts''. Gibt Validierungsstatus und verbleibende Versuche zurück. CREATE OR ALTER PROCEDURE auth.usp_mfa_session_consume @challenge_id VARCHAR(64), @max_attempts INT = 5, @user_id INT OUTPUT, @is_valid BIT OUTPUT, @is_expired BIT OUTPUT, @attempts INT OUTPUT, @attempts_remaining INT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; SELECT @user_id = user_id, @is_expired = CASE WHEN expires_at < SYSUTCDATETIME() THEN 1 ELSE 0 END, @attempts = attempts FROM auth.mfa_session WITH (UPDLOCK, ROWLOCK) WHERE mfa_session_id = @challenge_id AND is_verified = 0; IF @user_id IS NULL BEGIN -- Challenge unbekannt SET @is_valid = 0; ROLLBACK; RETURN; END; -- Versuchs-Counter erhöhen -- NOTE: mfa_session hat keine updated_at-Spalte UPDATE auth.mfa_session SET attempts = attempts + 1 WHERE mfa_session_id = @challenge_id; SET @attempts = @attempts + 1; SET @attempts_remaining = CASE WHEN @attempts >= @max_attempts THEN 0 ELSE @max_attempts - @attempts END; -- Gültig: Kein Ablauf, kein Überschreiten der Max-Versuche SET @is_valid = CASE WHEN @is_expired = 0 AND @attempts <= @max_attempts THEN 1 ELSE 0 END; -- Bei Erfolg: Session als verifiziert markieren IF @is_valid = 1 UPDATE auth.mfa_session SET is_verified = 1, verified_at = SYSUTCDATETIME() WHERE mfa_session_id = @challenge_id; COMMIT; END; | Tabellen | ''auth.mfa_session'' | | Aufgerufen von | POST ''/auth/totp/verify'' | | Transaktional | Ja — UPDLOCK verhindert Race Conditions bei gleichzeitigen Verify-Requests | ==== auth.usp_totp_failure ==== **Zweck:** Fehlerhaften TOTP-Code registrieren. Analog zu ''usp_password_failure'', aber für das TOTP-Lockout. CREATE OR ALTER PROCEDURE auth.usp_totp_failure @user_id INT, @max_attempts INT = 5, @lockout_minutes INT = 15, @attempts_remaining INT OUTPUT, @locked_until DATETIME2 OUTPUT AS BEGIN SET NOCOUNT ON; UPDATE auth.[user] SET totp_failed_attempts = totp_failed_attempts + 1, totp_locked_until = CASE WHEN totp_failed_attempts + 1 >= @max_attempts THEN DATEADD(MINUTE, @lockout_minutes, SYSUTCDATETIME()) ELSE NULL END, updated_at = SYSUTCDATETIME() WHERE id = @user_id; SELECT @attempts_remaining = CASE WHEN totp_failed_attempts >= @max_attempts THEN 0 ELSE @max_attempts - totp_failed_attempts END, @locked_until = totp_locked_until FROM auth.[user] WHERE id = @user_id; END; | Tabellen | ''auth.[user]'' | | Aufgerufen von | POST ''/auth/totp/verify'' | ==== auth.usp_device_session_create ==== **Zweck:** Neues Gerät registrieren und initialen Refresh-Token-Hash speichern. Der eigentliche Refresh-Token-Wert verlässt den Gateway nie in Klartext — nur der SHA-256-Hash landet in der DB. CREATE OR ALTER PROCEDURE auth.usp_device_session_create @user_id INT, @device_id VARCHAR(64), @name NVARCHAR(100), @refresh_token_hash VARBINARY(64), @family_id VARCHAR(64), @ip_address VARCHAR(45), @user_agent NVARCHAR(512), @device_pk INT OUTPUT AS BEGIN SET NOCOUNT ON; INSERT INTO auth.device (device_id, user_id, name, refresh_token_hash, refresh_family_id, refresh_generation, last_ip, last_user_agent, session_state, is_active, created_at) VALUES (@device_id, @user_id, @name, @refresh_token_hash, @family_id, 1, @ip_address, @user_agent, 'ACTIVE', 1, SYSUTCDATETIME()); SET @device_pk = SCOPE_IDENTITY(); -- Refresh-Token-Family initialisieren INSERT INTO auth.refresh_token_family (family_id, user_id, device_id, current_generation, is_revoked, created_at) VALUES (@family_id, @user_id, @device_pk, 1, 0, SYSUTCDATETIME()); END; | Tabellen | ''auth.device'', ''auth.refresh_token_family'' | | Aufgerufen von | POST ''/auth/kerberos'', POST ''/auth/totp/verify'' | ==== auth.usp_refresh_token_rotate ==== **Zweck:** Refresh-Token rotieren — Generation prüfen, neuen Hash speichern, Generation inkrementieren. Bei Generation-Mismatch: gesamte Token-Familie widerrufen (gestohlenes Token erkannt, CWE-613). CREATE OR ALTER PROCEDURE auth.usp_refresh_token_rotate @family_id VARCHAR(64), @expected_generation INT, @new_token_hash VARBINARY(64), @new_expires_at DATETIME2, @success BIT OUTPUT, @user_id INT OUTPUT, @device_pk INT OUTPUT, @revoke_reason VARCHAR(50) OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; SELECT @user_id = user_id, @device_pk = device_id FROM auth.refresh_token_family WITH (UPDLOCK, ROWLOCK) WHERE family_id = @family_id AND current_generation = @expected_generation AND is_revoked = 0; IF @user_id IS NULL BEGIN -- Generation-Mismatch oder bereits widerrufen → -- Gesamte Familie widerrufen (Replay / Diebstahl) SET @success = 0; SET @revoke_reason = 'generation_mismatch'; UPDATE auth.refresh_token_family SET is_revoked = 1, revoked_reason = 'generation_mismatch', revoked_at = SYSUTCDATETIME() WHERE family_id = @family_id; COMMIT; RETURN; END; -- Generation inkrementieren, neuen Hash speichern UPDATE auth.refresh_token_family SET current_generation = current_generation + 1, last_used_at = SYSUTCDATETIME() WHERE family_id = @family_id; UPDATE auth.device SET refresh_token_hash = @new_token_hash, refresh_token_expires_at = @new_expires_at, refresh_generation = refresh_generation + 1, last_used_at = SYSUTCDATETIME() WHERE id = @device_pk; SET @success = 1; SET @revoke_reason = NULL; COMMIT; END; | Tabellen | ''auth.refresh_token_family'', ''auth.device'' | | Aufgerufen von | POST ''/auth/token/refresh'' | | Transaktional | Ja — UPDLOCK verhindert parallele Rotation derselben Family | ==== auth.usp_session_revoke ==== **Zweck:** Geräte-Session widerrufen (Logout, Admin-Eingriff). CREATE OR ALTER PROCEDURE auth.usp_session_revoke @device_id VARCHAR(64), @reason VARCHAR(50) AS BEGIN SET NOCOUNT ON; -- NOTE: auth.device hat keine updated_at-Spalte UPDATE auth.device SET session_state = 'REVOKED', session_revoked_at = SYSUTCDATETIME(), session_revoked_reason = @reason, is_active = 0 WHERE device_id = @device_id; -- Zugehörige Refresh-Token-Familie ebenfalls widerrufen UPDATE rtf SET rtf.is_revoked = 1, rtf.revoked_reason = @reason, rtf.revoked_at = SYSUTCDATETIME() FROM auth.refresh_token_family rtf JOIN auth.device d ON d.id = rtf.device_id WHERE d.device_id = @device_id; END; | Tabellen | ''auth.device'', ''auth.refresh_token_family'' | | Aufgerufen von | Logout-Endpoint (zukünftig), Admin-Revoke | ==== auth.usp_totp_enrollment_begin ==== **Zweck:** TOTP-Enrollment starten — ''totp_pending_secret'' setzen. Das Geheimnis verbleibt bis zur Bestätigung im Pending-Zustand (CWE-256: verschlüsselt übergeben). CREATE OR ALTER PROCEDURE auth.usp_totp_enrollment_begin @user_id INT, @pending_secret VARBINARY(256), -- AES-256-GCM-verschlüsselt vom Gateway @expires_minutes INT = 10 AS BEGIN SET NOCOUNT ON; UPDATE auth.[user] SET totp_pending_secret = @pending_secret, totp_pending_expires_at = DATEADD(MINUTE, @expires_minutes, SYSUTCDATETIME()), updated_at = SYSUTCDATETIME() WHERE id = @user_id; END; | Tabellen | ''auth.[user]'' | | Aufgerufen von | GET ''/auth/totp/enroll'' | ==== auth.usp_totp_enrollment_confirm ==== **Zweck:** TOTP-Enrollment abschließen — ''totp_pending_secret'' → ''totp_secret'' übernehmen, ''totp_enabled = 1'' setzen. Schlägt fehl (''@success = 0'') wenn kein gültiges Pending vorhanden oder abgelaufen. CREATE OR ALTER PROCEDURE auth.usp_totp_enrollment_confirm @user_id INT, @success BIT OUTPUT AS BEGIN SET NOCOUNT ON; UPDATE auth.[user] SET totp_secret = totp_pending_secret, totp_enabled = 1, totp_verified_at = SYSUTCDATETIME(), totp_failed_attempts = 0, totp_locked_until = NULL, totp_pending_secret = NULL, totp_pending_expires_at = NULL, updated_at = SYSUTCDATETIME() WHERE id = @user_id AND totp_pending_secret IS NOT NULL AND totp_pending_expires_at > SYSUTCDATETIME(); SET @success = CASE WHEN @@ROWCOUNT > 0 THEN 1 ELSE 0 END; END; | Tabellen | ''auth.[user]'' | | Aufgerufen von | GET ''/auth/totp/enroll'' (nach Bestätigungs-Code vom User) | ==== auth.usp_totp_reset ==== **Zweck:** TOTP-Enrollment vollständig zurücksetzen — ''totp_secret'', ''totp_enabled'', Pending-Felder löschen. CREATE OR ALTER PROCEDURE auth.usp_totp_reset @user_id INT AS BEGIN SET NOCOUNT ON; UPDATE auth.[user] SET totp_secret = NULL, totp_enabled = 0, totp_verified_at = NULL, totp_failed_attempts = 0, totp_locked_until = NULL, totp_pending_secret = NULL, totp_pending_expires_at = NULL, updated_at = SYSUTCDATETIME() WHERE id = @user_id; END; | Tabellen | ''auth.[user]'' | | Aufgerufen von | DELETE ''/auth/totp/enroll'' | ==== auth.usp_nonce_check_insert ==== **Zweck:** Replay-Schutz — Nonce atomar prüfen und einfügen. Ein PK-Konflikt bedeutet Replay (die Nonce wurde bereits verarbeitet). CREATE OR ALTER PROCEDURE auth.usp_nonce_check_insert @nonce VARCHAR(64), @device_id INT, @expires_at DATETIME2, @is_replay BIT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY INSERT INTO auth.nonce_cache (nonce, device_id, received_at, expires_at) VALUES (@nonce, @device_id, SYSUTCDATETIME(), @expires_at); SET @is_replay = 0; END TRY BEGIN CATCH -- PK-Violation = Nonce wurde bereits gesehen → Replay IF ERROR_NUMBER() = 2627 SET @is_replay = 1; ELSE THROW; END CATCH; END; | Tabellen | ''auth.nonce_cache'' | | Aufgerufen von | Replay-Schutz-Middleware (vor jeder authentifizierten Anfrage) | ==== auth.usp_rate_limit_record ==== **Zweck:** Rate-Limit-Ereignis protokollieren. Der Gateway ruft diese SP nach jedem relevanten Auth-Ereignis auf (Fehlversuch, unbekannter User, Brute-Force-Indikator). CREATE OR ALTER PROCEDURE auth.usp_rate_limit_record @user_id INT = NULL, @device_id INT = NULL, @ip_address VARCHAR(45), @event_type VARCHAR(30) AS BEGIN SET NOCOUNT ON; INSERT INTO auth.rate_limit_event (user_id, device_id, ip_address, event_type, created_at) VALUES (@user_id, @device_id, @ip_address, @event_type, SYSUTCDATETIME()); END; | Tabellen | ''auth.rate_limit_event'' | | Aufgerufen von | Rate-Limit-Middleware, Auth-Controller bei Fehlversuchen | ==== auth.usp_expired_cleanup ==== **Zweck:** Wartungsaufgabe — abgelaufene Nonces und MFA-Sessions bereinigen. Wird vom Gateway periodisch aufgerufen (z. B. alle 5 Minuten via Timer-Thread). CREATE OR ALTER PROCEDURE auth.usp_expired_cleanup AS BEGIN SET NOCOUNT ON; DECLARE @cutoff DATETIME2 = SYSUTCDATETIME(); DELETE FROM auth.nonce_cache WHERE expires_at < @cutoff; DELETE FROM auth.mfa_session WHERE expires_at < @cutoff AND is_verified = 0; DELETE FROM auth.rate_limit_event WHERE created_at < DATEADD(DAY, -7, @cutoff); END; | Tabellen | ''auth.nonce_cache'', ''auth.mfa_session'', ''auth.rate_limit_event'' | | Aufgerufen von | Gateway-interner Maintenance-Timer | ===== Scalar Functions ===== ==== auth.fn_is_user_locked ==== **Zweck:** Prüft ob der Benutzer wegen Passwort-Fehlversuchen gesperrt ist. CREATE OR ALTER FUNCTION auth.fn_is_user_locked (@user_id INT) RETURNS BIT AS BEGIN RETURN ( SELECT CASE WHEN password_locked_until > SYSUTCDATETIME() THEN 1 ELSE 0 END FROM auth.[user] WHERE id = @user_id ); END; ==== auth.fn_is_totp_locked ==== **Zweck:** Prüft ob der TOTP-Kanal des Benutzers gesperrt ist. CREATE OR ALTER FUNCTION auth.fn_is_totp_locked (@user_id INT) RETURNS BIT AS BEGIN RETURN ( SELECT CASE WHEN totp_locked_until > SYSUTCDATETIME() THEN 1 ELSE 0 END FROM auth.[user] WHERE id = @user_id ); END; ==== auth.fn_is_rate_limited ==== **Zweck:** Zählt Rate-Limit-Ereignisse in einem Zeitfenster und gibt 1 zurück wenn das Limit überschritten ist. CREATE OR ALTER FUNCTION auth.fn_is_rate_limited ( @user_id INT, @ip_address VARCHAR(45), @event_type VARCHAR(30), @window_seconds INT, @max_count INT ) RETURNS BIT AS BEGIN DECLARE @count INT; DECLARE @window_start DATETIME2 = DATEADD(SECOND, -@window_seconds, SYSUTCDATETIME()); SELECT @count = COUNT(*) FROM auth.rate_limit_event WHERE event_type = @event_type AND created_at >= @window_start AND ( (@user_id IS NOT NULL AND user_id = @user_id) OR (@ip_address IS NOT NULL AND ip_address = @ip_address) ); RETURN CASE WHEN @count >= @max_count THEN 1 ELSE 0 END; END; ==== auth.fn_get_user_id ==== **Zweck:** Komfort-Wrapper um ''fn_user_lookup'' — gibt direkt die ''user_id'' zurück. Vermeidet CROSS APPLY, wenn nur die ID benötigt wird (z. B. in Middleware-Checks vor dem eigentlichen Auth-Flow). Gibt ''NULL'' zurück, wenn der Username nicht existiert. CREATE OR ALTER FUNCTION auth.fn_get_user_id (@username VARCHAR(50)) RETURNS INT AS BEGIN RETURN ( SELECT user_id FROM auth.fn_user_lookup(@username) ); END; | Aufgerufen von | Middleware: Autorisierung, überall wo ''user_id'' aus ''username'' benötigt wird | ==== auth.fn_is_user_member_of ==== **Zweck:** Prüft ob ein Benutzer mindestens eine Rolle aus einer kommaseparierten Liste besitzt. Wraps ''fn_user_roles'' und vergleicht ''role_code'' gegen alle Einträge in ''@csv''. Gibt 1 zurück sobald ein Treffer gefunden wird, sonst 0. Typische Verwendung in Middleware-Autorisierung: -- Zugriff erlauben wenn der User 'admin' oder 'supervisor' ist IF auth.fn_is_user_member_of(@user_id, 'admin,supervisor') = 1 ... CREATE OR ALTER FUNCTION auth.fn_is_user_member_of ( @user_id INT, @csv VARCHAR(250) -- kommaseparierte role_codes, z. B. 'admin,supervisor' ) RETURNS BIT AS BEGIN -- CHARINDEX mit umgebenden Kommas verhindert Teilstring-Treffer -- (z. B. 'admin' matcht nicht 'administrator' in 'administrator,viewer') RETURN ( SELECT CASE WHEN EXISTS ( SELECT 1 FROM auth.fn_user_roles(@user_id, NULL) r WHERE CHARINDEX( ',' + LTRIM(RTRIM(r.role_code)) + ',', ',' + @csv + ',' ) > 0 ) THEN 1 ELSE 0 END ); END; | Aufgerufen von | Middleware: Autorisierung | | Kombinierbar mit | ''auth.fn_get_user_id'' für den typischen Pattern: ''fn_is_user_member_of(fn_get_user_id(@username), @csv)'' | ===== Table-Valued Functions ===== ==== auth.fn_user_lookup ==== **Zweck:** User-Datensatz für Authentifizierung laden — reine Leseoperation, daher TVF. Passwort-Hash und TOTP-Status werden zurückgegeben; der Gateway-Layer vergleicht den Hash selbst (CWE-256 — kein Krypto-Code in der DB). Gibt 0 Zeilen zurück wenn der Username nicht existiert. Verwendbar in ''FROM'', ''JOIN'' und ''CROSS APPLY'' — kombinierbar mit ''fn_user_roles'': -- Beispiel: User-Daten + Rollen in einem Query SELECT u.*, r.role_code FROM auth.fn_user_lookup('max.muster') u CROSS APPLY auth.fn_user_roles(u.user_id, NULL) r; CREATE OR ALTER FUNCTION auth.fn_user_lookup (@username VARCHAR(50)) RETURNS TABLE AS RETURN ( -- Passwort-Hash + Salt werden im Gateway per PBKDF2 verglichen (CWE-256). -- Niemals Klartext-Passwort zurückgeben oder in der DB vergleichen. SELECT u.id AS user_id, u.person_id, p.first_name, p.last_name, u.is_active, u.password_hash, u.password_salt, u.password_changed_at, u.password_failed_attempts, u.password_locked_until, u.totp_enabled, u.totp_failed_attempts, u.totp_locked_until FROM auth.[user] u JOIN core.person p ON p.id = u.person_id WHERE u.username = @username COLLATE Latin1_General_CI_AS ); | Tabellen | ''auth.[user]'', ''core.person'' | | Aufgerufen von | POST ''/auth/kerberos'', POST ''/auth/external'' | ==== auth.fn_user_roles ==== **Zweck:** Gibt alle aktiven Rollen eines Benutzers zurück — optional gefiltert nach Site-Scope. Wird im Gateway verwendet, um JWT-Role-Claims zu befüllen. CREATE OR ALTER FUNCTION auth.fn_user_roles ( @user_id INT, @site_id INT = NULL -- NULL = globale + site-spezifische Rollen zurückgeben ) RETURNS TABLE AS RETURN ( SELECT r.id AS role_id, r.code AS role_code, r.name AS role_name, ur.site_id, ur.granted_at FROM auth.user_role ur JOIN auth.role r ON r.id = ur.role_id WHERE ur.user_id = @user_id AND r.is_active = 1 AND ( @site_id IS NULL -- alle Rollen OR ur.site_id IS NULL -- globale Rollen immer einschließen OR ur.site_id = @site_id -- site-spezifische Rollen ) ); | Aufgerufen von | POST ''/auth/kerberos'', POST ''/auth/totp/verify'' (JWT-Claims befüllen) | ==== auth.fn_get_features ==== **Zweck:** Gibt die Feature-Keys zurück, die für eine gegebene ''install_id'' aktiv sind. Liest aktuell aus ENIVERSCAFM (''auth.*''-Namespace), später aus ENIVERSASYS. -- Platzhalter-Implementierung: aktuell Feature-Flags aus einer -- einfachen Konfigurations-Tabelle. Schema wird in Phase 5 erweitert. CREATE OR ALTER FUNCTION auth.fn_get_features (@install_id VARCHAR(64)) RETURNS TABLE AS RETURN ( SELECT feature_key FROM auth.feature_flag WHERE is_active = 1 AND (install_id = @install_id OR install_id IS NULL) ); > **Hinweis:** ''auth.feature_flag'' ist eine zukünftige Tabelle — wird in Phase 5 > (ENIVERSASYS) definiert. Bis dahin kann ''fn_get_features'' eine statische Liste zurückgeben. | Aufgerufen von | GET ''/features'' | ===== Views ===== ==== auth.v_user_auth ==== **Zweck:** Kombinierte Sicht auf User + Person + berechnete Lockout-Flags. Vereinfacht den häufigsten Auth-Lesezugriff im Gateway. CREATE OR ALTER VIEW auth.v_user_auth AS SELECT u.id AS user_id, u.person_id, u.username, p.first_name, p.last_name, u.is_active, u.totp_enabled, u.totp_failed_attempts, -- Berechnete Lockout-Flags (live, kein Cache-Risiko) CASE WHEN u.password_locked_until > SYSUTCDATETIME() THEN 1 ELSE 0 END AS is_password_locked, u.password_locked_until, u.password_failed_attempts, CASE WHEN u.totp_locked_until > SYSUTCDATETIME() THEN 1 ELSE 0 END AS is_totp_locked, u.totp_locked_until, u.last_login_at, u.created_at FROM auth.[user] u JOIN core.person p ON p.id = u.person_id; | Tabellen | ''auth.[user]'', ''core.person'' | | Aufgerufen von | Direkter SELECT statt ''usp_user_lookup'' für einfache Lesefälle |