summaryrefslogtreecommitdiff
path: root/app/middleware
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-05-24 00:08:00 -0600
committermo khan <mo@mokhan.ca>2025-05-24 00:08:00 -0600
commita8e47145f93f07740d751be37d450599b26b2fc8 (patch)
tree3d8fdd7f7fcbda853cf1f42b4a5d96df57c3d71d /app/middleware
parent1ab4de8ec28d4fdd46cd9b3e246eea4a85ca6b6c (diff)
feat: create middleware to check if user has permission
Diffstat (limited to 'app/middleware')
-rw-r--r--app/middleware/permission.go24
-rw-r--r--app/middleware/require_permission.go33
-rw-r--r--app/middleware/require_permission_test.go58
3 files changed, 115 insertions, 0 deletions
diff --git a/app/middleware/permission.go b/app/middleware/permission.go
new file mode 100644
index 0000000..03e7cf9
--- /dev/null
+++ b/app/middleware/permission.go
@@ -0,0 +1,24 @@
+package middleware
+
+import (
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain"
+)
+
+type Permission string
+
+func (p Permission) ToGID() string {
+ return "gid://sparkle/Permission/" + p.String()
+}
+
+func (p Permission) RequestFor(user domain.Identifiable, resource domain.Identifiable) *rpc.AllowRequest {
+ return &rpc.AllowRequest{
+ Subject: user.ToGID(),
+ Permission: p.ToGID(),
+ Resource: resource.ToGID(),
+ }
+}
+
+func (p Permission) String() string {
+ return string(p)
+}
diff --git a/app/middleware/require_permission.go b/app/middleware/require_permission.go
new file mode 100644
index 0000000..563278e
--- /dev/null
+++ b/app/middleware/require_permission.go
@@ -0,0 +1,33 @@
+package middleware
+
+import (
+ "net/http"
+
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls"
+)
+
+func RequirePermission(permission Permission, ability rpc.Ability) func(http.Handler) http.Handler {
+ return func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ user := cfg.CurrentUser.From(r.Context())
+
+ reply, err := ability.Allowed(r.Context(),
+ permission.RequestFor(user, &domain.Sparkle{ID: "*"}),
+ )
+ if err != nil {
+ pls.LogError(r.Context(), err)
+ w.WriteHeader(http.StatusForbidden)
+ return
+ }
+
+ if reply.Result {
+ next.ServeHTTP(w, r)
+ } else {
+ w.WriteHeader(http.StatusForbidden)
+ }
+ })
+ }
+}
diff --git a/app/middleware/require_permission_test.go b/app/middleware/require_permission_test.go
new file mode 100644
index 0000000..34a04a7
--- /dev/null
+++ b/app/middleware/require_permission_test.go
@@ -0,0 +1,58 @@
+package middleware
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/xlgmokha/x/pkg/test"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/authzd.git/pkg/rpc"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg"
+ "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain"
+)
+
+type MockAbility func(context.Context, *rpc.AllowRequest) (*rpc.AllowReply, error)
+
+func (m MockAbility) Allowed(ctx context.Context, r *rpc.AllowRequest) (*rpc.AllowReply, error) {
+ return m(ctx, r)
+}
+
+func TestRequirePermission(t *testing.T) {
+ user := &domain.User{ID: domain.ID("1")}
+ ctx := cfg.CurrentUser.With(t.Context(), user)
+ permission := Permission("read_sparkles")
+
+ t.Run("when the permission is granted", func(t *testing.T) {
+ r, w := test.RequestResponse("GET", "/sparkles", test.WithContext(ctx))
+
+ middleware := RequirePermission(permission, MockAbility(func(ctx context.Context, r *rpc.AllowRequest) (*rpc.AllowReply, error) {
+ require.Equal(t, "gid://sparkle/User/"+user.ID.String(), r.Subject)
+ require.Equal(t, permission.ToGID(), r.Permission)
+ require.Equal(t, "gid://sparkle/Sparkle/*", r.Resource)
+
+ return &rpc.AllowReply{Result: true}, nil
+ }))
+ server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusTeapot)
+ }))
+ server.ServeHTTP(w, r)
+
+ require.Equal(t, http.StatusTeapot, w.Code)
+ })
+
+ t.Run("when the permission is denied", func(t *testing.T) {
+ r, w := test.RequestResponse("GET", "/sparkles", test.WithContext(ctx))
+
+ middleware := RequirePermission(permission, MockAbility(func(ctx context.Context, r *rpc.AllowRequest) (*rpc.AllowReply, error) {
+ return &rpc.AllowReply{Result: false}, nil
+ }))
+ server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Fail(t, "unexpected call to handler")
+ }))
+
+ server.ServeHTTP(w, r)
+
+ require.Equal(t, http.StatusForbidden, w.Code)
+ })
+}