From 700567071ba07838d33264f9dbc0a2acdc2d5946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Fri, 27 Mar 2026 21:29:26 +0100 Subject: [PATCH] feat(auth): RequireNotRestricted middleware Co-Authored-By: Claude Sonnet 4.6 --- server/backend/internal/httpapi/middleware.go | 14 +++++ .../internal/httpapi/middleware_test.go | 63 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 server/backend/internal/httpapi/middleware_test.go diff --git a/server/backend/internal/httpapi/middleware.go b/server/backend/internal/httpapi/middleware.go index c2911a7..ac3ce33 100644 --- a/server/backend/internal/httpapi/middleware.go +++ b/server/backend/internal/httpapi/middleware.go @@ -55,6 +55,20 @@ func RequireAdmin(next http.Handler) http.Handler { }) } +// RequireNotRestricted is middleware that blocks users with role "restricted". +// It must be chained after RequireAuth (so a user is present in context). +// On failure it responds with 403 Forbidden. +func RequireNotRestricted(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := UserFromContext(r.Context()) + if user != nil && user.Role == "restricted" { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} + // RequireTenantAccess is middleware that allows access only when the // authenticated user belongs to the tenant identified by the {tenantSlug} // path value, or when the user has role "admin" (admins can access everything). diff --git a/server/backend/internal/httpapi/middleware_test.go b/server/backend/internal/httpapi/middleware_test.go new file mode 100644 index 0000000..31c9223 --- /dev/null +++ b/server/backend/internal/httpapi/middleware_test.go @@ -0,0 +1,63 @@ +package httpapi_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "git.az-it.net/az/morz-infoboard/server/backend/internal/httpapi" + "git.az-it.net/az/morz-infoboard/server/backend/internal/reqcontext" + "git.az-it.net/az/morz-infoboard/server/backend/internal/store" +) + +func userCtx(role string) context.Context { + return reqcontext.WithUser(context.Background(), &store.User{Role: role}) +} + +func TestRequireNotRestricted_blocks_restricted(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", nil).WithContext(userCtx("restricted")) + rr := httptest.NewRecorder() + httpapi.RequireNotRestricted(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatal("should not be called") + })).ServeHTTP(rr, req) + if rr.Code != http.StatusForbidden { + t.Fatalf("expected 403, got %d", rr.Code) + } +} + +func TestRequireNotRestricted_allows_screen_user(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", nil).WithContext(userCtx("screen_user")) + rr := httptest.NewRecorder() + called := false + httpapi.RequireNotRestricted(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + })).ServeHTTP(rr, req) + if !called { + t.Fatal("expected next to be called") + } +} + +func TestRequireNotRestricted_allows_admin(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", nil).WithContext(userCtx("admin")) + rr := httptest.NewRecorder() + called := false + httpapi.RequireNotRestricted(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + })).ServeHTTP(rr, req) + if !called { + t.Fatal("expected next to be called") + } +} + +func TestRequireNotRestricted_allows_no_user(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, "/", nil) + rr := httptest.NewRecorder() + called := false + httpapi.RequireNotRestricted(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + called = true + })).ServeHTTP(rr, req) + if !called { + t.Fatal("no user in context — RequireAuth handles that, this middleware passes through") + } +}