diff --git a/dms/cmd/app/main.go b/dms/cmd/app/main.go index 32802594..42cf69e6 100644 --- a/dms/cmd/app/main.go +++ b/dms/cmd/app/main.go @@ -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 diff --git a/dms/internal/authz/admin_role_test.go b/dms/internal/authz/admin_role_test.go new file mode 100644 index 00000000..322abb9d --- /dev/null +++ b/dms/internal/authz/admin_role_test.go @@ -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") + } +} diff --git a/dms/internal/authz/authz.go b/dms/internal/authz/authz.go index e9b05757..0ec4cf6c 100644 --- a/dms/internal/authz/authz.go +++ b/dms/internal/authz/authz.go @@ -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 { diff --git a/dms/internal/moduleauth/middleware.go b/dms/internal/moduleauth/middleware.go index 685a097f..7db317f0 100644 --- a/dms/internal/moduleauth/middleware.go +++ b/dms/internal/moduleauth/middleware.go @@ -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 diff --git a/scripts/__pycache__/fdn19_smoke.cpython-313.pyc b/scripts/__pycache__/fdn19_smoke.cpython-313.pyc new file mode 100644 index 00000000..1d3e77d1 Binary files /dev/null and b/scripts/__pycache__/fdn19_smoke.cpython-313.pyc differ diff --git a/scripts/__pycache__/gesamt_token_smoke.cpython-313.pyc b/scripts/__pycache__/gesamt_token_smoke.cpython-313.pyc new file mode 100644 index 00000000..c0a123c9 Binary files /dev/null and b/scripts/__pycache__/gesamt_token_smoke.cpython-313.pyc differ diff --git a/scripts/gesamt-token-smoke.sh b/scripts/gesamt-token-smoke.sh index 04ab0713..622f0d6b 100755 --- a/scripts/gesamt-token-smoke.sh +++ b/scripts/gesamt-token-smoke.sh @@ -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() { diff --git a/scripts/gesamt_token_smoke.py b/scripts/gesamt_token_smoke.py index 2ac9deba..81ebc1a1 100755 --- a/scripts/gesamt_token_smoke.py +++ b/scripts/gesamt_token_smoke.py @@ -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)