CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
POST /authorize nimmt optional den Mandanten entgegen und wertet modulgebundene Rechte ueber AuthorizeForTenant aus (fail closed). Mandant ist Pflicht fuer Rechte mit Modul-Bindung, das Service-Token laesst sich auf Mandanten einschraenken. Migration 0012 bindet die dms.*-Rechte an das Flag dms. Der DMS-policyclient sendet den Mandanten mit und trennt den Cache je Mandant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
113 lines
4.2 KiB
Go
113 lines
4.2 KiB
Go
package policy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/flag"
|
|
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
|
|
)
|
|
|
|
// ModuleScope verknuepft eine Policy-Regel mit einem Feature-Flag: existiert
|
|
// ein ModuleScope fuer (role, permission), gilt die Regel nur zusaetzlich zur
|
|
// Grundberechtigung, wenn FlagKey fuer den jeweiligen Tenant aktiv ist
|
|
// (Akzeptanzkriterium 1: Rechte folgen der Lizenz).
|
|
type ModuleScope struct {
|
|
Role rbac.Role
|
|
Permission rbac.Permission
|
|
Module string
|
|
FlagKey string
|
|
}
|
|
|
|
// SetModuleScope verknuepft eine bestehende Policy-Regel mit einem Modul/
|
|
// Feature-Flag. Die Regel selbst (Store.Grant) muss unabhaengig davon
|
|
// existieren — ModuleScope schraenkt sie nur zusaetzlich ein.
|
|
func (s *Store) SetModuleScope(ctx context.Context, role rbac.Role, perm rbac.Permission, module, flagKey string) error {
|
|
_, err := s.pool.Exec(ctx, `
|
|
INSERT INTO policy_module_scopes (role, permission, module, flag_key)
|
|
VALUES ($1, $2, $3, $4)
|
|
ON CONFLICT (role, permission) DO UPDATE SET module = $3, flag_key = $4
|
|
`, string(role), string(perm), module, flagKey)
|
|
if err != nil {
|
|
return fmt.Errorf("modul-scope setzen: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) GetModuleScope(ctx context.Context, role rbac.Role, perm rbac.Permission) (ModuleScope, bool, error) {
|
|
var ms ModuleScope
|
|
ms.Role, ms.Permission = role, perm
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT module, flag_key FROM policy_module_scopes WHERE role = $1 AND permission = $2
|
|
`, string(role), string(perm)).Scan(&ms.Module, &ms.FlagKey)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return ModuleScope{}, false, nil
|
|
}
|
|
return ModuleScope{}, false, fmt.Errorf("modul-scope lesen: %w", err)
|
|
}
|
|
return ms, true, nil
|
|
}
|
|
|
|
// AuthorizeForTenant ist dieselbe zentrale Entscheidungsfunktion wie
|
|
// Authorize (Akzeptanzkriterium 3: keine zweite Enforcement-Schicht),
|
|
// erweitert um die Modul-Scoping-Pruefung: eine sonst passende Regel greift
|
|
// NICHT, wenn das zugehoerige Modul fuer den Tenant nicht aktiviert ist
|
|
// (Akzeptanzkriterium 1). Feature-Flag-Aenderungen wirken ohne Neustart
|
|
// (Akzeptanzkriterium 2), da flag.Service dieselbe TTL-Cache-Instanz der
|
|
// aufrufenden Core-Instanz nutzt.
|
|
func (e *Enforcer) AuthorizeForTenant(ctx context.Context, flags *flag.Service, tenantSlug string, role rbac.Role, perm rbac.Permission) error {
|
|
if err := e.Authorize(ctx, role, perm); err != nil {
|
|
return err
|
|
}
|
|
|
|
scope, found, err := e.store.GetModuleScope(ctx, role, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
return nil // keine Modul-Bindung fuer diese Regel — Grundberechtigung reicht.
|
|
}
|
|
|
|
if flags == nil || !flags.IsEnabled(ctx, tenantSlug, scope.FlagKey) {
|
|
return fmt.Errorf("%w: modul %q ist fuer diesen mandanten nicht aktiviert", ErrModuleInactive, scope.Module)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AuthorizeRequest ist die Entscheidung fuer den modulübergreifenden HTTP-
|
|
// Endpunkt (RBAC-10). Mit Mandant gilt AuthorizeForTenant. Ohne Mandant gilt
|
|
// nur die Grundberechtigung, aber nie fuer eine Regel mit Modul-Bindung: deren
|
|
// Aktivierung laesst sich ohne Mandant nicht pruefen, also wird verweigert
|
|
// (fail closed).
|
|
func (e *Enforcer) AuthorizeRequest(ctx context.Context, flags *flag.Service, tenantSlug string, role rbac.Role, perm rbac.Permission) error {
|
|
if tenantSlug != "" {
|
|
return e.AuthorizeForTenant(ctx, flags, tenantSlug, role, perm)
|
|
}
|
|
if err := e.Authorize(ctx, role, perm); err != nil {
|
|
return err
|
|
}
|
|
_, found, err := e.store.GetModuleScope(ctx, role, perm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if found {
|
|
return fmt.Errorf("%w: recht %q ist modulgebunden, mandant erforderlich", ErrTenantRequired, perm)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GuardModuleScoped ist Guard mit zusaetzlicher Modul-Scoping-Pruefung —
|
|
// dieselbe zentrale Enforcement-Funktion, kein paralleler Mechanismus
|
|
// (Akzeptanzkriterium 3).
|
|
func GuardModuleScoped[T any](ctx context.Context, e *Enforcer, flags *flag.Service, tenantSlug string, role rbac.Role, perm rbac.Permission, query func(ctx context.Context) (T, error)) (T, error) {
|
|
var zero T
|
|
if err := e.AuthorizeForTenant(ctx, flags, tenantSlug, role, perm); err != nil {
|
|
return zero, err
|
|
}
|
|
return query(ctx)
|
|
}
|