internal/rbac: drei Grundrollen (superadmin, tenant_admin, user) mit Hierarchie ueber eine einfache Eltern-Map (tenant_admin erbt von user, superadmin erbt von tenant_admin) — EffectivePermissions loest die volle vererbte Rechtemenge auf (Akzeptanzkriterium 2). Policy-Modell bewusst als reine Go-Datenstruktur getrennt von der Durchsetzung (RBAC-02), nach Casbin-Prinzip. Store verwaltet Rollenzuweisungen innerhalb EINER Tenant-Datenbank (Modell C, analog internal/user.TenantUserStore) — nur 'user' und 'tenant_admin' sind hier zuweisbar (assignableRoles-Matrix). Ein Zuweisungsversuch fuer 'superadmin' wird abgewiesen, da diese Rolle mandantenuebergreifend ist und bereits durch die Existenz eines Kontos in IAM-01s SuperadminStore repraesentiert wird — keine doppelte Modellierung. Jede Zuweisung schreibt zusaetzlich einen Historieneintrag (role_assignment_history) mit grantedBy/grantedAt, atomar in derselben Transaktion (Akzeptanzkriterium 3). Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Zuweisung ausserhalb der erlaubten Matrix abgewiesen — TestStore_RejectsSuperadminOutsideAllowedMatrix und TestStore_RejectsUnknownRole: beide ErrRoleNotAssignableInTenantScope, kein Datensatz hinterlassen. PASS. 2. Rollenhierarchie liefert erwartete effektive Rechtemenge — TestEffectivePermissions_Inheritance: tenant_admin hat geerbte user-Rechte + eigene, aber nicht platform.manage_tenants; superadmin hat die volle Kette. PASS. 3. Datenmodell von zweiter Person gegen Dokumentation geprueft — NICHT durchgefuehrt (keine zweite Person in dieser Session verfuegbar). Offen. Zusaetzlich automatisiert getestet (Akzeptanzkriterium 3): TestStore_HistoryTracksWhoAndWhen — zwei aufeinanderfolgende Zuweisungen, Historie liefert beide mit korrektem grantedBy in chronologischer Reihenfolge. PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
125 lines
3.8 KiB
Go
125 lines
3.8 KiB
Go
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()
|
|
}
|