201 lines
5.9 KiB
Go
201 lines
5.9 KiB
Go
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
|
|
}
|
|
|
|
// ListGroups liefert alle Gruppen des Mandanten (Akzeptanzkriterium 1/2,
|
|
// Grundlage fuer die Administrationsoberflaeche).
|
|
func (s *GroupStore) ListGroups(ctx context.Context) ([]Group, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT id, name, COALESCE(role, '') FROM groups ORDER BY name`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gruppen auflisten: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []Group
|
|
for rows.Next() {
|
|
var g Group
|
|
var role string
|
|
if err := rows.Scan(&g.ID, &g.Name, &role); err != nil {
|
|
return nil, fmt.Errorf("gruppe lesen: %w", err)
|
|
}
|
|
g.Role = Role(role)
|
|
out = append(out, g)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// Members liefert die User-IDs der Mitglieder einer Gruppe.
|
|
func (s *GroupStore) Members(ctx context.Context, groupID string) ([]string, error) {
|
|
rows, err := s.pool.Query(ctx, `SELECT user_id FROM group_members WHERE group_id = $1`, groupID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("mitglieder abfragen: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []string
|
|
for rows.Next() {
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, fmt.Errorf("mitglied lesen: %w", err)
|
|
}
|
|
out = append(out, id)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// 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
|
|
}
|