diff options
| -rw-r--r-- | app/controllers/sparkles/controller.go | 3 | ||||
| -rw-r--r-- | app/controllers/sparkles/controller_test.go | 77 | ||||
| -rw-r--r-- | app/domain/user.go | 3 | ||||
| -rw-r--r-- | app/init.go | 25 | ||||
| -rw-r--r-- | app/middleware/require_permission.go | 2 | ||||
| -rw-r--r-- | app/middleware/require_permission_test.go | 32 | ||||
| -rw-r--r-- | internal/stub/check.go | 53 | ||||
| -rw-r--r-- | pkg/authz/spice.go | 2 |
8 files changed, 116 insertions, 81 deletions
diff --git a/app/controllers/sparkles/controller.go b/app/controllers/sparkles/controller.go index 7e6975c..80e95cb 100644 --- a/app/controllers/sparkles/controller.go +++ b/app/controllers/sparkles/controller.go @@ -9,6 +9,7 @@ import ( "github.com/xlgmokha/x/pkg/x" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/middleware" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/authz" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" ) @@ -16,7 +17,7 @@ type Controller struct { db domain.Repository[*domain.Sparkle] } -func New(db domain.Repository[*domain.Sparkle]) *Controller { +func New(db domain.Repository[*domain.Sparkle], check authz.PermissionService) *Controller { return &Controller{db: db} } diff --git a/app/controllers/sparkles/controller_test.go b/app/controllers/sparkles/controller_test.go index 37e1a82..006c3fa 100644 --- a/app/controllers/sparkles/controller_test.go +++ b/app/controllers/sparkles/controller_test.go @@ -13,6 +13,7 @@ import ( "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/db" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/internal/stub" ) type FailingResponseWriter struct { @@ -38,7 +39,7 @@ func TestSparkles(t *testing.T) { store.Save(t.Context(), sparkle) mux := http.NewServeMux() - controller := New(store) + controller := New(store, stub.AllowWith(t, "user:*", "read", "sparkle:*")) controller.MountTo(mux) t.Run("returns JSON", func(t *testing.T) { @@ -64,47 +65,49 @@ func TestSparkles(t *testing.T) { t.Run("when a user is logged in", func(t *testing.T) { currentUser := domain.NewUser(domain.WithID[*domain.User](domain.ID("1"))) - t.Run("saves a new sparkle", func(t *testing.T) { - repository := db.NewRepository[*domain.Sparkle]() - mux := http.NewServeMux() - controller := New(repository) - controller.MountTo(mux) - - 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), - ) - - mux.ServeHTTP(response, request) - - require.Equal(t, http.StatusCreated, response.Code) - - t.Run("returns a JSON representation of the sparkle", func(t *testing.T) { - item, err := serde.FromJSON[*domain.Sparkle](response.Body) - require.NoError(t, err) - - assert.NotEmpty(t, item.ID) - assert.Equal(t, "@tanuki", item.Sparklee) - assert.Equal(t, "for reviewing my code!", item.Reason) - }) - - t.Run("saves the sparkle to the db", func(t *testing.T) { - assert.Equal(t, 1, len(repository.All(t.Context()))) - item := repository.All(t.Context())[0] - - assert.Equal(t, "@tanuki", item.Sparklee) - assert.Equal(t, "for reviewing my code!", item.Reason) - assert.Equal(t, currentUser, item.Author) + 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) + + 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), + ) + + mux.ServeHTTP(response, request) + + require.Equal(t, http.StatusCreated, response.Code) + + t.Run("returns a JSON representation of the sparkle", func(t *testing.T) { + item, err := serde.FromJSON[*domain.Sparkle](response.Body) + require.NoError(t, err) + + assert.NotEmpty(t, item.ID) + assert.Equal(t, "@tanuki", item.Sparklee) + assert.Equal(t, "for reviewing my code!", item.Reason) + }) + + t.Run("saves the sparkle to the db", func(t *testing.T) { + assert.Equal(t, 1, len(repository.All(t.Context()))) + item := repository.All(t.Context())[0] + + assert.Equal(t, "@tanuki", item.Sparklee) + assert.Equal(t, "for reviewing my code!", item.Reason) + assert.Equal(t, currentUser, item.Author) + }) }) }) t.Run("prevents double WriteHeader when serialization fails", func(t *testing.T) { repository := db.NewRepository[*domain.Sparkle]() - controller := New(repository) + controller := New(repository, stub.Allow()) currentUser := domain.NewUser(domain.WithID[*domain.User](domain.ID("1"))) sparkle, _ := domain.NewSparkle("@user for testing") diff --git a/app/domain/user.go b/app/domain/user.go index 88930c5..d59baac 100644 --- a/app/domain/user.go +++ b/app/domain/user.go @@ -42,9 +42,8 @@ func (self *User) ToGID() string { } func (self *User) ToObjectReference() *v1.ObjectReference { - // TODO:: Username is easy for demos but will need to change to ID return &v1.ObjectReference{ ObjectType: "user", - ObjectId: self.Username, + ObjectId: self.ID.String(), } } diff --git a/app/init.go b/app/init.go index 53f9e66..2986809 100644 --- a/app/init.go +++ b/app/init.go @@ -23,6 +23,16 @@ func init() { ioc.RegisterSingleton[*zerolog.Logger](ioc.Default, func() *zerolog.Logger { return log.New(os.Stdout, log.Fields{"app": "sparkled"}) }) + ioc.RegisterSingleton[*authzed.Client](ioc.Default, func() *authzed.Client { + return authz.NewSpiceDBClient( + context.Background(), + env.Fetch("ZED_ENDPOINT", ":50051"), + env.Fetch("ZED_TOKEN", "secret"), + ) + }) + ioc.Register[authz.PermissionService](ioc.Default, func() authz.PermissionService { + return ioc.MustResolve[*authzed.Client](ioc.Default) + }) ioc.RegisterSingleton[domain.Repository[*domain.Sparkle]](ioc.Default, func() domain.Repository[*domain.Sparkle] { return db.NewRepository[*domain.Sparkle]() }) @@ -33,7 +43,10 @@ func init() { return dashboard.New() }) ioc.Register[*sparkles.Controller](ioc.Default, func() *sparkles.Controller { - return sparkles.New(ioc.MustResolve[domain.Repository[*domain.Sparkle]](ioc.Default)) + return sparkles.New( + ioc.MustResolve[domain.Repository[*domain.Sparkle]](ioc.Default), + ioc.MustResolve[authz.PermissionService](ioc.Default), + ) }) ioc.RegisterSingleton[*http.Client](ioc.Default, func() *http.Client { return &http.Client{ @@ -42,16 +55,6 @@ func init() { }, } }) - ioc.RegisterSingleton[*authzed.Client](ioc.Default, func() *authzed.Client { - return authz.NewSpiceDBClient( - context.Background(), - env.Fetch("ZED_ENDPOINT", ":50051"), - env.Fetch("ZED_TOKEN", "secret"), - ) - }) - ioc.Register[authz.CheckPermission](ioc.Default, func() authz.CheckPermission { - return ioc.MustResolve[*authzed.Client](ioc.Default) - }) http.DefaultClient = ioc.MustResolve[*http.Client](ioc.Default) diff --git a/app/middleware/require_permission.go b/app/middleware/require_permission.go index a10a9b6..cfcae0c 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.CheckPermission) func(http.Handler) http.Handler { +func RequirePermission(permission Permission, client authz.PermissionService) 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 df21b5a..b11a33c 100644 --- a/app/middleware/require_permission_test.go +++ b/app/middleware/require_permission_test.go @@ -1,45 +1,25 @@ package middleware import ( - "context" "net/http" "testing" - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/stretchr/testify/require" "github.com/xlgmokha/x/pkg/test" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/cfg" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" - "google.golang.org/grpc" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/internal/stub" ) -type MockAbility func(context.Context, *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) - -func (m MockAbility) CheckPermission(ctx context.Context, r *v1.CheckPermissionRequest, opts ...grpc.CallOption) (*v1.CheckPermissionResponse, 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") + permission := Permission("read") 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 *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) { - require.Equal(t, "user", r.Subject.Object.ObjectType) - require.Equal(t, user.Username, r.Subject.Object.ObjectId) - - require.Equal(t, permission.String(), r.Permission) - - require.Equal(t, "sparkle", r.Resource.ObjectType) - require.Equal(t, "*", r.Resource.ObjectId) - - return &v1.CheckPermissionResponse{ - Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, - }, nil - })) + middleware := RequirePermission(permission, stub.AllowWith(t, "user:1", permission.String(), "sparkle:*")) server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) })) @@ -51,11 +31,7 @@ func TestRequirePermission(t *testing.T) { 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 *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) { - return &v1.CheckPermissionResponse{ - Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, - }, nil - })) + middleware := RequirePermission(permission, stub.Deny()) server := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Fail(t, "unexpected call to handler") })) diff --git a/internal/stub/check.go b/internal/stub/check.go new file mode 100644 index 0000000..ec257e3 --- /dev/null +++ b/internal/stub/check.go @@ -0,0 +1,53 @@ +package stub + +import ( + "context" + "strings" + "testing" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/authz" + "google.golang.org/grpc" +) + +type Check func(context.Context, *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) + +func (m Check) CheckPermission(ctx context.Context, r *v1.CheckPermissionRequest, opts ...grpc.CallOption) (*v1.CheckPermissionResponse, error) { + return m(ctx, r) +} + +func AllowWith(t *testing.T, subject string, permission string, resource string) authz.PermissionService { + user := strings.SplitN(subject, ":", 2) + model := strings.SplitN(resource, ":", 2) + + return Check(func(ctx context.Context, r *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) { + require.Equal(t, user[0], r.Subject.Object.ObjectType) + require.Equal(t, user[1], r.Subject.Object.ObjectId) + + require.Equal(t, permission, r.Permission) + + require.Equal(t, model[0], r.Resource.ObjectType) + require.Equal(t, model[1], r.Resource.ObjectId) + + return &v1.CheckPermissionResponse{ + Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, + }, nil + }) +} + +func Allow() authz.PermissionService { + return Check(func(ctx context.Context, r *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) { + return &v1.CheckPermissionResponse{ + Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, + }, nil + }) +} + +func Deny() authz.PermissionService { + return Check(func(ctx context.Context, r *v1.CheckPermissionRequest) (*v1.CheckPermissionResponse, error) { + return &v1.CheckPermissionResponse{ + Permissionship: v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION, + }, nil + }) +} diff --git a/pkg/authz/spice.go b/pkg/authz/spice.go index a45a732..47468d9 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 CheckPermission interface { +type PermissionService interface { CheckPermission(ctx context.Context, in *v1.CheckPermissionRequest, opts ...grpc.CallOption) (*v1.CheckPermissionResponse, error) } |
