package rbac import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5/pgxpool" ) var ErrGroupNotFound = errors.New("rbac: gruppe nicht gefunden") type Group struct { ID string Name string Role Role // leer, wenn der Gruppe noch keine Rolle zugewiesen wurde } // GroupStore verwaltet Gruppen/Abteilungen innerhalb GENAU EINER Tenant- // Datenbank (Modell C, wie Store fuer direkte Rollenzuweisungen) — // Akzeptanzkriterium 3 (tenant-isoliert) ist damit strukturell erfuellt. type GroupStore struct { pool *pgxpool.Pool } func NewGroupStore(pool *pgxpool.Pool) *GroupStore { return &GroupStore{pool: pool} } // CreateGroup legt eine neue, noch rollenlose Gruppe an (Akzeptanzkriterium 1). func (s *GroupStore) CreateGroup(ctx context.Context, name string) (Group, error) { if name == "" { return Group{}, errors.New("rbac: gruppenname darf nicht leer sein") } var g Group g.Name = name if err := s.pool.QueryRow(ctx, ` INSERT INTO groups (name) VALUES ($1) RETURNING id `, name).Scan(&g.ID); err != nil { return Group{}, fmt.Errorf("gruppe anlegen: %w", err) } return g, nil } // SetGroupRole weist der Gruppe eine Rolle zu (Akzeptanzkriterium 2) — nur // 'user'/'tenant_admin' sind auf Gruppenebene zuweisbar, dieselbe erlaubte // Matrix wie in Store.Assign (superadmin bleibt mandantenuebergreifend und // ausserhalb jeder Gruppenlogik). func (s *GroupStore) SetGroupRole(ctx context.Context, groupID string, role Role) error { if !assignableRoles[role] { return ErrRoleNotAssignableInTenantScope } tag, err := s.pool.Exec(ctx, `UPDATE groups SET role = $2 WHERE id = $1`, groupID, string(role)) if err != nil { return fmt.Errorf("gruppenrolle setzen: %w", err) } if tag.RowsAffected() == 0 { return ErrGroupNotFound } return nil } // AddMember nimmt einen Benutzer in die Gruppe auf. func (s *GroupStore) AddMember(ctx context.Context, groupID, userID string) error { _, err := s.pool.Exec(ctx, ` INSERT INTO group_members (group_id, user_id) VALUES ($1, $2) ON CONFLICT (group_id, user_id) DO NOTHING `, groupID, userID) if err != nil { return fmt.Errorf("mitglied hinzufuegen: %w", err) } return nil } // RemoveMember entfernt einen Benutzer aus der Gruppe — die gruppenbasierten // Rechte entfallen fuer ihn sofort (Akzeptanzkriterium 3 / Pruefung 2), da // EffectivePermissionsForUser bei jedem Aufruf neu berechnet wird, nicht // zwischengespeichert. func (s *GroupStore) RemoveMember(ctx context.Context, groupID, userID string) error { _, err := s.pool.Exec(ctx, `DELETE FROM group_members WHERE group_id = $1 AND user_id = $2`, groupID, userID) if err != nil { return fmt.Errorf("mitglied entfernen: %w", err) } return nil } // DeleteGroup loescht die Gruppe — ON DELETE CASCADE entfernt die // Mitgliedschaften mit, Benutzerkonten selbst bleiben unberuehrt // (Akzeptanzkriterium 3). func (s *GroupStore) DeleteGroup(ctx context.Context, groupID string) error { _, err := s.pool.Exec(ctx, `DELETE FROM groups WHERE id = $1`, groupID) if err != nil { return fmt.Errorf("gruppe loeschen: %w", err) } return nil } // RolesForUser liefert alle Rollen, die einem Benutzer ueber seine // Gruppenmitgliedschaften zufallen (Akzeptanzkriterium 2: wirkt auf alle // Mitglieder). Eine Gruppe ohne zugewiesene Rolle traegt nichts bei. func (s *GroupStore) RolesForUser(ctx context.Context, userID string) ([]Role, error) { rows, err := s.pool.Query(ctx, ` SELECT g.role FROM groups g JOIN group_members gm ON gm.group_id = g.id WHERE gm.user_id = $1 AND g.role IS NOT NULL `, userID) if err != nil { return nil, fmt.Errorf("gruppenrollen abfragen: %w", err) } defer rows.Close() var out []Role for rows.Next() { var r string if err := rows.Scan(&r); err != nil { return nil, fmt.Errorf("gruppenrolle lesen: %w", err) } out = append(out, Role(r)) } return out, rows.Err() } // EffectivePermissionsForUser vereinigt die Rechte aus der direkten // Rollenzuweisung (Store, RBAC-01) mit allen Rechten aus Gruppenrollen — // jede einzelne Rolle wird ueber die bestehende Hierarchie (EffectivePermissions) // aufgeloest, das Ergebnis dedupliziert. func (s *GroupStore) EffectivePermissionsForUser(ctx context.Context, directStore *Store, userID string) ([]Permission, error) { seen := make(map[Permission]bool) var out []Permission add := func(role Role) { for _, p := range EffectivePermissions(role) { if !seen[p] { seen[p] = true out = append(out, p) } } } if directStore != nil { assignment, err := directStore.Get(ctx, userID) if err != nil && !errors.Is(err, ErrNotFound) { return nil, err } if err == nil { add(assignment.Role) } } groupRoles, err := s.RolesForUser(ctx, userID) if err != nil { return nil, err } for _, r := range groupRoles { add(r) } return out, nil }