package rbac import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( // ErrRoleNotAssignableInTenantScope wird geliefert, wenn versucht wird, // eine mandantenuebergreifende Rolle (superadmin) ueber den tenant- // gescopten Store zu vergeben — die erlaubte Matrix laesst hier nur // user/tenant_admin zu (Akzeptanzkriterium 1 / Pruefung 1). ErrRoleNotAssignableInTenantScope = errors.New("rbac: rolle ist in diesem geltungsbereich nicht zuweisbar") ErrNotFound = errors.New("rbac: keine rollenzuweisung gefunden") ) // assignableRoles ist die erlaubte Matrix fuer Store (tenant-gescoped). var assignableRoles = map[Role]bool{ RoleUser: true, RoleTenantAdmin: true, } type Assignment struct { UserID string Role Role GrantedBy string } // Store verwaltet Rollenzuweisungen innerhalb GENAU EINER Tenant-Datenbank — // analog zu internal/user.TenantUserStore (Modell C: der Pool bestimmt den // Tenant, keine tenant_id-Spalte noetig). type Store struct { pool *pgxpool.Pool } func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // Assign vergibt eine Rolle an einen Benutzer. grantedBy identifiziert den // Akteur, der die Zuweisung vorgenommen hat (Akzeptanzkriterium 3). Jede // Zuweisung wird zusaetzlich in role_assignment_history festgehalten, auch // wenn sie eine vorherige Rolle ersetzt. func (s *Store) Assign(ctx context.Context, userID string, role Role, grantedBy string) (Assignment, error) { if !assignableRoles[role] { return Assignment{}, ErrRoleNotAssignableInTenantScope } if grantedBy == "" { return Assignment{}, errors.New("rbac: grantedBy darf nicht leer sein") } tx, err := s.pool.Begin(ctx) if err != nil { return Assignment{}, fmt.Errorf("transaktion starten: %w", err) } defer func() { _ = tx.Rollback(ctx) }() if _, err := tx.Exec(ctx, ` INSERT INTO role_assignments (user_id, role, granted_by, granted_at) VALUES ($1, $2, $3, now()) ON CONFLICT (user_id) DO UPDATE SET role = $2, granted_by = $3, granted_at = now() `, userID, string(role), grantedBy); err != nil { return Assignment{}, fmt.Errorf("rolle zuweisen: %w", err) } if _, err := tx.Exec(ctx, ` INSERT INTO role_assignment_history (user_id, role, granted_by, granted_at) VALUES ($1, $2, $3, now()) `, userID, string(role), grantedBy); err != nil { return Assignment{}, fmt.Errorf("historie schreiben: %w", err) } if err := tx.Commit(ctx); err != nil { return Assignment{}, fmt.Errorf("transaktion committen: %w", err) } return Assignment{UserID: userID, Role: role, GrantedBy: grantedBy}, nil } func (s *Store) Get(ctx context.Context, userID string) (Assignment, error) { var a Assignment var role string a.UserID = userID if err := s.pool.QueryRow(ctx, ` SELECT role, granted_by FROM role_assignments WHERE user_id = $1 `, userID).Scan(&role, &a.GrantedBy); err != nil { if errors.Is(err, pgx.ErrNoRows) { return Assignment{}, ErrNotFound } return Assignment{}, fmt.Errorf("rollenzuweisung lesen: %w", err) } a.Role = Role(role) return a, nil } // History liefert jede Rollenzuweisung eines Benutzers in chronologischer // Reihenfolge — die Grundlage fuer "wer hat wann welche Rolle vergeben" // (Akzeptanzkriterium 3). func (s *Store) History(ctx context.Context, userID string) ([]Assignment, error) { rows, err := s.pool.Query(ctx, ` SELECT role, granted_by FROM role_assignment_history WHERE user_id = $1 ORDER BY granted_at `, userID) if err != nil { return nil, fmt.Errorf("historie abfragen: %w", err) } defer rows.Close() var out []Assignment for rows.Next() { var role string a := Assignment{UserID: userID} if err := rows.Scan(&role, &a.GrantedBy); err != nil { return nil, fmt.Errorf("historieneintrag lesen: %w", err) } a.Role = Role(role) out = append(out, a) } return out, rows.Err() }