summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-23 12:40:12 -0600
committermo khan <mo@mokhan.ca>2025-07-23 12:40:12 -0600
commitd2ebd0a9afed57ba11f053266e6ae1edb84a0f36 (patch)
treeeb547006085f2549f2cea5773c1ec75dba47bfd0
parent9674cfaedfdb8d583cfe75e1c1738a1c1d66c7f9 (diff)
feat: authorize requests to create sparkles
-rw-r--r--app/controllers/sparkles/controller.go12
-rw-r--r--app/controllers/sparkles/controller_test.go58
-rw-r--r--app/domain/permission.go (renamed from app/middleware/permission.go)9
-rw-r--r--app/init.go4
-rw-r--r--app/middleware/require_permission.go2
-rw-r--r--app/middleware/require_permission_test.go2
-rw-r--r--internal/stub/check.go6
-rw-r--r--pkg/authz/spice.go2
8 files changed, 56 insertions, 39 deletions
diff --git a/app/controllers/sparkles/controller.go b/app/controllers/sparkles/controller.go
index 80e95cb..ef2ecd5 100644
--- a/app/controllers/sparkles/controller.go
+++ b/app/controllers/sparkles/controller.go
@@ -14,11 +14,15 @@ import (
)
type Controller struct {
- db domain.Repository[*domain.Sparkle]
+ db domain.Repository[*domain.Sparkle]
+ check authz.CheckPermissionService
}
-func New(db domain.Repository[*domain.Sparkle], check authz.PermissionService) *Controller {
- return &Controller{db: db}
+func New(db domain.Repository[*domain.Sparkle], check authz.CheckPermissionService) *Controller {
+ return &Controller{
+ check: check,
+ db: db,
+ }
}
func (c *Controller) MountTo(mux *http.ServeMux) {
@@ -26,7 +30,7 @@ func (c *Controller) MountTo(mux *http.ServeMux) {
mux.Handle("POST /sparkles", x.Middleware[http.Handler](
http.HandlerFunc(c.Create),
middleware.RequireUser(),
- // middleware.RequirePermission("create", x.Try(ioc.Resolve[authz.CheckPermission](ioc.Default))),
+ middleware.RequirePermission("create", c.check),
))
// This is a temporary endpoint to restore a backup
diff --git a/app/controllers/sparkles/controller_test.go b/app/controllers/sparkles/controller_test.go
index 006c3fa..64b4dc5 100644
--- a/app/controllers/sparkles/controller_test.go
+++ b/app/controllers/sparkles/controller_test.go
@@ -62,16 +62,16 @@ func TestSparkles(t *testing.T) {
})
t.Run("POST /sparkles", func(t *testing.T) {
- t.Run("when a user is logged in", func(t *testing.T) {
+ t.Run("when a user is authenticated", func(t *testing.T) {
currentUser := domain.NewUser(domain.WithID[*domain.User](domain.ID("1")))
+ repository := db.NewRepository[*domain.Sparkle]()
- t.Run("when the user is authorized to create sparkles", func(t *testing.T) {
- t.Run("saves a new sparkle", func(t *testing.T) {
- repository := db.NewRepository[*domain.Sparkle]()
- mux := http.NewServeMux()
- controller := New(repository, stub.AllowWith(t, "user:1", "create", "sparkle:*"))
- controller.MountTo(mux)
+ t.Run("when the user is authorized", func(t *testing.T) {
+ mux := http.NewServeMux()
+ controller := New(repository, stub.AllowWith(t, "user:1", "create", "sparkle:*"))
+ controller.MountTo(mux)
+ t.Run("saves a new sparkle", func(t *testing.T) {
sparkle, _ := domain.NewSparkle("@tanuki for reviewing my code!")
request, response := test.RequestResponse(
"POST",
@@ -103,24 +103,42 @@ func TestSparkles(t *testing.T) {
assert.Equal(t, currentUser, item.Author)
})
})
+
+ t.Run("prevents double WriteHeader when serialization fails", func(t *testing.T) {
+ currentUser := domain.NewUser(domain.WithID[*domain.User](domain.ID("1")))
+ sparkle, _ := domain.NewSparkle("@user for testing")
+
+ request, response := test.RequestResponse(
+ "POST",
+ "/sparkles",
+ test.WithAcceptHeader(serde.JSON),
+ test.WithContentType(sparkle, serde.JSON),
+ test.WithContextKeyValue(t.Context(), cfg.CurrentUser, currentUser),
+ )
+
+ mux.ServeHTTP(&FailingResponseWriter{T: t, ResponseRecorder: response}, request)
+ })
})
- t.Run("prevents double WriteHeader when serialization fails", func(t *testing.T) {
- repository := db.NewRepository[*domain.Sparkle]()
- controller := New(repository, stub.Allow())
+ t.Run("when the user is not authorized", func(t *testing.T) {
+ mux := http.NewServeMux()
+ controller := New(repository, stub.Deny())
+ controller.MountTo(mux)
- currentUser := domain.NewUser(domain.WithID[*domain.User](domain.ID("1")))
- sparkle, _ := domain.NewSparkle("@user for testing")
+ t.Run("returns an error", func(t *testing.T) {
+ sparkle, _ := domain.NewSparkle("@tanuki for reviewing my code!")
+ request, response := test.RequestResponse(
+ "POST",
+ "/sparkles",
+ test.WithAcceptHeader(serde.JSON),
+ test.WithContentType(sparkle, serde.JSON),
+ test.WithContextKeyValue(t.Context(), cfg.CurrentUser, currentUser),
+ )
- request, response := test.RequestResponse(
- "POST",
- "/sparkles",
- test.WithAcceptHeader(serde.JSON),
- test.WithContentType(sparkle, serde.JSON),
- test.WithContextKeyValue(t.Context(), cfg.CurrentUser, currentUser),
- )
+ mux.ServeHTTP(response, request)
- controller.Create(&FailingResponseWriter{T: t, ResponseRecorder: response}, request)
+ require.Equal(t, http.StatusForbidden, response.Code)
+ })
})
})
})
diff --git a/app/middleware/permission.go b/app/domain/permission.go
index 36a7ea0..53d4754 100644
--- a/app/middleware/permission.go
+++ b/app/domain/permission.go
@@ -1,17 +1,12 @@
-package middleware
+package domain
import (
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
- "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) *v1.CheckPermissionRequest {
+func (p Permission) RequestFor(user Identifiable, resource Identifiable) *v1.CheckPermissionRequest {
return &v1.CheckPermissionRequest{
Subject: &v1.SubjectReference{
Object: user.ToObjectReference(),
diff --git a/app/init.go b/app/init.go
index 2986809..960102a 100644
--- a/app/init.go
+++ b/app/init.go
@@ -30,7 +30,7 @@ func init() {
env.Fetch("ZED_TOKEN", "secret"),
)
})
- ioc.Register[authz.PermissionService](ioc.Default, func() authz.PermissionService {
+ ioc.Register[authz.CheckPermissionService](ioc.Default, func() authz.CheckPermissionService {
return ioc.MustResolve[*authzed.Client](ioc.Default)
})
ioc.RegisterSingleton[domain.Repository[*domain.Sparkle]](ioc.Default, func() domain.Repository[*domain.Sparkle] {
@@ -45,7 +45,7 @@ func init() {
ioc.Register[*sparkles.Controller](ioc.Default, func() *sparkles.Controller {
return sparkles.New(
ioc.MustResolve[domain.Repository[*domain.Sparkle]](ioc.Default),
- ioc.MustResolve[authz.PermissionService](ioc.Default),
+ ioc.MustResolve[authz.CheckPermissionService](ioc.Default),
)
})
ioc.RegisterSingleton[*http.Client](ioc.Default, func() *http.Client {
diff --git a/app/middleware/require_permission.go b/app/middleware/require_permission.go
index cfcae0c..441b334 100644
--- a/app/middleware/require_permission.go
+++ b/app/middleware/require_permission.go
@@ -10,7 +10,7 @@ import (
"gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls"
)
-func RequirePermission(permission Permission, client authz.PermissionService) func(http.Handler) http.Handler {
+func RequirePermission(permission domain.Permission, client authz.CheckPermissionService) 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())
diff --git a/app/middleware/require_permission_test.go b/app/middleware/require_permission_test.go
index b11a33c..2023345 100644
--- a/app/middleware/require_permission_test.go
+++ b/app/middleware/require_permission_test.go
@@ -14,7 +14,7 @@ import (
func TestRequirePermission(t *testing.T) {
user := &domain.User{ID: domain.ID("1")}
ctx := cfg.CurrentUser.With(t.Context(), user)
- permission := Permission("read")
+ permission := domain.Permission("read")
t.Run("when the permission is granted", func(t *testing.T) {
r, w := test.RequestResponse("GET", "/sparkles", test.WithContext(ctx))
diff --git a/internal/stub/check.go b/internal/stub/check.go
index ec257e3..073b35b 100644
--- a/internal/stub/check.go
+++ b/internal/stub/check.go
@@ -17,7 +17,7 @@ func (m Check) CheckPermission(ctx context.Context, r *v1.CheckPermissionRequest
return m(ctx, r)
}
-func AllowWith(t *testing.T, subject string, permission string, resource string) authz.PermissionService {
+func AllowWith(t *testing.T, subject string, permission string, resource string) authz.CheckPermissionService {
user := strings.SplitN(subject, ":", 2)
model := strings.SplitN(resource, ":", 2)
@@ -36,7 +36,7 @@ func AllowWith(t *testing.T, subject string, permission string, resource string)
})
}
-func Allow() authz.PermissionService {
+func Allow() authz.CheckPermissionService {
return Check(func(ctx context.Context, r *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) {
return &v1.CheckPermissionResponse{
Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
@@ -44,7 +44,7 @@ func Allow() authz.PermissionService {
})
}
-func Deny() authz.PermissionService {
+func Deny() authz.CheckPermissionService {
return Check(func(ctx context.Context, r *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) {
return &v1.CheckPermissionResponse{
Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION,
diff --git a/pkg/authz/spice.go b/pkg/authz/spice.go
index 47468d9..cd534a1 100644
--- a/pkg/authz/spice.go
+++ b/pkg/authz/spice.go
@@ -21,6 +21,6 @@ func NewSpiceDBClient(ctx context.Context, host string, presharedKey string) *au
return client
}
-type PermissionService interface {
+type CheckPermissionService interface {
CheckPermission(ctx context.Context, in *v1.CheckPermissionRequest, opts ...grpc.CallOption) (*v1.CheckPermissionResponse, error)
}