DMS: Token-Rolle tenant_admin gilt im Guard als Admin (Core-Token-Modus); Smoke: Admin als Lebenszyklus-Akteur, Rollenpruefung R5
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
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
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e1a6a9b276
commit
403ceb6b61
8 files changed
+61
-5
No files matched your search
+1
-1
@@ -133,7 +133,7 @@ func guardMux(next http.Handler, dsn, prefix, adminIDs string) (http.Handler, er
|
||||
admins[id] = true
|
||||
}
|
||||
}
|
||||
return authz.Guard(next, &authz.PoolChecker{Pool: pool, Admins: admins}, prefix), nil
|
||||
return authz.Guard(next, &authz.PoolChecker{Pool: pool, Admins: admins, AdminRole: "tenant_admin"}, prefix), nil
|
||||
}
|
||||
|
||||
// recoverMiddleware faengt Panics in Handlern ab und meldet sie zentral
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth"
|
||||
)
|
||||
|
||||
func TestIsAdmin_RoleFromVerifiedTokenCountsAsAdmin(t *testing.T) {
|
||||
c := &PoolChecker{Admins: map[string]bool{"listed": true}, AdminRole: "tenant_admin"}
|
||||
ctx := func(id moduleauth.Identity) context.Context { return moduleauth.WithIdentity(context.Background(), id) }
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
caller string
|
||||
want bool
|
||||
}{
|
||||
{"in der Liste", context.Background(), "listed", true},
|
||||
{"Rolle tenant_admin im Token", ctx(moduleauth.Identity{UserID: "u1", Role: "tenant_admin"}), "u1", true},
|
||||
{"Rolle user im Token", ctx(moduleauth.Identity{UserID: "u1", Role: "user"}), "u1", false},
|
||||
{"Rolle gehoert zu anderem Nutzer", ctx(moduleauth.Identity{UserID: "u2", Role: "tenant_admin"}), "u1", false},
|
||||
{"ohne Token-Identitaet (Interimsakteur)", context.Background(), "u1", false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got, err := c.IsAdmin(tc.ctx, tc.caller)
|
||||
if err != nil || got != tc.want {
|
||||
t.Errorf("%s: got %v, %v; want %v", tc.name, got, err, tc.want)
|
||||
}
|
||||
}
|
||||
noRole := &PoolChecker{}
|
||||
if got, _ := noRole.IsAdmin(ctx(moduleauth.Identity{UserID: "u1", Role: "tenant_admin"}), "u1"); got {
|
||||
t.Error("ohne AdminRole darf die Token-Rolle nicht wirken")
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/dms/internal/moduleauth"
|
||||
)
|
||||
|
||||
type Checker interface {
|
||||
@@ -27,6 +29,8 @@ type Checker interface {
|
||||
type PoolChecker struct {
|
||||
Pool *pgxpool.Pool
|
||||
Admins map[string]bool
|
||||
// AdminRole: Rolle im verifizierten Modul-Token, die wie ein Eintrag in Admins gilt.
|
||||
AdminRole string
|
||||
}
|
||||
|
||||
func (c *PoolChecker) owner(ctx context.Context, query, caller, id string) (bool, error) {
|
||||
@@ -62,8 +66,15 @@ func (c *PoolChecker) OwnsUploadSession(ctx context.Context, caller, id string)
|
||||
return c.owner(ctx, `SELECT created_by::text FROM upload_sessions WHERE id = $1`, caller, id)
|
||||
}
|
||||
|
||||
func (c *PoolChecker) IsAdmin(_ context.Context, caller string) (bool, error) {
|
||||
return c.Admins[caller], nil
|
||||
func (c *PoolChecker) IsAdmin(ctx context.Context, caller string) (bool, error) {
|
||||
if c.Admins[caller] {
|
||||
return true, nil
|
||||
}
|
||||
if c.AdminRole != "" {
|
||||
id, ok := moduleauth.IdentityFrom(ctx)
|
||||
return ok && id.UserID == caller && id.Role == c.AdminRole, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func validUUID(s string) bool {
|
||||
|
||||
@@ -17,6 +17,10 @@ type Identity struct {
|
||||
|
||||
type identityKey struct{}
|
||||
|
||||
func WithIdentity(ctx context.Context, id Identity) context.Context {
|
||||
return context.WithValue(ctx, identityKey{}, id)
|
||||
}
|
||||
|
||||
func IdentityFrom(ctx context.Context) (Identity, bool) {
|
||||
id, ok := ctx.Value(identityKey{}).(Identity)
|
||||
return id, ok
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -48,8 +48,8 @@ if [ "${GESAMT_TEARDOWN_ONLY:-}" = 1 ]; then teardown; echo "zurueckgebaut"; exi
|
||||
set -a; source /etc/nexarch-dev/core-dev.env; source /etc/nexarch-dev/tenant-dev.env; set +a
|
||||
case "$TEST_ADMIN_DSN$TEST_TENANT_DSN" in *:5433/*) ;; *) echo "DSN zeigt nicht auf den Wegwerf-Cluster (5433) - Abbruch"; exit 2 ;; esac
|
||||
|
||||
mkdir -p "$BASE"/{bin,env,data/storage,data/staging,log,corpus,gocache,gopath,results}
|
||||
snapshot /tmp/gesamt-smoke-vorher.txt
|
||||
mkdir -p "$BASE"/{bin,env,data/storage,data/staging,log,corpus,gocache,gopath,results}
|
||||
finish() {
|
||||
[ "${GESAMT_KEEP:-}" = 1 ] && return
|
||||
cp "$RES"/*.txt /tmp/ 2>/dev/null
|
||||
@@ -190,7 +190,7 @@ chown -R "$SVC_USER":"$SVC_USER" "$BASE/corpus"
|
||||
export FDN19_API="http://127.0.0.1:8600/api/dms/v1" FDN19_ROOT="http://127.0.0.1:8600" FDN19_STUB="http://$STUB"
|
||||
export FDN19_DSN="$TEST_TENANT_DSN" FDN19_TENANT=$TENANT FDN19_CORPUS="$BASE/corpus" FDN19_TERM=Zinnoberrot FDN19_TOOL="$BASE/bin/smoke-tool"
|
||||
export FDN19_MIGRATIONS="$ROOT/dms/migrations/tenant" NEXARCH_DMS_APP_TENANT_DSN="$TEST_TENANT_DSN"
|
||||
export FDN19_CORE_ACCOUNT=http://127.0.0.1:8099 FDN19_CORE_ACCOUNT_B=http://127.0.0.1:8098 FDN19_CORE_PASSWORD="$password" FDN19_CORE_USERS="alice@e2e.test,carol@e2e.test"
|
||||
export FDN19_CORE_ACCOUNT=http://127.0.0.1:8099 FDN19_CORE_ACCOUNT_B=http://127.0.0.1:8098 FDN19_CORE_PASSWORD="$password" FDN19_CORE_USERS="bob@e2e.test,carol@e2e.test"
|
||||
cd scripts || exit 2
|
||||
|
||||
measure() {
|
||||
|
||||
@@ -38,6 +38,12 @@ def roles():
|
||||
f.check("R2 %s: user 403, tenant_admin erlaubt" % path, su == 403 and sa not in (401, 403), "user %s, admin %s" % (su, sa))
|
||||
st_u = status("/trash/purge", user_tok, "POST")
|
||||
f.check("R3 POST /trash/purge: user 403", st_u == 403, "status %s" % st_u)
|
||||
uid = f.psql("SELECT id FROM users WHERE email = '%s'" % ALICE).split("\n")[0]
|
||||
f.CORE_EMAIL[uid] = ALICE
|
||||
u = f.Actor("core", uid)
|
||||
doc = u.upload("user-rolle.txt", "text/plain", ("Rollenbeleg %d" % time.time_ns()).encode())
|
||||
st, _ = u.call("GET", "/documents/%s" % doc)
|
||||
f.check("R5 user (nur Standardrolle) laedt hoch und liest eigenes Dokument", st == 200, "status %s" % st)
|
||||
st_none = f.http("GET", f.API + "/tags")[0]
|
||||
f.check("R4 ohne Token 401", st_none == 401, "status %s" % st_none)
|
||||
|
||||
|
||||
Reference in new issue
Block a user