Files
nexarch/internal/migration/role/migrate.go
T
patrickandClaude Sonnet 5.5 5b8c20777d refactor: Pruefgate-Pakete nach Funktion benannt, migration* gruppiert
- dms/ai/workflow: qa0x-Pakete nach internal/gates/<thema> verschoben, Paketname = Thema
- internal/migration{acl,audit,...} -> internal/migration/{acl,audit,...}
- Importpfade, Gate-Skripte und zwei relative Migrationspfade angepasst
- keine Logikaenderung; mail-Gates bewusst unveraendert (pfadabhaengig)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 12:44:21 +01:00

130 lines
3.5 KiB
Go

package migrationrole
import (
"context"
"fmt"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
"gitea.perlbach24.de/scripte/nexarch/internal/tenant"
)
type PoolOpener func(ctx context.Context, dsn string) (*pgxpool.Pool, error)
func defaultPoolOpener(ctx context.Context, dsn string) (*pgxpool.Pool, error) {
return pgxpool.New(ctx, dsn)
}
// Migrator weist jedem freigegebenen role_migration_map-Eintrag die ueber
// CoreRole berechnete Core-Rolle zu, in der zugehoerigen Tenant-Datenbank.
type Migrator struct {
mapStore *MapStore
registry *tenant.Registry
openPool PoolOpener
tenantPools map[string]*pgxpool.Pool
}
func NewMigrator(mapStore *MapStore, registry *tenant.Registry) *Migrator {
return &Migrator{
mapStore: mapStore,
registry: registry,
openPool: defaultPoolOpener,
tenantPools: map[string]*pgxpool.Pool{},
}
}
func (m *Migrator) WithPoolOpener(opener PoolOpener) *Migrator {
m.openPool = opener
return m
}
// Assignment fasst eine einzelne, ausgefuehrte Rollenzuweisung zusammen.
type Assignment struct {
UserMigrationMapID int64
NexarchUserID string
SourceSystem string
SourceRole string
NexarchRole rbac.Role
}
// Result fasst den Ausgang eines Migrationslaufs zusammen.
type Result struct {
Assigned []Assignment
}
// MigrateApproved liest die freigegebene Rollen-Map und weist jedem Eintrag
// die gemappte Core-Rolle in der passenden Tenant-Datenbank zu
// (Akzeptanzkriterien 1/2). Ein unbekannter source_role-Wert bricht den
// gesamten Lauf mit ErrUnmappedRole ab, statt den Benutzer ohne Rolle oder
// mit einer geratenen Rolle zu migrieren (Akzeptanzkriterium 3).
func (m *Migrator) MigrateApproved(ctx context.Context) (Result, error) {
entries, err := m.mapStore.Approved(ctx)
if err != nil {
return Result{}, err
}
var res Result
for _, e := range entries {
role, err := CoreRole(e.SourceSystem, strings.ToLower(strings.TrimSpace(e.SourceRole)))
if err != nil {
return res, fmt.Errorf("role-map-eintrag %d (benutzer %s): %w", e.ID, e.NexarchUserID, err)
}
pool, err := m.tenantPool(ctx, e.NexarchTenantID)
if err != nil {
return res, fmt.Errorf("tenant-verbindung fuer role-map-eintrag %d: %w", e.ID, err)
}
grantedBy := e.GrantedBy
if grantedBy == "" && e.ApprovedBy != nil {
grantedBy = *e.ApprovedBy
}
if grantedBy == "" {
grantedBy = "cor-03-migration"
}
store := rbac.NewStore(pool)
if _, err := store.Assign(ctx, e.NexarchUserID, role, grantedBy); err != nil {
return res, fmt.Errorf("rolle fuer benutzer %s (role-map-eintrag %d) zuweisen: %w", e.NexarchUserID, e.ID, err)
}
res.Assigned = append(res.Assigned, Assignment{
UserMigrationMapID: e.UserMigrationMapID,
NexarchUserID: e.NexarchUserID,
SourceSystem: e.SourceSystem,
SourceRole: e.SourceRole,
NexarchRole: role,
})
}
return res, nil
}
func (m *Migrator) tenantPool(ctx context.Context, tenantID string) (*pgxpool.Pool, error) {
if pool, ok := m.tenantPools[tenantID]; ok {
return pool, nil
}
t, err := m.registry.GetByID(ctx, tenantID)
if err != nil {
return nil, fmt.Errorf("tenant %q laden: %w", tenantID, err)
}
pool, err := m.openPool(ctx, t.DBDSN)
if err != nil {
return nil, fmt.Errorf("tenant %q verbinden: %w", tenantID, err)
}
m.tenantPools[tenantID] = pool
return pool, nil
}
// Close schliesst alle im Lauf geoeffneten Tenant-Pools.
func (m *Migrator) Close() {
for _, pool := range m.tenantPools {
pool.Close()
}
m.tenantPools = map[string]*pgxpool.Pool{}
}