====== 5. Auth-Migration — Legacy-Systeme ====== //Stand: 2026-03-05// Übergeordnet: [[de:int:wvdsshell:notes:01-siam:start|Auth-Architektur — Gesamtübersicht]] Verwandt: [[de:int:wvdsshell:notes:01-siam:auth-database|4. Datenschicht — DB-Schemas]] ===== Ausgangssituation ===== Vor der Migration existierten Benutzer und Rollen in zwei vollständig getrennten Systemen. ENIVERSCAFM verwaltete Auth-Daten in eigenen Tabellen (''auth.*''), die aus der historischen ENIVERS-Entwicklung stammten. Das LD-System führte parallel dazu ''tblPerson'' und ''tblRolle'' — eine eigene Benutzer- und Rollenverwaltung für die Elektrikerseite, mit eigenem Passwort-Mechanismus und eigener Admin-Oberfläche. Ein Benutzer, der in beiden Systemen aktiv war, hatte in der Praxis zwei Identitäten: eine für AMED-Funktionen, eine für LD-Funktionen. Änderungen an Rollen oder Passwörtern mussten doppelt gepflegt werden. Es gab keine gemeinsame Audit-Spur — ob ein Benutzer in System A oder System B tätig war, ließ sich nicht zusammenhängend nachvollziehen. Die Migration ist deshalb kein rein technischer Vorgang, sondern die Umsetzung einer konzeptuellen Entscheidung: Es gibt eine Benutzeridentität, nicht zwei. Phasen 1 und 2 haben diesen Zustand hergestellt — 558 Benutzer aus zwei Quellsystemen, konsolidiert in ENIVERSSIAM, ohne Anpassungen am bestehenden Anwendungscode, da ENIVERSCAFM die Tabellen weiterhin über Synonyme erreicht. Ab Phase 3 folgt die inhaltliche Migration: Assets, Prüfprozesse, Legacy-Rohrdaten. ===== Migrations-Übersicht ===== ^ Phase ^ Inhalt ^ Status ^ Ergebnis ^ | 1 | ''auth.*'' + ''core.person'': ENIVERSCAFM → ENIVERSSIAM | ✓ Abgeschlossen | 15 Permissions, 26 Rollen, 241 User, 9.445 Rollenzuordnungen | | 2 | LD-System: ENIVERS ''tblPerson/tblRolle'' → ENIVERSSIAM | ✓ Abgeschlossen | 317 Personen, 314 User, 38 Rollen, 9.476 Rollenzuordnungen | | 3 | AMED Bestandsdaten (Branches, Devices, Facilities) → ENIVERSCAFM | Ausstehend | — | | 4 | ENIVERSPIMS aufbauen; WIS/PROOF/TecDB migrieren | Ausstehend | — | | 5 | ENIVERSASYS aufbauen; Feature-Flags aus ENIVERSCAFM auslagern | Ausstehend | — | ===== Phase 1 — ENIVERSCAFM auth.* → ENIVERSSIAM ===== ==== Ausgangslage ==== Die Auth-Tabellen (''auth.*'') und die Personen-Tabelle (''hr.person'') lagen ursprünglich in **ENIVERSCAFM**. Ziel war die Konsolidierung: Auth-Daten gehören in die dedizierte Identity-Datenbank **ENIVERSSIAM**. ENIVERSCAFM erhält danach Synonyme — bestehender Code funktioniert transparent weiter ohne Änderungen. ==== Mapping: auth.* (13 Tabellen + core.person) ==== ^ ENIVERSCAFM (Quelle) ^ ENIVERSSIAM (Ziel) ^ Verfahren ^ | ''auth.permission'' | ''auth.permission'' | Daten kopiert, Tabelle dropped, Synonym erstellt | | ''auth.role'' | ''auth.role'' | Daten kopiert, Tabelle dropped, Synonym erstellt | | ''auth.[user]'' | ''auth.[user]'' | Daten kopiert, Tabelle dropped, Synonym erstellt | | ''auth.user_role'' | ''auth.user_role'' | Daten kopiert, Tabelle dropped, Synonym erstellt | | ''auth.role_permission'' | ''auth.role_permission'' | Daten kopiert, Tabelle dropped, Synonym erstellt | | ''auth.device'' | ''auth.device'' | Synonym (Tabelle war leer) | | ''auth.backup_code'' | ''auth.backup_code'' | Synonym (Tabelle war leer) | | ''auth.known_location'' | ''auth.known_location'' | Synonym (Tabelle war leer) | | ''auth.mfa_session'' | ''auth.mfa_session'' | Synonym (Tabelle war leer) | | ''auth.nonce_cache'' | ''auth.nonce_cache'' | Synonym (Tabelle war leer) | | ''auth.rate_limit_event'' | ''auth.rate_limit_event'' | Synonym (Tabelle war leer) | | ''auth.refresh_token_family'' | ''auth.refresh_token_family'' | Synonym (Tabelle war leer) | | ''auth.server_key'' | ''auth.server_key'' | Synonym (Tabelle war leer) | | ''hr.person'' | ''core.person'' | Daten kopiert, Tabelle dropped, Synonym erstellt | > **Hinweis:** Cross-DB Foreign Keys können in SQL Server nicht erzwungen werden. > Referenzielle Integrität zwischen ENIVERSCAFM und ENIVERSSIAM wird app-seitig > (Gateway.Service) sichergestellt. ==== Skripte Phase 1 ==== **Ausführungsreihenfolge:** - ''siam_create_tables.sql'' — ENIVERSSIAM: Schema ''auth'' + 13 Tabellen + ''core'' Schema anlegen - ''siam_copy_data.sql'' — Daten von ENIVERSCAFM.auth.* nach ENIVERSSIAM.auth.* kopieren - ''siam_person.sql'' — ''core.person'' anlegen, Daten kopieren, FK wiederherstellen, Synonym in ENIVERSCAFM - ''siam_drop_and_synonyms.sql'' — ENIVERSCAFM: auth-Tabellen droppen, 13 Synonyme erstellen === siam_create_tables.sql — ENIVERSSIAM Schema + Tabellen === /* ================================================================ siam_create_tables.sql Ziel: ENIVERSSIAM Zweck: Schema 'auth' anlegen, alle 13 Tabellen erstellen Hinweis: Idempotent — IF NOT EXISTS-Guards für alle Objekte ================================================================ */ USE ENIVERSSIAM; GO -- Schema 'auth' anlegen (falls nicht vorhanden) IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'auth') EXEC('CREATE SCHEMA auth'); PRINT 'Schema auth: OK'; GO -- ------------------------------------------------------------------ -- 1. auth.permission — Berechtigungs-Objekte (Resource / Action / Scope) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'permission' ) BEGIN CREATE TABLE auth.permission ( id INT NOT NULL IDENTITY(1,1), resource VARCHAR(50) NOT NULL, action VARCHAR(20) NOT NULL, scope VARCHAR(20) NOT NULL CONSTRAINT df_permission_scope DEFAULT ('all'), description NVARCHAR(256) NULL, CONSTRAINT pk_permission PRIMARY KEY (id), CONSTRAINT uq_permission UNIQUE (resource, action, scope) ); PRINT 'auth.permission: erstellt'; END GO -- ------------------------------------------------------------------ -- 2. auth.role — Rollen-Definitionen -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'role' ) BEGIN CREATE TABLE auth.role ( id INT NOT NULL IDENTITY(1,1), code VARCHAR(50) NOT NULL, name NVARCHAR(100) NOT NULL, description NVARCHAR(MAX) NULL, is_system_role BIT NOT NULL CONSTRAINT df_role_system DEFAULT (0), is_active BIT NOT NULL CONSTRAINT df_role_active DEFAULT (1), created_at DATETIME2 NOT NULL CONSTRAINT df_role_created DEFAULT (SYSUTCDATETIME()), updated_at DATETIME2 NOT NULL CONSTRAINT df_role_updated DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_role PRIMARY KEY (id), CONSTRAINT uq_role_code UNIQUE (code) ); PRINT 'auth.role: erstellt'; END GO -- ------------------------------------------------------------------ -- 3. auth.[user] — Anmeldedaten, TOTP, Passwort-Hash -- Hinweis: person_id referenziert core.person (Intra-DB-FK, -- wird nach Daten-Migration in siam_person.sql gesetzt) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'user' ) BEGIN CREATE TABLE auth.[user] ( id INT NOT NULL IDENTITY(1,1), person_id INT NOT NULL, username VARCHAR(50) NOT NULL, email NVARCHAR(256) NULL, -- Passwort-Felder: Hash + Salt, nie Klartext (CWE-256) password_hash VARBINARY(512) NULL, password_salt VARBINARY(32) NULL, password_changed_at DATETIME2 NULL, password_failed_attempts INT NOT NULL CONSTRAINT df_user_pw_fails DEFAULT (0), password_locked_until DATETIME2 NULL, -- TOTP-Felder: Secret AES-verschlüsselt gespeichert (CWE-256) totp_secret VARBINARY(256) NULL, totp_enabled BIT NOT NULL CONSTRAINT df_user_totp_en DEFAULT (0), totp_verified_at DATETIME2 NULL, totp_failed_attempts INT NOT NULL CONSTRAINT df_user_totp_fails DEFAULT (0), totp_locked_until DATETIME2 NULL, totp_pending_secret VARBINARY(256) NULL, totp_pending_expires_at DATETIME2 NULL, last_mfa_at DATETIME2 NULL, last_mfa_method VARCHAR(20) NULL, is_active BIT NOT NULL CONSTRAINT df_user_active DEFAULT (1), last_login_at DATETIME2 NULL, created_at DATETIME2 NOT NULL CONSTRAINT df_user_created DEFAULT (SYSUTCDATETIME()), updated_at DATETIME2 NOT NULL CONSTRAINT df_user_updated DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_user PRIMARY KEY (id), CONSTRAINT uq_user_person UNIQUE (person_id), CONSTRAINT uq_user_username UNIQUE (username) ); CREATE INDEX ix_user_person ON auth.[user] (person_id); CREATE INDEX ix_user_username ON auth.[user] (username); CREATE INDEX ix_user_active ON auth.[user] (is_active); CREATE INDEX ix_user_totp_enabled ON auth.[user] (totp_enabled); PRINT 'auth.[user]: erstellt'; END GO -- ------------------------------------------------------------------ -- 4. auth.device — Geräte-Sessions, PQ-Keys, Refresh-Token-Hash -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'device' ) BEGIN CREATE TABLE auth.device ( id INT NOT NULL IDENTITY(1,1), device_id VARCHAR(64) NOT NULL, user_id INT NOT NULL, name NVARCHAR(100) NULL, passkey_credential_id VARCHAR(256) NULL, passkey_public_key VARBINARY(MAX) NULL, pq_dsa_public_key VARBINARY(MAX) NULL, pq_dsa_key_id VARCHAR(64) NULL, pq_kem_public_key VARBINARY(MAX) NULL, pq_kem_key_id VARCHAR(64) NULL, pq_key_created_at DATETIME2 NULL, pq_dsa_public_key_prev VARBINARY(MAX) NULL, pq_dsa_key_id_prev VARCHAR(64) NULL, pq_kem_public_key_prev VARBINARY(MAX) NULL, pq_kem_key_id_prev VARCHAR(64) NULL, pq_key_prev_expires_at DATETIME2 NULL, refresh_token_hash VARBINARY(64) NULL, refresh_token_expires_at DATETIME2 NULL, refresh_family_id VARCHAR(64) NULL, refresh_generation INT NOT NULL CONSTRAINT df_device_regen DEFAULT (0), last_ip VARCHAR(45) NULL, last_geo_country CHAR(2) NULL, last_geo_region VARCHAR(50) NULL, last_asn INT NULL, last_asn_type VARCHAR(20) NULL, last_user_agent NVARCHAR(512) NULL, session_state VARCHAR(20) NOT NULL CONSTRAINT df_device_state DEFAULT ('ACTIVE'), session_hold_until DATETIME2 NULL, session_hold_reason VARCHAR(50) NULL, session_revoked_at DATETIME2 NULL, session_revoked_reason VARCHAR(50) NULL, is_active BIT NOT NULL CONSTRAINT df_device_active DEFAULT (1), last_used_at DATETIME2 NULL, created_at DATETIME2 NOT NULL CONSTRAINT df_device_created DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_device PRIMARY KEY (id), CONSTRAINT uq_device_id UNIQUE (device_id), CONSTRAINT fk_device_user FOREIGN KEY (user_id) REFERENCES auth.[user](id) ); CREATE INDEX ix_device_user ON auth.device (user_id); CREATE INDEX ix_device_user_active ON auth.device (user_id, is_active); CREATE INDEX ix_device_session_state ON auth.device (session_state, user_id); CREATE INDEX ix_device_refresh_family ON auth.device (refresh_family_id); PRINT 'auth.device: erstellt'; END GO -- ------------------------------------------------------------------ -- 5. auth.backup_code — MFA Backup-Codes (gehashed, CWE-256) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'backup_code' ) BEGIN CREATE TABLE auth.backup_code ( id INT NOT NULL IDENTITY(1,1), user_id INT NOT NULL, code_hash VARBINARY(64) NOT NULL, code_prefix CHAR(4) NOT NULL, is_used BIT NOT NULL CONSTRAINT df_bc_used DEFAULT (0), used_at DATETIME2 NULL, used_from_ip VARCHAR(45) NULL, created_at DATETIME2 NOT NULL CONSTRAINT df_bc_created DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_backup_code PRIMARY KEY (id), CONSTRAINT fk_bc_user FOREIGN KEY (user_id) REFERENCES auth.[user](id) ); CREATE INDEX ix_backup_code_user ON auth.backup_code (user_id, is_used); PRINT 'auth.backup_code: erstellt'; END GO -- ------------------------------------------------------------------ -- 6. auth.known_location — Bekannte Standorte (Geo-IP, MFA-verifiziert) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'known_location' ) BEGIN CREATE TABLE auth.known_location ( id INT NOT NULL IDENTITY(1,1), user_id INT NOT NULL, geo_country CHAR(2) NOT NULL, geo_region VARCHAR(50) NULL, asn INT NULL, first_seen_at DATETIME2 NOT NULL CONSTRAINT df_kl_first DEFAULT (SYSUTCDATETIME()), last_seen_at DATETIME2 NOT NULL CONSTRAINT df_kl_last DEFAULT (SYSUTCDATETIME()), expires_at DATETIME2 NOT NULL, verified_via_mfa BIT NOT NULL CONSTRAINT df_kl_mfa DEFAULT (1), CONSTRAINT pk_known_location PRIMARY KEY (id), CONSTRAINT uq_known_location UNIQUE (user_id, geo_country, geo_region, asn), CONSTRAINT fk_kl_user FOREIGN KEY (user_id) REFERENCES auth.[user](id) ); CREATE INDEX ix_known_location_user ON auth.known_location (user_id, expires_at); PRINT 'auth.known_location: erstellt'; END GO -- ------------------------------------------------------------------ -- 7. auth.mfa_session — TOTP Challenge-Sessions (In-Memory-Äquivalent in DB) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'mfa_session' ) BEGIN CREATE TABLE auth.mfa_session ( id INT NOT NULL IDENTITY(1,1), mfa_session_id VARCHAR(64) NOT NULL, user_id INT NOT NULL, device_id INT NULL, reason VARCHAR(50) NOT NULL, original_action VARCHAR(50) NULL, is_verified BIT NOT NULL CONSTRAINT df_mfas_verified DEFAULT (0), verified_method VARCHAR(20) NULL, attempts INT NOT NULL CONSTRAINT df_mfas_attempts DEFAULT (0), created_at DATETIME2 NOT NULL CONSTRAINT df_mfas_created DEFAULT (SYSUTCDATETIME()), expires_at DATETIME2 NOT NULL, verified_at DATETIME2 NULL, CONSTRAINT pk_mfa_session PRIMARY KEY (id), CONSTRAINT uq_mfa_session_id UNIQUE (mfa_session_id), CONSTRAINT fk_mfas_user FOREIGN KEY (user_id) REFERENCES auth.[user](id), CONSTRAINT fk_mfas_device FOREIGN KEY (device_id) REFERENCES auth.device(id) ); CREATE INDEX ix_mfa_session_user ON auth.mfa_session (user_id, created_at); PRINT 'auth.mfa_session: erstellt'; END GO -- ------------------------------------------------------------------ -- 8. auth.nonce_cache — Replay-Protection (jti-Blacklist) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'nonce_cache' ) BEGIN CREATE TABLE auth.nonce_cache ( nonce VARCHAR(64) NOT NULL, device_id INT NOT NULL, received_at DATETIME2 NOT NULL CONSTRAINT df_nc_received DEFAULT (SYSUTCDATETIME()), expires_at DATETIME2 NOT NULL, CONSTRAINT pk_nonce_cache PRIMARY KEY (nonce) ); CREATE INDEX ix_nonce_expires ON auth.nonce_cache (expires_at); PRINT 'auth.nonce_cache: erstellt'; END GO -- ------------------------------------------------------------------ -- 9. auth.rate_limit_event — Rate-Limit Protokoll -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'rate_limit_event' ) BEGIN CREATE TABLE auth.rate_limit_event ( id INT NOT NULL IDENTITY(1,1), user_id INT NULL, device_id INT NULL, ip_address VARCHAR(45) NOT NULL, event_type VARCHAR(30) NOT NULL, created_at DATETIME2 NOT NULL CONSTRAINT df_rle_created DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_rate_limit_event PRIMARY KEY (id), CONSTRAINT fk_rle_user FOREIGN KEY (user_id) REFERENCES auth.[user](id), CONSTRAINT fk_rle_device FOREIGN KEY (device_id) REFERENCES auth.device(id) ); CREATE INDEX ix_rate_limit_user_time ON auth.rate_limit_event (user_id, created_at); CREATE INDEX ix_rate_limit_ip_time ON auth.rate_limit_event (ip_address, created_at); CREATE INDEX ix_rate_limit_cleanup ON auth.rate_limit_event (created_at); PRINT 'auth.rate_limit_event: erstellt'; END GO -- ------------------------------------------------------------------ -- 10. auth.refresh_token_family — Refresh Token Rotation Tracking -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'refresh_token_family' ) BEGIN CREATE TABLE auth.refresh_token_family ( id INT NOT NULL IDENTITY(1,1), family_id VARCHAR(64) NOT NULL, user_id INT NOT NULL, device_id INT NOT NULL, current_generation INT NOT NULL CONSTRAINT df_rtf_gen DEFAULT (0), is_revoked BIT NOT NULL CONSTRAINT df_rtf_revoked DEFAULT (0), revoked_reason VARCHAR(50) NULL, created_at DATETIME2 NOT NULL CONSTRAINT df_rtf_created DEFAULT (SYSUTCDATETIME()), last_used_at DATETIME2 NULL, revoked_at DATETIME2 NULL, CONSTRAINT pk_refresh_token_family PRIMARY KEY (id), CONSTRAINT uq_rtf_family_id UNIQUE (family_id), CONSTRAINT fk_rtf_user FOREIGN KEY (user_id) REFERENCES auth.[user](id), CONSTRAINT fk_rtf_device FOREIGN KEY (device_id) REFERENCES auth.device(id) ); CREATE INDEX ix_refresh_family_user ON auth.refresh_token_family (user_id, is_revoked); PRINT 'auth.refresh_token_family: erstellt'; END GO -- ------------------------------------------------------------------ -- 11. auth.role_permission — Rollen-Berechtigungs-Zuordnung (Junction) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'role_permission' ) BEGIN CREATE TABLE auth.role_permission ( role_id INT NOT NULL, permission_id INT NOT NULL, CONSTRAINT pk_role_permission PRIMARY KEY (role_id, permission_id), CONSTRAINT fk_rp_role FOREIGN KEY (role_id) REFERENCES auth.role(id), CONSTRAINT fk_rp_permission FOREIGN KEY (permission_id) REFERENCES auth.permission(id) ); PRINT 'auth.role_permission: erstellt'; END GO -- ------------------------------------------------------------------ -- 12. auth.server_key — Server-seitige ML-DSA/ML-KEM Schlüssel -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'server_key' ) BEGIN CREATE TABLE auth.server_key ( id INT NOT NULL IDENTITY(1,1), key_id VARCHAR(64) NOT NULL, key_type VARCHAR(20) NOT NULL, algorithm VARCHAR(30) NOT NULL, public_key VARBINARY(MAX) NOT NULL, -- Private Key verschlüsselt gespeichert (CWE-256) private_key_encrypted VARBINARY(MAX) NOT NULL, is_active BIT NOT NULL CONSTRAINT df_sk_active DEFAULT (1), not_before DATETIME2 NOT NULL CONSTRAINT df_sk_before DEFAULT (SYSUTCDATETIME()), not_after DATETIME2 NOT NULL, created_at DATETIME2 NOT NULL CONSTRAINT df_sk_created DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_server_key PRIMARY KEY (id), CONSTRAINT uq_server_key_id UNIQUE (key_id) ); CREATE INDEX ix_server_key_type_active ON auth.server_key (key_type, is_active, not_after); PRINT 'auth.server_key: erstellt'; END GO -- ------------------------------------------------------------------ -- 13. auth.user_role — User-Rollen-Zuordnung (mit optionalem Site-Scope) -- Hinweis: site_id referenziert org.site (Cross-DB → kein echtes FK, -- Integrität app-seitig sichergestellt) -- ------------------------------------------------------------------ IF NOT EXISTS ( SELECT 1 FROM sys.tables t JOIN sys.schemas s ON s.schema_id = t.schema_id WHERE s.name = 'auth' AND t.name = 'user_role' ) BEGIN CREATE TABLE auth.user_role ( id INT NOT NULL IDENTITY(1,1), user_id INT NOT NULL, role_id INT NOT NULL, site_id INT NULL, -- NULL = global, sonst Standort-Scope granted_at DATETIME2 NOT NULL CONSTRAINT df_ur_granted DEFAULT (SYSUTCDATETIME()), granted_by INT NULL, CONSTRAINT pk_user_role PRIMARY KEY (id), CONSTRAINT uq_user_role_site UNIQUE (user_id, role_id, site_id), CONSTRAINT fk_ur_user FOREIGN KEY (user_id) REFERENCES auth.[user](id), CONSTRAINT fk_ur_role FOREIGN KEY (role_id) REFERENCES auth.role(id), CONSTRAINT fk_ur_granted_by FOREIGN KEY (granted_by) REFERENCES auth.[user](id) ); PRINT 'auth.user_role: erstellt'; END GO PRINT '=== siam_create_tables: 13 Tabellen in ENIVERSSIAM.auth bereit ==='; === siam_copy_data.sql — Daten ENIVERSCAFM → ENIVERSSIAM === /* ================================================================ siam_copy_data.sql Quelle: ENIVERSCAFM.auth.* Ziel: ENIVERSSIAM.auth.* Zweck: Bestehende Auth-Daten in neue Identity-Datenbank übertragen Reihenfolge: Parents vor Children (FK-Abhängigkeiten beachten) ================================================================ */ -- ------------------------------------------------------------------ -- 1. auth.permission — 15 Zeilen, keine Abhängigkeiten -- ------------------------------------------------------------------ SET IDENTITY_INSERT ENIVERSSIAM.auth.permission ON; INSERT INTO ENIVERSSIAM.auth.permission (id, resource, action, scope, description) SELECT id, resource, action, scope, description FROM ENIVERSCAFM.auth.permission; SET IDENTITY_INSERT ENIVERSSIAM.auth.permission OFF; PRINT 'auth.permission: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; -- ------------------------------------------------------------------ -- 2. auth.role — 26 Zeilen, keine Abhängigkeiten -- ------------------------------------------------------------------ SET IDENTITY_INSERT ENIVERSSIAM.auth.role ON; INSERT INTO ENIVERSSIAM.auth.role (id, code, name, description, is_system_role, is_active, created_at, updated_at) SELECT id, code, name, description, is_system_role, is_active, created_at, updated_at FROM ENIVERSCAFM.auth.role; SET IDENTITY_INSERT ENIVERSSIAM.auth.role OFF; PRINT 'auth.role: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; -- ------------------------------------------------------------------ -- 3. auth.[user] — 241 Zeilen, hängt von core.person ab (person_id) -- FK auth.[user] → core.person wird nach siam_person.sql gesetzt -- ------------------------------------------------------------------ SET IDENTITY_INSERT ENIVERSSIAM.auth.[user] ON; INSERT INTO ENIVERSSIAM.auth.[user] ( id, person_id, username, email, password_hash, password_salt, password_changed_at, password_failed_attempts, password_locked_until, totp_secret, totp_enabled, totp_verified_at, totp_failed_attempts, totp_locked_until, totp_pending_secret, totp_pending_expires_at, last_mfa_at, last_mfa_method, is_active, last_login_at, created_at, updated_at ) SELECT id, person_id, username, email, password_hash, password_salt, password_changed_at, password_failed_attempts, password_locked_until, totp_secret, totp_enabled, totp_verified_at, totp_failed_attempts, totp_locked_until, totp_pending_secret, totp_pending_expires_at, last_mfa_at, last_mfa_method, is_active, last_login_at, created_at, updated_at FROM ENIVERSCAFM.auth.[user]; SET IDENTITY_INSERT ENIVERSSIAM.auth.[user] OFF; PRINT 'auth.[user]: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; -- ------------------------------------------------------------------ -- 4–8. Tabellen ohne Daten (0 Zeilen) — Synonyme genügen -- ------------------------------------------------------------------ SET IDENTITY_INSERT ENIVERSSIAM.auth.device ON; INSERT INTO ENIVERSSIAM.auth.device ( id, device_id, user_id, name, passkey_credential_id, passkey_public_key, pq_dsa_public_key, pq_dsa_key_id, pq_kem_public_key, pq_kem_key_id, pq_key_created_at, pq_dsa_public_key_prev, pq_dsa_key_id_prev, pq_kem_public_key_prev, pq_kem_key_id_prev, pq_key_prev_expires_at, refresh_token_hash, refresh_token_expires_at, refresh_family_id, refresh_generation, last_ip, last_geo_country, last_geo_region, last_asn, last_asn_type, last_user_agent, session_state, session_hold_until, session_hold_reason, session_revoked_at, session_revoked_reason, is_active, last_used_at, created_at ) SELECT id, device_id, user_id, name, passkey_credential_id, passkey_public_key, pq_dsa_public_key, pq_dsa_key_id, pq_kem_public_key, pq_kem_key_id, pq_key_created_at, pq_dsa_public_key_prev, pq_dsa_key_id_prev, pq_kem_public_key_prev, pq_kem_key_id_prev, pq_key_prev_expires_at, refresh_token_hash, refresh_token_expires_at, refresh_family_id, refresh_generation, last_ip, last_geo_country, last_geo_region, last_asn, last_asn_type, last_user_agent, session_state, session_hold_until, session_hold_reason, session_revoked_at, session_revoked_reason, is_active, last_used_at, created_at FROM ENIVERSCAFM.auth.device; SET IDENTITY_INSERT ENIVERSSIAM.auth.device OFF; PRINT 'auth.device: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; SET IDENTITY_INSERT ENIVERSSIAM.auth.backup_code ON; INSERT INTO ENIVERSSIAM.auth.backup_code (id, user_id, code_hash, code_prefix, is_used, used_at, used_from_ip, created_at) SELECT id, user_id, code_hash, code_prefix, is_used, used_at, used_from_ip, created_at FROM ENIVERSCAFM.auth.backup_code; SET IDENTITY_INSERT ENIVERSSIAM.auth.backup_code OFF; PRINT 'auth.backup_code: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; SET IDENTITY_INSERT ENIVERSSIAM.auth.known_location ON; INSERT INTO ENIVERSSIAM.auth.known_location (id, user_id, geo_country, geo_region, asn, first_seen_at, last_seen_at, expires_at, verified_via_mfa) SELECT id, user_id, geo_country, geo_region, asn, first_seen_at, last_seen_at, expires_at, verified_via_mfa FROM ENIVERSCAFM.auth.known_location; SET IDENTITY_INSERT ENIVERSSIAM.auth.known_location OFF; PRINT 'auth.known_location: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; SET IDENTITY_INSERT ENIVERSSIAM.auth.mfa_session ON; INSERT INTO ENIVERSSIAM.auth.mfa_session (id, mfa_session_id, user_id, device_id, reason, original_action, is_verified, verified_method, attempts, created_at, expires_at, verified_at) SELECT id, mfa_session_id, user_id, device_id, reason, original_action, is_verified, verified_method, attempts, created_at, expires_at, verified_at FROM ENIVERSCAFM.auth.mfa_session; SET IDENTITY_INSERT ENIVERSSIAM.auth.mfa_session OFF; PRINT 'auth.mfa_session: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; INSERT INTO ENIVERSSIAM.auth.nonce_cache (nonce, device_id, received_at, expires_at) SELECT nonce, device_id, received_at, expires_at FROM ENIVERSCAFM.auth.nonce_cache; PRINT 'auth.nonce_cache: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; SET IDENTITY_INSERT ENIVERSSIAM.auth.rate_limit_event ON; INSERT INTO ENIVERSSIAM.auth.rate_limit_event (id, user_id, device_id, ip_address, event_type, created_at) SELECT id, user_id, device_id, ip_address, event_type, created_at FROM ENIVERSCAFM.auth.rate_limit_event; SET IDENTITY_INSERT ENIVERSSIAM.auth.rate_limit_event OFF; PRINT 'auth.rate_limit_event: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; SET IDENTITY_INSERT ENIVERSSIAM.auth.refresh_token_family ON; INSERT INTO ENIVERSSIAM.auth.refresh_token_family (id, family_id, user_id, device_id, current_generation, is_revoked, revoked_reason, created_at, last_used_at, revoked_at) SELECT id, family_id, user_id, device_id, current_generation, is_revoked, revoked_reason, created_at, last_used_at, revoked_at FROM ENIVERSCAFM.auth.refresh_token_family; SET IDENTITY_INSERT ENIVERSSIAM.auth.refresh_token_family OFF; PRINT 'auth.refresh_token_family: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; -- ------------------------------------------------------------------ -- 9. auth.role_permission — 33 Zeilen, hängt von role + permission ab -- ------------------------------------------------------------------ INSERT INTO ENIVERSSIAM.auth.role_permission (role_id, permission_id) SELECT role_id, permission_id FROM ENIVERSCAFM.auth.role_permission; PRINT 'auth.role_permission: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; SET IDENTITY_INSERT ENIVERSSIAM.auth.server_key ON; INSERT INTO ENIVERSSIAM.auth.server_key (id, key_id, key_type, algorithm, public_key, private_key_encrypted, is_active, not_before, not_after, created_at) SELECT id, key_id, key_type, algorithm, public_key, private_key_encrypted, is_active, not_before, not_after, created_at FROM ENIVERSCAFM.auth.server_key; SET IDENTITY_INSERT ENIVERSSIAM.auth.server_key OFF; PRINT 'auth.server_key: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; -- ------------------------------------------------------------------ -- 10. auth.user_role — 9.445 Zeilen, zuletzt (FK auf user + role) -- ------------------------------------------------------------------ SET IDENTITY_INSERT ENIVERSSIAM.auth.user_role ON; INSERT INTO ENIVERSSIAM.auth.user_role (id, user_id, role_id, site_id, granted_at, granted_by) SELECT id, user_id, role_id, site_id, granted_at, granted_by FROM ENIVERSCAFM.auth.user_role; SET IDENTITY_INSERT ENIVERSSIAM.auth.user_role OFF; PRINT 'auth.user_role: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; PRINT '=== siam_copy_data: Datenmigration ENIVERSCAFM → ENIVERSSIAM abgeschlossen ==='; === siam_person.sql — core.person anlegen, befüllen, FK + Synonym === /* ================================================================ siam_person.sql Zweck: core.person in ENIVERSSIAM erstellen, Daten aus ENIVERSCAFM.hr.person übertragen, FK von auth.[user] wiederherstellen, Synonym in ENIVERSCAFM anlegen ================================================================ */ -- ------------------------------------------------------------------ -- Schritt 1: Schema 'core' und Tabelle 'core.person' in ENIVERSSIAM -- ------------------------------------------------------------------ USE ENIVERSSIAM; GO IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'core') EXEC('CREATE SCHEMA core'); GO CREATE TABLE core.person ( id INT NOT NULL IDENTITY(1,1), first_name NVARCHAR(50) NOT NULL, last_name NVARCHAR(50) NOT NULL, position NVARCHAR(100) NULL, notes NVARCHAR(MAX) NULL, is_active BIT NOT NULL CONSTRAINT df_person_active DEFAULT (1), is_system_user BIT NOT NULL CONSTRAINT df_person_sysuser DEFAULT (0), created_at DATETIME2 NOT NULL CONSTRAINT df_person_created DEFAULT (SYSUTCDATETIME()), updated_at DATETIME2 NOT NULL CONSTRAINT df_person_updated DEFAULT (SYSUTCDATETIME()), CONSTRAINT pk_person PRIMARY KEY (id) ); PRINT 'core.person: erstellt'; GO -- ------------------------------------------------------------------ -- Schritt 2: Daten aus ENIVERSCAFM.hr.person übertragen (241 Zeilen) -- IDENTITY_INSERT ON, damit Original-IDs erhalten bleiben -- ------------------------------------------------------------------ SET IDENTITY_INSERT ENIVERSSIAM.core.person ON; INSERT INTO ENIVERSSIAM.core.person (id, first_name, last_name, position, notes, is_active, is_system_user, created_at, updated_at) SELECT id, first_name, last_name, position, notes, is_active, is_system_user, created_at, updated_at FROM ENIVERSCAFM.hr.person; SET IDENTITY_INSERT ENIVERSSIAM.core.person OFF; PRINT 'core.person: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen kopiert'; GO -- ------------------------------------------------------------------ -- Schritt 3: FK auth.[user].person_id → core.person(id) wiederherstellen -- (jetzt echter Intra-DB-FK innerhalb ENIVERSSIAM) -- ------------------------------------------------------------------ ALTER TABLE auth.[user] ADD CONSTRAINT fk_user_person FOREIGN KEY (person_id) REFERENCES core.person(id); PRINT 'FK auth.[user].person_id → core.person(id): wiederhergestellt'; GO -- ------------------------------------------------------------------ -- Schritt 4: hr.person in ENIVERSCAFM droppen und Synonym anlegen -- ------------------------------------------------------------------ USE ENIVERSCAFM; GO DROP TABLE hr.person; PRINT 'hr.person: aus ENIVERSCAFM entfernt'; GO CREATE SYNONYM hr.person FOR ENIVERSSIAM.core.person; PRINT 'Synonym hr.person → ENIVERSSIAM.core.person: erstellt'; GO PRINT '=== siam_person: core.person Migration abgeschlossen ==='; === siam_drop_and_synonyms.sql — FKs droppen, Tabellen droppen, Synonyme erstellen === /* ================================================================ siam_drop_and_synonyms.sql Ziel: ENIVERSCAFM Zweck: Cross-Schema FKs und auth-Tabellen entfernen, 13 Synonyme auf ENIVERSSIAM erstellen Hinweis: Reihenfolge zwingend — Children vor Parents droppen ================================================================ */ USE ENIVERSCAFM; GO -- ------------------------------------------------------------------ -- Schritt 1: Cross-Schema FKs droppen -- (audit→auth, settings→auth, auth→hr, auth→org) -- Diese können nach DB-Move nicht als echte FKs wieder- -- hergestellt werden — Integrität wird app-seitig gewährleistet -- ------------------------------------------------------------------ IF OBJECT_ID('audit.event_log') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_el_user' AND parent_object_id = OBJECT_ID('audit.event_log')) ALTER TABLE audit.event_log DROP CONSTRAINT fk_el_user; IF OBJECT_ID('audit.event_log') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_el_device' AND parent_object_id = OBJECT_ID('audit.event_log')) ALTER TABLE audit.event_log DROP CONSTRAINT fk_el_device; IF OBJECT_ID('audit.pq_signature') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_pqs_user' AND parent_object_id = OBJECT_ID('audit.pq_signature')) ALTER TABLE audit.pq_signature DROP CONSTRAINT fk_pqs_user; IF OBJECT_ID('audit.pq_signature') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_pqs_device' AND parent_object_id = OBJECT_ID('audit.pq_signature')) ALTER TABLE audit.pq_signature DROP CONSTRAINT fk_pqs_device; IF OBJECT_ID('settings.user_preference') IS NOT NULL AND EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_user_preference_user' AND parent_object_id = OBJECT_ID('settings.user_preference')) ALTER TABLE settings.user_preference DROP CONSTRAINT fk_user_preference_user; IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_user_person' AND parent_object_id = OBJECT_ID('auth.user')) ALTER TABLE auth.[user] DROP CONSTRAINT fk_user_person; IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'fk_ur_site' AND parent_object_id = OBJECT_ID('auth.user_role')) ALTER TABLE auth.user_role DROP CONSTRAINT fk_ur_site; PRINT 'Cross-Schema FKs: entfernt'; GO -- ------------------------------------------------------------------ -- Schritt 2: Interne auth-FKs droppen (Children zuerst) -- ------------------------------------------------------------------ -- auth.user_role hängt von auth.[user] + auth.role ab ALTER TABLE auth.user_role DROP CONSTRAINT fk_ur_granted_by; ALTER TABLE auth.user_role DROP CONSTRAINT fk_ur_user; ALTER TABLE auth.user_role DROP CONSTRAINT fk_ur_role; -- auth.role_permission ALTER TABLE auth.role_permission DROP CONSTRAINT fk_rp_permission; ALTER TABLE auth.role_permission DROP CONSTRAINT fk_rp_role; -- auth.refresh_token_family ALTER TABLE auth.refresh_token_family DROP CONSTRAINT fk_rtf_device; ALTER TABLE auth.refresh_token_family DROP CONSTRAINT fk_rtf_user; -- auth.rate_limit_event ALTER TABLE auth.rate_limit_event DROP CONSTRAINT fk_rle_device; ALTER TABLE auth.rate_limit_event DROP CONSTRAINT fk_rle_user; -- auth.mfa_session ALTER TABLE auth.mfa_session DROP CONSTRAINT fk_mfas_device; ALTER TABLE auth.mfa_session DROP CONSTRAINT fk_mfas_user; -- auth.known_location ALTER TABLE auth.known_location DROP CONSTRAINT fk_kl_user; -- auth.backup_code ALTER TABLE auth.backup_code DROP CONSTRAINT fk_bc_user; -- auth.device (letzte Child-Tabelle von auth.[user]) ALTER TABLE auth.device DROP CONSTRAINT fk_device_user; PRINT 'Interne auth-FKs: entfernt'; GO -- ------------------------------------------------------------------ -- Schritt 3: auth-Tabellen droppen (Children vor Parents) -- ------------------------------------------------------------------ DROP TABLE auth.nonce_cache; DROP TABLE auth.mfa_session; DROP TABLE auth.rate_limit_event; DROP TABLE auth.refresh_token_family; DROP TABLE auth.backup_code; DROP TABLE auth.known_location; DROP TABLE auth.role_permission; DROP TABLE auth.user_role; DROP TABLE auth.device; DROP TABLE auth.[user]; DROP TABLE auth.role; DROP TABLE auth.permission; DROP TABLE auth.server_key; PRINT 'auth-Tabellen: aus ENIVERSCAFM entfernt'; GO -- ------------------------------------------------------------------ -- Schritt 4: 13 Synonyme auf ENIVERSSIAM.auth.* erstellen -- Bestehender Code kann weiterhin ENIVERSCAFM.auth.* -- ansprechen — transparent umgeleitet -- ------------------------------------------------------------------ CREATE SYNONYM auth.backup_code FOR ENIVERSSIAM.auth.backup_code; CREATE SYNONYM auth.device FOR ENIVERSSIAM.auth.device; CREATE SYNONYM auth.known_location FOR ENIVERSSIAM.auth.known_location; CREATE SYNONYM auth.mfa_session FOR ENIVERSSIAM.auth.mfa_session; CREATE SYNONYM auth.nonce_cache FOR ENIVERSSIAM.auth.nonce_cache; CREATE SYNONYM auth.permission FOR ENIVERSSIAM.auth.permission; CREATE SYNONYM auth.rate_limit_event FOR ENIVERSSIAM.auth.rate_limit_event; CREATE SYNONYM auth.refresh_token_family FOR ENIVERSSIAM.auth.refresh_token_family; CREATE SYNONYM auth.role FOR ENIVERSSIAM.auth.role; CREATE SYNONYM auth.role_permission FOR ENIVERSSIAM.auth.role_permission; CREATE SYNONYM auth.server_key FOR ENIVERSSIAM.auth.server_key; CREATE SYNONYM auth.[user] FOR ENIVERSSIAM.auth.[user]; CREATE SYNONYM auth.user_role FOR ENIVERSSIAM.auth.user_role; PRINT '13 Synonyme in ENIVERSCAFM.auth: erstellt'; GO PRINT '=== siam_drop_and_synonyms: Phase 1 vollständig abgeschlossen ==='; ===== Phase 2 — LD-System: ENIVERS → ENIVERSSIAM ===== ==== Quellsysteme ==== === Quelle 1: ENIVERS-Datenbank (SQL Server) === **Verbindung:** ''localhost'', Datenbank ''ENIVERS'', Schema ''dbo'' ^ ENIVERS-Tabelle ^ Zeilen ^ Ziel ENIVERSSIAM ^ Mapping-Hinweise ^ | ''tblPerson'' | 80 | ''core.person'' + ''auth.user'' | Feldmapping siehe unten | | ''tblRolle'' | 14 | ''auth.role'' | ''strRolAbk''→''code'', ''strRolName''→''name'' | | ''tblPersonRolle'' | 70 | ''auth.user_role'' | Ohne Site-Scope (''site_id = NULL'') | | ''tblPersonRolleBetrieb'' | 35 | ''auth.user_role'' | Mit Site-Scope (''fiPrbBetrieb''→''site_id'') | | ''tblRecht'' | 1 | ''auth.permission'' | Nur 1 Eintrag — manuell prüfen | | ''tblRolleRechtObjekt'' | 1.130 | ''auth.role_permission'' | Strukturell verschieden — separate Phase | === Quelle 2: LdUsr.accdb (MS Access, Datei-Datenbank) === **Pfad:** ''D:\Workspace\AMED\LdUsr_ACCDB\LdUsr.accdb'' **Export-Stand:** 2026-01-31 (288 Formulare, 95 Berichte, 76 Module, 19 Makros) Die Access-Datei enthält primär die **Benutzeroberfläche** (Formulare, Berichte, VBA) und nutzt **verknüpfte Tabellen** (Linked Tables) aus der ENIVERS SQL-Server-Datenbank. Es gibt keine eigenständigen Access-nativen Tabellen — alle Daten kommen aus Quelle 1. Relevant als Referenz für Geschäftsregeln: * Formular-Logik: ''fpopLogin'', ''fpopMandant'' * Rollen-/Rechte-Zuordnung: ''sfrRolleRechtObjekt'', ''sfrPersonRolle'' * Scheduling-Logik: ''ufrmAgent_Schedule'', ''ufrmAgent_Log'' ==== Feldmapping: tblPerson → core.person + auth.user ==== ^ tblPerson-Feld ^ Ziel-Tabelle ^ Ziel-Feld ^ Bemerkung ^ | ''idPerson'' | ''core.person'' | ''id'' | IDENTITY_INSERT ON — IDs 1–179, kein Konflikt mit 591–2896 | | ''strPeVorname'' | ''core.person'' | ''first_name'' | ISNULL → leer (''∅'') | | ''strPeNachname'' | ''core.person'' | ''last_name'' | ISNULL → leer | | ''strPePosition'' | ''core.person'' | ''position'' | | | ''memPeNotiz'' | ''core.person'' | ''notes'' | | | ''bitPeAktiv'' | ''core.person'' | ''is_active'' | ISNULL → 1 | | ''bitPeSysUser'' | ''core.person'' | ''is_system_user'' | ISNULL → 0 | | ''dtCreated'' | ''core.person'' | ''created_at'' | | | ''dtUpdated'' | ''core.person'' | ''updated_at'' | ISNULL → dtCreated | | ''idPerson'' | ''auth.[user]'' | ''person_id'' | FK auf core.person | | ''strPeWinUser'' | ''auth.[user]'' | ''username'' | Windows-Username → für Kerberos SSO | | ''strPeAccUser'' | — | — | **Nicht übernehmen** — Legacy Access-Credential | | ''strPeAccPasswort'' | — | — | **Nicht übernehmen** — Klartext-Passwort (CWE-256) | | ''dtmPeGueltigBis'' | — | — | Prüfen ob für ''license.expires'' relevant | | ''fiPeFirma'' | — | — | Firma-Referenz — kein Ziel definiert | | ''fiPeBetrieb'' | — | — | Betrieb-Referenz → org.site prüfen | > **Sicherheitshinweis:** ''strPeAccPasswort'' enthält Klartext-Passwörter aus dem alten > Access-System. Diese Spalte darf **niemals** übernommen werden (CWE-256). > Benutzer erhalten beim ersten Login einen Passwort-Reset-Link. ==== Feldmapping: tblRolle → auth.role ==== ^ tblRolle-Feld ^ Ziel-Feld ^ Bemerkung ^ | ''idRolle'' | — | Kein IDENTITY_INSERT — ID-Konflikt mit id=4 (viewer) | | ''strRolAbk'' | ''code'' | Code-basiertes Matching statt ID-basiert | | ''strRolName'' | ''name'' | | | ''memRolNotiz'' | ''description'' | | | ''bitRolSysRole'' | ''is_system_role'' | ISNULL → 0 | | ''bitRolAktiv'' | ''is_active'' | ISNULL → 1 | | ''dtCreated'' | ''created_at'' | | | ''dtEdited'' | ''updated_at'' | ISNULL → dtCreated | ==== Feldmapping: tblPersonRolle + tblPersonRolleBetrieb → auth.user_role ==== ^ Quell-Tabelle ^ Quell-Feld ^ Ziel-Feld ^ Bemerkung ^ | ''tblPersonRolle'' | ''fiPrPerson'' | ''user_id'' | Via #person_map (enivers_id → siam_user_id) | | ''tblPersonRolle'' | ''fiPrRolle'' | ''role_id'' | Via #role_map (enivers_id → siam_id) | | ''tblPersonRolle'' | ''dtCreated'' | ''granted_at'' | | | ''tblPersonRolle'' | — | ''site_id'' | NULL (kein Site-Scope) | | ''tblPersonRolleBetrieb'' | ''fiPrbPerson'' | ''user_id'' | Via #person_map | | ''tblPersonRolleBetrieb'' | ''fiPrbRolle'' | ''role_id'' | Via #role_map | | ''tblPersonRolleBetrieb'' | ''fiPrbBetrieb'' | ''site_id'' | Direkte Übernahme des Betrieb-IDs | | ''tblPersonRolleBetrieb'' | ''dtCreated'' | ''granted_at'' | | ==== Besonderheiten ==== === Rollen-ID-Konflikt === ''ENIVERS.dbo.tblRolle'' enthält ''idRolle = 4'' — in ENIVERSSIAM ist ''auth.role.id = 4'' bereits durch eine System-Rolle belegt. Kein IDENTITY_INSERT für ''auth.role'' möglich. **Lösung:** Code-basiertes Mapping über temporäre ''#role_map''-Tabelle: * Matching via ''LOWER(ar.code) = LOWER(r.strRolAbk)'' * Für bestehende Rollen (z. B. ''entwickler'', ''admins'') → existing id übernehmen * Für neue LD-Rollen → auto-generierte IDs aus IDENTITY === Username-Duplikate === 3 Duplikat-Paare in ''tblPerson'' (gleicher ''strPeWinUser'', verschiedene ''idPerson''): ^ strPeWinUser ^ idPerson (Duplikate) ^ Lösung ^ | ''uid1041442'' | 150, 151 | MIN(idPerson) → 150 | | ''EID0109386'' | 171, 172 | MIN(idPerson) → 171 | | ''pede0153'' | 129, 168 | MIN(idPerson) → 129 | **Lösung:** ''WITH dedup AS (SELECT MIN(idPerson) ... GROUP BY strPeWinUser)'' Ergebnis: 73 neue ''auth.user''-Einträge statt 76 (3 Duplikate zusammengeführt). ==== Skripte Phase 2 ==== === ld_migration.sql — Hauptmigration === /* ================================================================ ld_migration.sql Quelle: ENIVERS.dbo (tblPerson, tblRolle, tblPersonRolle, tblPersonRolleBetrieb) Ziel: ENIVERSSIAM (core.person, auth.[user], auth.role, auth.user_role) Strategie: core.person — IDENTITY_INSERT ON (IDs 1–179 kollidieren nicht mit bestehenden 591–2896) auth.[user] — auto-ID (kein IDENTITY_INSERT) auth.role — auto-ID; Code-basiertes Mapping alter → neuer IDs auth.user_role — nur fehlende Einträge einfügen (NOT EXISTS) Passwortfelder — nicht übernommen (CWE-256, Klartext) ================================================================ */ USE ENIVERSSIAM; GO -- ------------------------------------------------------------------ -- Schritt 1: Neue Personen einfügen (76 Zeilen, IDENTITY_INSERT) -- Nur Personen, die noch keinen übereinstimmenden -- auth.[user]-Eintrag haben (Collation-sicherer Vergleich) -- ------------------------------------------------------------------ SET IDENTITY_INSERT core.person ON; INSERT INTO core.person (id, first_name, last_name, position, notes, is_active, is_system_user, created_at, updated_at) SELECT p.idPerson, ISNULL(p.strPeVorname, ''), ISNULL(p.strPeNachname, ''), p.strPePosition, p.memPeNotiz, ISNULL(p.bitPeAktiv, 1), ISNULL(p.bitPeSysUser, 0), p.dtCreated, ISNULL(p.dtUpdated, p.dtCreated) FROM ENIVERS.dbo.tblPerson p WHERE NOT EXISTS ( SELECT 1 FROM auth.[user] u WHERE LOWER(u.username COLLATE Latin1_General_CI_AS) = LOWER(p.strPeWinUser COLLATE Latin1_General_CI_AS) ); SET IDENTITY_INSERT core.person OFF; PRINT 'core.person: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' neue Zeilen eingefügt'; GO -- ------------------------------------------------------------------ -- Schritt 2: Neue auth.[user] einfügen (auto-ID) -- person_id = idPerson aus tblPerson (soeben eingefügt) -- username = strPeWinUser (Windows-Login für Kerberos) -- Kein Passwort — Legacy-Passwörter nicht übernehmen (CWE-256) -- ------------------------------------------------------------------ INSERT INTO auth.[user] (person_id, username, is_active, created_at, updated_at) SELECT p.idPerson, p.strPeWinUser, ISNULL(p.bitPeAktiv, 1), p.dtCreated, ISNULL(p.dtUpdated, p.dtCreated) FROM ENIVERS.dbo.tblPerson p WHERE NOT EXISTS ( SELECT 1 FROM auth.[user] u WHERE LOWER(u.username COLLATE Latin1_General_CI_AS) = LOWER(p.strPeWinUser COLLATE Latin1_General_CI_AS) ); PRINT 'auth.[user]: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' neue Zeilen eingefügt'; GO -- ------------------------------------------------------------------ -- Schritt 3: Neue Rollen einfügen (auto-ID) -- Kein IDENTITY_INSERT — idRolle=4 kollidiert mit -- bestehendem System-Role (viewer). Code-basiertes Matching. -- 12 neue LD-spezifische Rollen werden eingefügt. -- ------------------------------------------------------------------ INSERT INTO auth.role (code, name, description, is_system_role, is_active, created_at, updated_at) SELECT r.strRolAbk, r.strRolName, r.memRolNotiz, ISNULL(r.bitRolSysRole, 0), ISNULL(r.bitRolAktiv, 1), r.dtCreated, ISNULL(r.dtEdited, r.dtCreated) FROM ENIVERS.dbo.tblRolle r WHERE NOT EXISTS ( SELECT 1 FROM auth.role ar WHERE LOWER(ar.code COLLATE Latin1_General_CI_AS) = LOWER(r.strRolAbk COLLATE Latin1_General_CI_AS) ); PRINT 'auth.role: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' neue Zeilen eingefügt'; GO -- ------------------------------------------------------------------ -- Schritt 4: Rollen-ID-Mapping aufbauen (#role_map, temporär) -- Verbindet ENIVERS idRolle mit dem tatsächlichen -- ENIVERSSIAM auth.role.id (via Code-Matching) -- ------------------------------------------------------------------ IF OBJECT_ID('tempdb..#role_map') IS NOT NULL DROP TABLE #role_map; CREATE TABLE #role_map ( enivers_id INT NOT NULL, siam_id INT NOT NULL ); INSERT INTO #role_map (enivers_id, siam_id) SELECT r.idRolle, ar.id FROM ENIVERS.dbo.tblRolle r JOIN auth.role ar ON LOWER(ar.code COLLATE Latin1_General_CI_AS) = LOWER(r.strRolAbk COLLATE Latin1_General_CI_AS); PRINT '#role_map: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Mappings erstellt'; -- ------------------------------------------------------------------ -- Schritt 5: Personen-ID-Mapping aufbauen (#person_map, temporär) -- Verbindet ENIVERS idPerson mit ENIVERSSIAM auth.[user].id -- ------------------------------------------------------------------ IF OBJECT_ID('tempdb..#person_map') IS NOT NULL DROP TABLE #person_map; CREATE TABLE #person_map ( enivers_id INT NOT NULL, siam_user_id INT NOT NULL ); INSERT INTO #person_map (enivers_id, siam_user_id) SELECT p.idPerson, u.id FROM ENIVERS.dbo.tblPerson p JOIN auth.[user] u ON LOWER(u.username COLLATE Latin1_General_CI_AS) = LOWER(p.strPeWinUser COLLATE Latin1_General_CI_AS); PRINT '#person_map: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Mappings erstellt'; -- ------------------------------------------------------------------ -- Schritt 6a: auth.user_role aus tblPersonRolle (kein Site-Scope) -- Nur einfügen wenn Kombination noch nicht vorhanden -- ------------------------------------------------------------------ INSERT INTO auth.user_role (user_id, role_id, granted_at, granted_by) SELECT pm.siam_user_id, rm.siam_id, pr.dtCreated, NULL FROM ENIVERS.dbo.tblPersonRolle pr JOIN #person_map pm ON pm.enivers_id = pr.fiPrPerson JOIN #role_map rm ON rm.enivers_id = pr.fiPrRolle WHERE NOT EXISTS ( SELECT 1 FROM auth.user_role ur WHERE ur.user_id = pm.siam_user_id AND ur.role_id = rm.siam_id AND ur.site_id IS NULL ); PRINT 'auth.user_role (ohne Site): ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen eingefügt'; -- ------------------------------------------------------------------ -- Schritt 6b: auth.user_role aus tblPersonRolleBetrieb (mit Site-Scope) -- fiPrbBetrieb → site_id (FK auf org.site, Cross-DB, -- app-seitig sichergestellt) -- ------------------------------------------------------------------ INSERT INTO auth.user_role (user_id, role_id, site_id, granted_at, granted_by) SELECT pm.siam_user_id, rm.siam_id, prb.fiPrbBetrieb, -- site_id: direkte Übernahme des Betrieb-IDs prb.dtCreated, NULL FROM ENIVERS.dbo.tblPersonRolleBetrieb prb JOIN #person_map pm ON pm.enivers_id = prb.fiPrbPerson JOIN #role_map rm ON rm.enivers_id = prb.fiPrbRolle WHERE NOT EXISTS ( SELECT 1 FROM auth.user_role ur WHERE ur.user_id = pm.siam_user_id AND ur.role_id = rm.siam_id AND ur.site_id = prb.fiPrbBetrieb ); PRINT 'auth.user_role (mit Site): ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen eingefügt'; GO -- ------------------------------------------------------------------ -- Schritt 7: Ergebnis-Übersicht -- ------------------------------------------------------------------ SELECT 'core.person' AS tabelle, COUNT(*) AS gesamt FROM core.person UNION ALL SELECT 'auth.[user]', COUNT(*) FROM auth.[user] UNION ALL SELECT 'auth.role', COUNT(*) FROM auth.role UNION ALL SELECT 'auth.user_role', COUNT(*) FROM auth.user_role; PRINT '=== ld_migration: Phase 2 abgeschlossen ==='; GO === ld_migration_users_fix.sql — Username-Duplikat-Fix === /* ================================================================ ld_migration_users_fix.sql Zweck: auth.[user] für neue ENIVERS-Personen einfügen (Schritt 2 aus ld_migration.sql schlug fehl wegen 3 Username-Duplikaten in tblPerson) Duplikate: uid1041442 (idPerson 150 + 151) EID0109386 (idPerson 171 + 172) pede0153 (idPerson 129 + 168) Lösung: Pro Username nur den ältesten Eintrag (MIN idPerson) ================================================================ */ USE ENIVERSSIAM; GO -- Deduplizierung: Pro Username nur MIN(idPerson) verwenden WITH dedup AS ( SELECT MIN(idPerson) AS idPerson FROM ENIVERS.dbo.tblPerson GROUP BY strPeWinUser ) INSERT INTO auth.[user] (person_id, username, is_active, created_at, updated_at) SELECT p.idPerson, p.strPeWinUser, ISNULL(p.bitPeAktiv, 1), p.dtCreated, ISNULL(p.dtUpdated, p.dtCreated) FROM ENIVERS.dbo.tblPerson p JOIN dedup d ON d.idPerson = p.idPerson WHERE NOT EXISTS ( SELECT 1 FROM auth.[user] u WHERE LOWER(u.username COLLATE Latin1_General_CI_AS) = LOWER(p.strPeWinUser COLLATE Latin1_General_CI_AS) ); PRINT 'auth.[user]: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' Zeilen eingefügt'; -- Ergebnis-Übersicht SELECT 'core.person' AS tabelle, COUNT(*) AS gesamt FROM core.person UNION ALL SELECT 'auth.[user]', COUNT(*) FROM auth.[user] UNION ALL SELECT 'auth.role', COUNT(*) FROM auth.role UNION ALL SELECT 'auth.user_role', COUNT(*) FROM auth.user_role; GO ==== Ergebnis Phase 2 ==== ^ Tabelle ^ Vor Phase 2 ^ Nach Phase 2 ^ Neu eingefügt ^ | ''core.person'' | 241 | 317 | 76 | | ''auth.[user]'' | 241 | 314 | 73 | | ''auth.role'' | 26 | 38 | 12 | | ''auth.user_role'' | 9.445 | 9.476 | 31 | > **Hinweis:** 73 statt 76 neue User — 3 Username-Duplikate zusammengeführt (MIN idPerson). > Personen ohne gültigen ''strPeWinUser'' (NULL) erhalten keinen ''auth.[user]''-Eintrag. ===== Quellsysteme und Migrationspfade ===== ==== Stack-Übersicht (AMED / WIS / TecDB) ==== ^ Stack ^ Pfad ^ Technologie ^ Status ^ | AMED .NET Framework | ''D:\Workspace\AMED\AmedNET\**'' | C# + ADO.NET TypedDataSet | Produktiv | | AMED .NET 8 (SETY) | ''D:\Workspace\AMED\net8\src\**'' | C# .NET 8 + WebAPI | In Entwicklung | | Delphi (alle Module) | ''D:\Workspace\AMED\Delphi\src\**'' | Delphi + TADOConnection | Legacy, produktiv | ==== Delphi-Anwendungen (alle Module) ==== ^ Modul ^ Pfad (relativ zu src\) ^ Zweck ^ Migrationsziel ^ | ''WIS'' | ''WIS\'' | Geräte, Prüfplanung, Prüfausführung (PROOF) | ENIVERSCAFM + ENIVERSPIMS | | ''SIAM'' | ''SIAM\'' | Legacy-Benutzerverwaltung | **abgelöst** (Phase 2 ✓) | | ''SCCM_PWD'' | ''SCCM_PWD\'' | Passwort-Verwaltung (legacy) | **abgelöst** (Phase 2 ✓) | | ''SCCM_WEB'' | ''SCCM_WEB\'' | Web-Frontend für SIAM | **abgelöst** → Gateway.Service | | ''ATEA'' | ''ATEA\'' | Anlage-/Apparate-Verwaltung | ENIVERSCAFM | | ''ETCM'' | ''ETCM\'' | Konfigurationsverwaltung | ENIVERSCAFM | | ''MOT'' | ''MOT\'' | Motor-Stammdaten, Lager, Verteilung | ENIVERSCAFM | | ''_DEVICES_'' | ''_DEVICES_\'' | Geräte-Stammdaten (Package-Bibliothek) | ENIVERSCAFM | | ''_MOTORS_'' | ''_MOTORS_\'' | Motor-Package-Bibliothek | ENIVERSCAFM | | ''REP'' | ''REP\'' | Schadensmeldungen, Berichte, Gantt-Ansicht | ENIVERSPIMS | | ''SB'' | ''SB\'' | Hauptmenü / Switchboard (Launcher) | **abgelöst** → WvdS.Shell | | ''ISAPI'' | ''ISAPI\'' | Web-Endpunkte für AMED-Daten (legacy) | **abgelöst** → Gateway.Service | | ''PFED'' | ''PFED\'' | Feldgeräte-Verteilung (Personal) | ENIVERSCAFM (prüfen) | | ''UTI'' | ''UTI\'' | DataSetConverter, gemeinsame Hilfsfunktionen | kein Migrationsziel | | ''ZOHO'' | ''ZOHO\'' | Zoho-CRM-Inventar-Integration | extern / out-of-scope | ===== Phase 3 — AMED Bestandsdaten (geplant) ===== Migrationsziel: **ENIVERSCAFM** (thematische Schemas ''asset'', ''org'', ''facility'') Geplante Quellen: * AMED-Datenbank (SQL Server): Branches, BETRIEB, tblGebaeude, APPMASTER, MOTORMASTER, GreatPlants * AMED .NET Framework DAL (Referenz für Schema-Struktur) * Delphi ATEA / ETCM / MOT (ergänzende Bestandsdaten) Schema-Reorganisation: von ''dbo'' in thematische Schemas (''asset'', ''org'', ''facility'', ''elec'') ===== Phase 4 — ENIVERSPIMS aufbauen (geplant) ===== Neues Schema, Quellen: * WIS-Delphi: DAL-Units ''PJ.AMED.DAL.Proof.Plan.Data'', ''PJ.AMED.DAL.Proof.Exec.Data'' * TecDB (SQL Server): Rohrleitungs- und Messtechnik-Daten (Struktur noch zu analysieren) * Delphi REP: Schadensmeldungen, Berichte, Messergebnisse ===== Phase 5 — ENIVERSASYS aufbauen (geplant) ===== Feature-Flags-System aus ENIVERSCAFM auslagern: * Aktuell: Feature-Flags im ''auth.*''-Namespace in ENIVERSCAFM * Ziel: Dediziertes ENIVERSASYS mit eigenem Feature-Flags-Schema * Shell und Gateway sprechen bereits gegen den ''/api/v1/features''-Endpoint — das Backend-Target (ENIVERSCAFM vs. ENIVERSASYS) ist intern transparent