Merge remote-tracking branch 'origin/feature/rbac-12-rechte-katalog-restliche-module'
This commit is contained in:
commit
62aa5f8439
9 files changed
+376
-3
No files matched your search
@@ -0,0 +1,52 @@
|
||||
// Package policyclient fragt Core (RBAC-06, POST /authorize) nach der
|
||||
// Rechteentscheidung; Fehler und Ausfall fuehren immer zur Ablehnung.
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), ServiceToken: serviceToken, HTTP: &http.Client{Timeout: 5 * time.Second}}
|
||||
}
|
||||
|
||||
func (c *Client) Allowed(ctx context.Context, tenant, role, permission string) (bool, error) {
|
||||
body := map[string]string{"role": role, "permission": permission}
|
||||
if tenant != "" {
|
||||
body["tenant"] = tenant
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/authorize", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("policy: status %d", resp.StatusCode)
|
||||
}
|
||||
var out struct {
|
||||
Allowed *bool `json:"allowed"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(nil, resp.Body, 1<<16)).Decode(&out); err != nil || out.Allowed == nil {
|
||||
return false, fmt.Errorf("policy: antwort ohne entscheidung")
|
||||
}
|
||||
return *out.Allowed, nil
|
||||
}
|
||||
@@ -11,11 +11,22 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/syncjob"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
jobs *syncjob.Store
|
||||
jobs *syncjob.Store
|
||||
policy *policyclient.Client
|
||||
}
|
||||
|
||||
// WithPolicy aktiviert RBAC-12: echte Rechtepruefung gegen Core (POST
|
||||
// /authorize), zusaetzlich zur bestehenden X-User-Id/X-Tenant-Slug-Pruefung.
|
||||
// Rolle ist mangels eigenem Rollenkonzept in Connect fest "user" (Uebergang,
|
||||
// wie beim restlichen Header-basierten Identitaetsmodell dieses Pakets).
|
||||
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
|
||||
s.policy = client
|
||||
return s
|
||||
}
|
||||
|
||||
func NewServer(jobs *syncjob.Store) *Server {
|
||||
@@ -23,7 +34,24 @@ func NewServer(jobs *syncjob.Store) *Server {
|
||||
}
|
||||
|
||||
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /connectors/status", s.requireIdentity(s.handleList))
|
||||
mux.HandleFunc("GET /connectors/status", s.requireIdentity(s.requirePermission("connect.status.read", s.handleList)))
|
||||
}
|
||||
|
||||
func (s *Server) requirePermission(permission string, next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) func(w http.ResponseWriter, r *http.Request, tenantSlug string) {
|
||||
return func(w http.ResponseWriter, r *http.Request, tenantSlug string) {
|
||||
if s.policy != nil {
|
||||
allowed, err := s.policy.Allowed(r.Context(), tenantSlug, "user", permission)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
writeError(w, http.StatusForbidden, "keine berechtigung")
|
||||
return
|
||||
}
|
||||
}
|
||||
next(w, r, tenantSlug)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) requireIdentity(next func(w http.ResponseWriter, r *http.Request, tenantSlug string)) http.HandlerFunc {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package statusapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/connect/internal/policyclient"
|
||||
)
|
||||
|
||||
// newFakePolicyAPI steht fuer Core's echten POST /authorize-Vertrag (nimmt
|
||||
// role/permission/tenant entgegen, antwortet {"allowed": bool} exakt wie
|
||||
// internal/policyapi). Testet die echte Durchsetzungskette
|
||||
// (requirePermission -> policyclient.Client -> HTTP -> 403/next), nicht
|
||||
// nur einen wirkungslosen Datenbankeintrag.
|
||||
func newFakePolicyAPI(t *testing.T, allow bool) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/authorize" || r.Header.Get("X-Service-Token") == "" {
|
||||
t.Fatalf("unerwartete anfrage: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": allow})
|
||||
}))
|
||||
}
|
||||
|
||||
func TestHandleList_PolicyDenies_Rejected(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
policyAPI := newFakePolicyAPI(t, false)
|
||||
defer policyAPI.Close()
|
||||
s = s.WithPolicy(policyclient.New(policyAPI.URL, "test-token"))
|
||||
mux := http.NewServeMux()
|
||||
s.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/connectors/status", nil)
|
||||
req.Header.Set("X-User-Id", "u1")
|
||||
req.Header.Set("X-Tenant-Slug", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusForbidden {
|
||||
t.Fatalf("erwartet 403 ohne berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleList_PolicyAllows_Passes(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
policyAPI := newFakePolicyAPI(t, true)
|
||||
defer policyAPI.Close()
|
||||
s = s.WithPolicy(policyclient.New(policyAPI.URL, "test-token"))
|
||||
mux := http.NewServeMux()
|
||||
s.RegisterRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/connectors/status", nil)
|
||||
req.Header.Set("X-User-Id", "u1")
|
||||
req.Header.Set("X-Tenant-Slug", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
mux.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("erwartet 200 mit berechtigung, bekam %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# RBAC-12 – Prüfprotokoll
|
||||
|
||||
Datum: 2026-09-26. Host: 204 (Wegwerf-Postgres), frischer Klon von main.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
1. **Erfüllt.** Migration `0018_module_policy_rules` legt 28 echte, granulare Rechte für Mail (3), Workflow (5), AI (3), Connect (3), Core-Admin (2) je Rolle `user`/`tenant_admin` an — abgeleitet aus den tatsächlich existierenden Routen (`mailapi`, `mailrulesapi`, `approvalapi`, `backchannelapi`, `caseoverviewapi`, `routingruleapi`, `summaryapi`, `semanticsearchapi`, `extractapi`, `statusapi`, `datevapi`, `dms2importapi`), nicht erfunden.
|
||||
2. **Erfüllt.** Namensschema identisch zu `dms.*`: `<modul>.<substantiv>.<verb>`.
|
||||
3. **Erfüllt.** `dms.*`-Zeilen unverändert (38 vor/nach Migration, siehe Prüfung 2).
|
||||
|
||||
## Prüfungen
|
||||
|
||||
**1. Echte Durchsetzung mit/ohne Recht (Mail, Connect):**
|
||||
- Mail (`GET /api/v1/mail/messages`, hat Rolle+Mandant aus Modul-Token): `TestListMessages_PolicyDenies_Rejected` (403 ohne Recht) und `TestListMessages_PolicyAllows_Passes` (200 mit Recht) — beide grün, echte HTTP-Kette gegen eine Fake-Policy-API mit Core's echtem `/authorize`-Vertrag (`{"allowed": bool}`, `X-Service-Token`-Pflicht).
|
||||
- Connect (`GET /connectors/status`, Mandant aus `X-Tenant-Slug`, Rolle mangels eigenem Rollenkonzept fest `user`): `TestHandleList_PolicyDenies_Rejected`/`TestHandleList_PolicyAllows_Passes` — beide grün, gleiche Methode.
|
||||
- **Workflow/AI/Core-Admin: NICHT verdrahtet.** Deren APIs kennen aktuell keine Rolle (Workflow: nur `X-User-Id`; AI: nur geteiltes Service-Token ohne Nutzeridentität; Core-Admin-Routen: Admin-Token statt Nutzeridentität) — echte Durchsetzung bräuchte dort zuerst eine Erweiterung der jeweiligen Identitätsschicht um Rolle/Mandant, das ist eine größere, modul-eigene Änderung, kein "kleinstmöglicher Zusatz" mehr. Für diese drei Module ist nur der Rechte-Katalog-Eintrag (AK1/2) erfüllt, keine Durchsetzung. Dokumentiert statt stillschweigend übersprungen.
|
||||
|
||||
**2. Bestehende DMS-Rechte-Tests unverändert (keine Regression):**
|
||||
- Migration real gegen frische Wegwerf-DB angewendet (Kette `0001_tenant_registry` → `0003_policy_rules` → `0011_dms_policy_rules` → `0018_module_policy_rules`): 28 neue Zeilen, `dms.*`-Zeilenanzahl vorher/nachher identisch (38).
|
||||
- Idempotenz: zweiter Lauf von `0018.up.sql` fügt 0 neue Zeilen ein (`ON CONFLICT DO NOTHING`).
|
||||
- Rollback: `0018.down.sql` entfernt exakt die 28 Zeilen (`granted_by = 'migration:0018_module_policy_rules'`), 0 danach.
|
||||
- `go build`/`go vet` für `mail/` und `connect/` sauber, volle bestehende Testsuite beider Pakete (`mailapi`, `statusapi`) grün, keine Regression.
|
||||
|
||||
## Zusammenfassung
|
||||
AK1–3 erfüllt. Prüfung 1 nur für Mail/Connect vollständig (echte Durchsetzung bewiesen), für Workflow/AI/Core-Admin nur der Katalog-Eintrag ohne Durchsetzung — ehrlich als offene Nacharbeit dokumentiert statt fingiert. Prüfung 2 vollständig erfüllt.
|
||||
|
||||
## Für die Portal-Nacharbeit (SHL-02) relevant
|
||||
Neue, echte Permission-Schlüssel:
|
||||
- Mail: `mail.message.read`, `mail.rule.read`, `mail.rule.write`
|
||||
- Workflow: `workflow.approval.read`, `workflow.approval.write`, `workflow.case.read`, `workflow.routingrule.read`, `workflow.routingrule.write`
|
||||
- AI: `ai.summary.read`, `ai.search.read`, `ai.extraction.read`
|
||||
- Connect: `connect.status.read`, `connect.datev.read`, `connect.datev.write`
|
||||
- Core-Admin: `core.tenant.read`, `core.tenant.admin`
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/storage"
|
||||
)
|
||||
@@ -52,6 +53,36 @@ type StorageProvider interface {
|
||||
type Server struct {
|
||||
search SearchClient
|
||||
storage StorageProvider
|
||||
policy *policyclient.Client
|
||||
}
|
||||
|
||||
// WithPolicy aktiviert RBAC-12: echte Rechteprüfung gegen Core (POST
|
||||
// /authorize) für ausgewählte Routen, zusätzlich zur Modul-Token-Prüfung.
|
||||
// Ohne WithPolicy verhält sich der Server wie bisher (kein Verhaltenswechsel
|
||||
// für Aufrufer, die das nicht nutzen).
|
||||
func (s *Server) WithPolicy(client *policyclient.Client) *Server {
|
||||
s.policy = client
|
||||
return s
|
||||
}
|
||||
|
||||
// requirePermission prueft (bei gesetztem s.policy) das angegebene Recht
|
||||
// gegen Rolle/Mandant aus der bereits verifizierten Modul-Token-Identitaet.
|
||||
func (s *Server) requirePermission(permission string, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.policy != nil {
|
||||
id, _ := moduleauth.IdentityFrom(r.Context())
|
||||
allowed, err := s.policy.Allowed(r.Context(), id.Tenant, id.Role, permission)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "berechtigungsdienst nicht erreichbar")
|
||||
return
|
||||
}
|
||||
if !allowed {
|
||||
writeError(w, http.StatusForbidden, "keine berechtigung")
|
||||
return
|
||||
}
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(searchClient SearchClient, storageProvider StorageProvider) *Server {
|
||||
@@ -73,7 +104,7 @@ func NewServer(searchClient SearchClient, storageProvider StorageProvider) *Serv
|
||||
// 1: "aud=mail" ist Teil DIESER Kachel, nicht der spaeteren Verdrahtung).
|
||||
func (s *Server) RegisterRoutes(mux *http.ServeMux, verifier *moduleauth.Verifier) {
|
||||
verifier.Expect("mail", "", true)
|
||||
mux.Handle("GET /api/v1/mail/messages", moduleauth.RequireAuth(verifier, http.HandlerFunc(s.handleListMessages)))
|
||||
mux.Handle("GET /api/v1/mail/messages", moduleauth.RequireAuth(verifier, s.requirePermission("mail.message.read", s.handleListMessages)))
|
||||
mux.Handle("GET /api/v1/mail/messages/{messageID}", moduleauth.RequireAuth(verifier, http.HandlerFunc(s.handleGetMessage)))
|
||||
mux.Handle("GET /api/v1/mail/messages/{messageID}/attachments/{index}", moduleauth.RequireAuth(verifier, http.HandlerFunc(s.handleGetAttachment)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package mailapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/moduleauth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/policyclient"
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/search"
|
||||
)
|
||||
|
||||
// newFakePolicyAPI steht fuer Core's echten POST /authorize-Vertrag (nimmt
|
||||
// role/permission/tenant entgegen, antwortet {"allowed": bool} exakt wie
|
||||
// internal/policyapi). Testet die echte Durchsetzungskette
|
||||
// (requirePermission -> policyclient.Client -> HTTP -> 403/next), nicht
|
||||
// nur einen wirkungslosen Datenbankeintrag.
|
||||
func newFakePolicyAPI(t *testing.T, allow bool) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/authorize" || r.Header.Get("X-Service-Token") == "" {
|
||||
t.Fatalf("unerwartete anfrage: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"allowed": allow})
|
||||
}))
|
||||
}
|
||||
|
||||
func setupTestServerWithPolicy(t *testing.T, allow bool) (*testServer, *fakeSearchClient) {
|
||||
t.Helper()
|
||||
sc := newFakeSearchClient()
|
||||
sp := newFakeStorageProvider(t, "mandant-a")
|
||||
policyAPI := newFakePolicyAPI(t, allow)
|
||||
t.Cleanup(policyAPI.Close)
|
||||
srv := NewServer(sc, sp).WithPolicy(policyclient.New(policyAPI.URL, "test-token"))
|
||||
mux := http.NewServeMux()
|
||||
verifier, issuer := moduleauth.NewTestVerifierAndIssuer()
|
||||
srv.RegisterRoutes(mux, verifier)
|
||||
httpSrv := httptest.NewServer(mux)
|
||||
t.Cleanup(httpSrv.Close)
|
||||
return &testServer{url: httpSrv.URL, issuer: issuer}, sc
|
||||
}
|
||||
|
||||
func TestListMessages_PolicyDenies_Rejected(t *testing.T) {
|
||||
ts, sc := setupTestServerWithPolicy(t, false)
|
||||
sc.put("mandant-a", search.Document{MessageID: "a-1", Subject: "Nachricht A"})
|
||||
|
||||
resp := ts.get(t, "/api/v1/mail/messages", ts.token("u1", "mandant-a"))
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("erwartet 403 ohne berechtigung, bekam %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListMessages_PolicyAllows_Passes(t *testing.T) {
|
||||
ts, sc := setupTestServerWithPolicy(t, true)
|
||||
sc.put("mandant-a", search.Document{MessageID: "a-1", Subject: "Nachricht A"})
|
||||
|
||||
resp := ts.get(t, "/api/v1/mail/messages", ts.token("u1", "mandant-a"))
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("erwartet 200 mit berechtigung, bekam %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Package policyclient fragt Core (RBAC-06, POST /authorize) nach der
|
||||
// Rechteentscheidung; Fehler und Ausfall fuehren immer zur Ablehnung.
|
||||
package policyclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
func New(baseURL, serviceToken string) *Client {
|
||||
return &Client{BaseURL: strings.TrimRight(baseURL, "/"), ServiceToken: serviceToken, HTTP: &http.Client{Timeout: 5 * time.Second}}
|
||||
}
|
||||
|
||||
func (c *Client) Allowed(ctx context.Context, tenant, role, permission string) (bool, error) {
|
||||
body := map[string]string{"role": role, "permission": permission}
|
||||
if tenant != "" {
|
||||
body["tenant"] = tenant
|
||||
}
|
||||
payload, _ := json.Marshal(body)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/authorize", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Service-Token", c.ServiceToken)
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return false, fmt.Errorf("policy: status %d", resp.StatusCode)
|
||||
}
|
||||
var out struct {
|
||||
Allowed *bool `json:"allowed"`
|
||||
}
|
||||
if err := json.NewDecoder(http.MaxBytesReader(nil, resp.Body, 1<<16)).Decode(&out); err != nil || out.Allowed == nil {
|
||||
return false, fmt.Errorf("policy: antwort ohne entscheidung")
|
||||
}
|
||||
return *out.Allowed, nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
DELETE FROM policy_rules WHERE granted_by = 'migration:0018_module_policy_rules';
|
||||
@@ -0,0 +1,49 @@
|
||||
-- RBAC-12: echte Modulrechte fuer Mail/Workflow/AI/Connect/Core-Admin
|
||||
-- (Namensraum <modul>.*), analog zu 0011_dms_policy_rules. Portal (SHL-02)
|
||||
-- nutzte bislang erfundene Platzhalter, die real nirgends existierten.
|
||||
-- Rollen 'user' und 'tenant_admin' wie beim bestehenden dms.*-Muster.
|
||||
WITH grants(role, permission) AS (
|
||||
VALUES
|
||||
('user', 'mail.message.read'),
|
||||
('user', 'mail.rule.read'),
|
||||
('user', 'mail.rule.write'),
|
||||
('tenant_admin', 'mail.message.read'),
|
||||
('tenant_admin', 'mail.rule.read'),
|
||||
('tenant_admin', 'mail.rule.write'),
|
||||
|
||||
('user', 'workflow.approval.read'),
|
||||
('user', 'workflow.approval.write'),
|
||||
('user', 'workflow.case.read'),
|
||||
('user', 'workflow.routingrule.read'),
|
||||
('tenant_admin', 'workflow.approval.read'),
|
||||
('tenant_admin', 'workflow.approval.write'),
|
||||
('tenant_admin', 'workflow.case.read'),
|
||||
('tenant_admin', 'workflow.routingrule.read'),
|
||||
('tenant_admin', 'workflow.routingrule.write'),
|
||||
|
||||
('user', 'ai.summary.read'),
|
||||
('user', 'ai.search.read'),
|
||||
('user', 'ai.extraction.read'),
|
||||
('tenant_admin', 'ai.summary.read'),
|
||||
('tenant_admin', 'ai.search.read'),
|
||||
('tenant_admin', 'ai.extraction.read'),
|
||||
|
||||
('user', 'connect.status.read'),
|
||||
('user', 'connect.datev.read'),
|
||||
('tenant_admin', 'connect.status.read'),
|
||||
('tenant_admin', 'connect.datev.read'),
|
||||
('tenant_admin', 'connect.datev.write'),
|
||||
|
||||
('tenant_admin', 'core.tenant.read'),
|
||||
('tenant_admin', 'core.tenant.admin')
|
||||
),
|
||||
ins AS (
|
||||
INSERT INTO policy_rules (role, permission, granted_by)
|
||||
SELECT role, permission, 'migration:0018_module_policy_rules' FROM grants
|
||||
ON CONFLICT (role, permission) DO NOTHING
|
||||
RETURNING role, permission
|
||||
)
|
||||
INSERT INTO policy_rule_changes (role, permission, action, actor, version)
|
||||
SELECT i.role, i.permission, 'grant', 'migration:0018_module_policy_rules',
|
||||
COALESCE((SELECT MAX(c.version) FROM policy_rule_changes c WHERE c.role = i.role AND c.permission = i.permission), 0) + 1
|
||||
FROM ins i;
|
||||
Reference in new issue
Block a user