From c1698f896ff343b1b65e57d3961a78d3bb263b7c Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 30 Jul 2025 11:03:45 -0600 Subject: refactor: rename repository types --- app/db/authorization.go | 121 +++++++++++++++++++++++++++++++++++ app/db/publisher.go | 31 +++++++++ app/db/publisher_test.go | 34 ++++++++++ app/db/publishing_repository.go | 31 --------- app/db/publishing_repository_test.go | 34 ---------- app/db/secure_repository.go | 119 ---------------------------------- app/init.go | 10 +-- 7 files changed, 191 insertions(+), 189 deletions(-) create mode 100644 app/db/authorization.go create mode 100644 app/db/publisher.go create mode 100644 app/db/publisher_test.go delete mode 100644 app/db/publishing_repository.go delete mode 100644 app/db/publishing_repository_test.go delete mode 100644 app/db/secure_repository.go (limited to 'app') diff --git a/app/db/authorization.go b/app/db/authorization.go new file mode 100644 index 0000000..3041f51 --- /dev/null +++ b/app/db/authorization.go @@ -0,0 +1,121 @@ +package db + +import ( + "context" + "errors" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/authzed/authzed-go/v1" + "github.com/xlgmokha/x/pkg/x" + "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" +) + +type authorization[T domain.Entity] struct { + client *authzed.Client + repository domain.Repository[T] +} + +func WithAuthorization[T domain.Entity](client *authzed.Client) x.Option[domain.Repository[T]] { + return func(repository domain.Repository[T]) domain.Repository[T] { + return &authorization[T]{ + client: client, + repository: repository, + } + } +} + +func (r *authorization[T]) All(ctx context.Context) []T { + allItems := r.repository.All(ctx) + if len(allItems) == 0 { + return allItems + } + + response, err := r.client.CheckBulkPermissions(ctx, &v1.CheckBulkPermissionsRequest{ + Items: x.Map(allItems, func(item T) *v1.CheckBulkPermissionsRequestItem { + return &v1.CheckBulkPermissionsRequestItem{ + Resource: item.ToGID().ToObjectReference(), + Permission: "read", + Subject: r.subjectFrom(ctx), + } + }), + }) + if err != nil { + pls.LogError(ctx, err) + return x.Default[[]T]() + } + + filteredItems := make([]T, 0) + for i, pair := range response.Pairs { + if item := pair.GetItem(); item != nil { + if item.Permissionship == v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { + filteredItems = append(filteredItems, allItems[i]) + } + } + } + + return filteredItems +} + +func (r *authorization[T]) Find(ctx context.Context, id domain.ID) T { + item := r.repository.Find(ctx, id) + + response, err := r.client.CheckPermission(ctx, &v1.CheckPermissionRequest{ + Resource: item.ToGID().ToObjectReference(), + Permission: "read", + Subject: r.subjectFrom(ctx), + }) + if err != nil { + pls.LogError(ctx, err) + return x.Default[T]() + } + + if response.Permissionship != v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { + return x.Default[T]() + } + + return item +} + +func (r *authorization[T]) Save(ctx context.Context, item T) error { + currentUser := cfg.CurrentUser.From(ctx) + if currentUser == nil { + return errors.New("authentication required for creating or updating entities") + } + + if item.GetID() != "" { + response, err := r.client.CheckPermission(ctx, &v1.CheckPermissionRequest{ + Resource: item.ToGID().ToObjectReference(), + Permission: "update", + Subject: currentUser.ToSubjectReference(), + }) + if err != nil { + return err + } + + if response.Permissionship != v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { + return errors.New("user does not have permission to update this entity") + } + } + + if sparkle, ok := any(item).(*domain.Sparkle); ok && item.GetID() == "" { + sparkle.Author = currentUser + } + + return r.repository.Save(ctx, item) +} + +func (r *authorization[T]) subjectFrom(ctx context.Context) *v1.SubjectReference { + currentUser := cfg.CurrentUser.From(ctx) + if currentUser == nil { + return &v1.SubjectReference{ + Object: &v1.ObjectReference{ + ObjectType: "user", + ObjectId: "*", + }, + } + } + + return currentUser.ToSubjectReference() +} diff --git a/app/db/publisher.go b/app/db/publisher.go new file mode 100644 index 0000000..ee2a966 --- /dev/null +++ b/app/db/publisher.go @@ -0,0 +1,31 @@ +package db + +import ( + "context" + + "github.com/xlgmokha/x/pkg/event" + "github.com/xlgmokha/x/pkg/x" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" +) + +type publisher[T domain.Entity] struct { + aggregator *event.TypedAggregator[T] + domain.Repository[T] +} + +func WithPublishing[T domain.Entity](aggregator *event.TypedAggregator[T]) x.Option[domain.Repository[T]] { + return func(repository domain.Repository[T]) domain.Repository[T] { + return &publisher[T]{ + aggregator: aggregator, + Repository: repository, + } + } +} + +func (r *publisher[T]) Save(ctx context.Context, item T) error { + err := r.Repository.Save(ctx, item) + if err == nil { + r.aggregator.Publish("after.create", item) + } + return err +} diff --git a/app/db/publisher_test.go b/app/db/publisher_test.go new file mode 100644 index 0000000..eeb912a --- /dev/null +++ b/app/db/publisher_test.go @@ -0,0 +1,34 @@ +package db + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xlgmokha/x/pkg/event" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" +) + +func TestWithPublishing(t *testing.T) { + aggregator := event.New[*domain.Sparkle]() + storage := NewRepository[*domain.Sparkle](WithPublishing(aggregator)) + + t.Run("Save", func(t *testing.T) { + t.Run("publishes an event", func(t *testing.T) { + called := false + var payload *domain.Sparkle + + aggregator.SubscribeTo("after.create", func(item *domain.Sparkle) { + called = true + payload = item + }) + + sparkle := &domain.Sparkle{Sparklee: "@tanuki", Reason: "because"} + require.NoError(t, storage.Save(t.Context(), sparkle)) + + require.True(t, called) + require.NotNil(t, payload) + assert.Equal(t, sparkle, payload) + }) + }) +} diff --git a/app/db/publishing_repository.go b/app/db/publishing_repository.go deleted file mode 100644 index 049129e..0000000 --- a/app/db/publishing_repository.go +++ /dev/null @@ -1,31 +0,0 @@ -package db - -import ( - "context" - - "github.com/xlgmokha/x/pkg/event" - "github.com/xlgmokha/x/pkg/x" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" -) - -type publishingRepository[T domain.Entity] struct { - aggregator *event.TypedAggregator[T] - domain.Repository[T] -} - -func WithPublishing[T domain.Entity](aggregator *event.TypedAggregator[T]) x.Option[domain.Repository[T]] { - return func(repository domain.Repository[T]) domain.Repository[T] { - return &publishingRepository[T]{ - aggregator: aggregator, - Repository: repository, - } - } -} - -func (r *publishingRepository[T]) Save(ctx context.Context, item T) error { - err := r.Repository.Save(ctx, item) - if err == nil { - r.aggregator.Publish("after.create", item) - } - return err -} diff --git a/app/db/publishing_repository_test.go b/app/db/publishing_repository_test.go deleted file mode 100644 index 4a2c05f..0000000 --- a/app/db/publishing_repository_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package db - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/xlgmokha/x/pkg/event" - "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" -) - -func TestPublishingRepository(t *testing.T) { - aggregator := event.New[*domain.Sparkle]() - storage := NewRepository[*domain.Sparkle](WithPublishing(aggregator)) - - t.Run("Save", func(t *testing.T) { - t.Run("publishes an event", func(t *testing.T) { - called := false - var payload *domain.Sparkle - - aggregator.SubscribeTo("after.create", func(item *domain.Sparkle) { - called = true - payload = item - }) - - sparkle := &domain.Sparkle{Sparklee: "@tanuki", Reason: "because"} - require.NoError(t, storage.Save(t.Context(), sparkle)) - - require.True(t, called) - require.NotNil(t, payload) - assert.Equal(t, sparkle, payload) - }) - }) -} diff --git a/app/db/secure_repository.go b/app/db/secure_repository.go deleted file mode 100644 index 26b85c9..0000000 --- a/app/db/secure_repository.go +++ /dev/null @@ -1,119 +0,0 @@ -package db - -import ( - "context" - "errors" - - v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" - "github.com/authzed/authzed-go/v1" - "github.com/xlgmokha/x/pkg/x" - "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" -) - -type SecureRepository[T domain.Entity] struct { - client *authzed.Client - repository domain.Repository[T] -} - -func NewSecureRepository[T domain.Entity](client *authzed.Client, repository domain.Repository[T]) domain.Repository[T] { - return &SecureRepository[T]{ - client: client, - repository: repository, - } -} - -func (r *SecureRepository[T]) All(ctx context.Context) []T { - allItems := r.repository.All(ctx) - if len(allItems) == 0 { - return allItems - } - - response, err := r.client.CheckBulkPermissions(ctx, &v1.CheckBulkPermissionsRequest{ - Items: x.Map(allItems, func(item T) *v1.CheckBulkPermissionsRequestItem { - return &v1.CheckBulkPermissionsRequestItem{ - Resource: item.ToGID().ToObjectReference(), - Permission: "read", - Subject: r.subjectFrom(ctx), - } - }), - }) - if err != nil { - pls.LogError(ctx, err) - return x.Default[[]T]() - } - - filteredItems := make([]T, 0) - for i, pair := range response.Pairs { - if item := pair.GetItem(); item != nil { - if item.Permissionship == v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { - filteredItems = append(filteredItems, allItems[i]) - } - } - } - - return filteredItems -} - -func (r *SecureRepository[T]) Find(ctx context.Context, id domain.ID) T { - item := r.repository.Find(ctx, id) - - response, err := r.client.CheckPermission(ctx, &v1.CheckPermissionRequest{ - Resource: item.ToGID().ToObjectReference(), - Permission: "read", - Subject: r.subjectFrom(ctx), - }) - if err != nil { - pls.LogError(ctx, err) - return x.Default[T]() - } - - if response.Permissionship != v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { - return x.Default[T]() - } - - return item -} - -func (r *SecureRepository[T]) Save(ctx context.Context, item T) error { - currentUser := cfg.CurrentUser.From(ctx) - if currentUser == nil { - return errors.New("authentication required for creating or updating entities") - } - - if item.GetID() != "" { - response, err := r.client.CheckPermission(ctx, &v1.CheckPermissionRequest{ - Resource: item.ToGID().ToObjectReference(), - Permission: "update", - Subject: currentUser.ToSubjectReference(), - }) - if err != nil { - return err - } - - if response.Permissionship != v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION { - return errors.New("user does not have permission to update this entity") - } - } - - if sparkle, ok := any(item).(*domain.Sparkle); ok && item.GetID() == "" { - sparkle.Author = currentUser - } - - return r.repository.Save(ctx, item) -} - -func (r *SecureRepository[T]) subjectFrom(ctx context.Context) *v1.SubjectReference { - currentUser := cfg.CurrentUser.From(ctx) - if currentUser == nil { - return &v1.SubjectReference{ - Object: &v1.ObjectReference{ - ObjectType: "user", - ObjectId: "*", - }, - } - } - - return currentUser.ToSubjectReference() -} diff --git a/app/init.go b/app/init.go index 3a771f8..398c8a9 100644 --- a/app/init.go +++ b/app/init.go @@ -48,11 +48,11 @@ func init() { }) ioc.RegisterSingleton[domain.Repository[*domain.Sparkle]](c, func() domain.Repository[*domain.Sparkle] { aggregator := ioc.MustResolve[*event.TypedAggregator[*domain.Sparkle]](c) - repo := db.NewRepository[*domain.Sparkle](db.WithPublishing(aggregator)) - // repo := db.NewPublishingRepository[*domain.Sparkle](aggregator, db.NewRepository[*domain.Sparkle]()) - return db.NewSecureRepository[*domain.Sparkle]( - ioc.MustResolve[*authzed.Client](c), - repo, + client := ioc.MustResolve[*authzed.Client](c) + + return db.NewRepository[*domain.Sparkle]( + db.WithAuthorization[*domain.Sparkle](client), + db.WithPublishing(aggregator), ) }) ioc.RegisterSingleton[*http.ServeMux](c, func() *http.ServeMux { -- cgit v1.2.3