From f39fe4ef183164af559768e09ff3388f3617997c Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 30 Jul 2025 10:15:17 -0600 Subject: refactor: extract a separate repository to publish events --- app/controllers/sparkles/controller_test.go | 5 ++-- app/db/in_memory_repository.go | 14 ++++------- app/db/in_memory_repository_test.go | 25 +++----------------- app/db/publishing_repository.go | 36 +++++++++++++++++++++++++++++ app/db/publishing_repository_test.go | 35 ++++++++++++++++++++++++++++ app/init.go | 7 +++++- 6 files changed, 87 insertions(+), 35 deletions(-) create mode 100644 app/db/publishing_repository.go create mode 100644 app/db/publishing_repository_test.go (limited to 'app') diff --git a/app/controllers/sparkles/controller_test.go b/app/controllers/sparkles/controller_test.go index 5c37a11..71e1d96 100644 --- a/app/controllers/sparkles/controller_test.go +++ b/app/controllers/sparkles/controller_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xlgmokha/x/pkg/event" "github.com/xlgmokha/x/pkg/serde" "github.com/xlgmokha/x/pkg/test" "github.com/xlgmokha/x/pkg/x" @@ -37,7 +36,7 @@ func (f *FailingResponseWriter) Write([]byte) (int, error) { func TestSparkles(t *testing.T) { t.Run("GET /sparkles", func(t *testing.T) { sparkle := x.New[*domain.Sparkle](domain.WithText("@tanuki for helping me")) - store := db.NewRepository[*domain.Sparkle](event.New[*domain.Sparkle]()) + store := db.NewRepository[*domain.Sparkle]() store.Save(t.Context(), sparkle) mux := http.NewServeMux() @@ -66,7 +65,7 @@ func TestSparkles(t *testing.T) { t.Run("POST /sparkles", func(t *testing.T) { t.Run("when a user is authenticated", func(t *testing.T) { currentUser := x.New[*domain.User](domain.WithID[*domain.User](domain.ID("1"))) - repository := db.NewRepository[*domain.Sparkle](event.New[*domain.Sparkle]()) + repository := db.NewRepository[*domain.Sparkle]() t.Run("when the user is authorized", func(t *testing.T) { mux := http.NewServeMux() diff --git a/app/db/in_memory_repository.go b/app/db/in_memory_repository.go index 1177662..2aa1fed 100644 --- a/app/db/in_memory_repository.go +++ b/app/db/in_memory_repository.go @@ -5,23 +5,20 @@ import ( "sort" "sync" - "github.com/xlgmokha/x/pkg/event" "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/pkg/pls" ) type inMemoryRepository[T domain.Entity] struct { - aggregator *event.TypedAggregator[T] - items []T - mu sync.RWMutex + items []T + mu sync.RWMutex } -func NewRepository[T domain.Entity](aggregator *event.TypedAggregator[T]) domain.Repository[T] { +func NewRepository[T domain.Entity]() domain.Repository[T] { return &inMemoryRepository[T]{ - aggregator: aggregator, - items: []T{}, - mu: sync.RWMutex{}, + items: []T{}, + mu: sync.RWMutex{}, } } @@ -53,6 +50,5 @@ func (r *inMemoryRepository[T]) Save(ctx context.Context, item T) error { sort.Slice(r.items, func(i, j int) bool { return r.items[i].GetID() > r.items[j].GetID() }) - r.aggregator.Publish("after.create", item) return nil } diff --git a/app/db/in_memory_repository_test.go b/app/db/in_memory_repository_test.go index 5bb220e..b89f16a 100644 --- a/app/db/in_memory_repository_test.go +++ b/app/db/in_memory_repository_test.go @@ -7,14 +7,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/xlgmokha/x/pkg/event" "github.com/xlgmokha/x/pkg/x" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" ) func TestInMemoryRepository(t *testing.T) { - aggregator := event.New[*domain.Sparkle]() - storage := NewRepository[*domain.Sparkle](aggregator) + storage := NewRepository[*domain.Sparkle]() t.Run("Save", func(t *testing.T) { t.Run("an invalid Sparkle", func(t *testing.T) { @@ -33,25 +31,8 @@ func TestInMemoryRepository(t *testing.T) { assert.Equal(t, "because", sparkles[0].Reason) }) - 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) - }) - t.Run("prevents race conditions", func(t *testing.T) { - repository := NewRepository[*domain.Sparkle](aggregator) + repository := NewRepository[*domain.Sparkle]() ctx := context.Background() numGoroutines := 100 @@ -112,7 +93,7 @@ func TestInMemoryRepository(t *testing.T) { }) t.Run("All", func(t *testing.T) { - repository := NewRepository[*domain.Sparkle](aggregator) + repository := NewRepository[*domain.Sparkle]() require.NoError(t, repository.Save(t.Context(), &domain.Sparkle{ Sparklee: "@tanuki", Reason: "because", diff --git a/app/db/publishing_repository.go b/app/db/publishing_repository.go new file mode 100644 index 0000000..6be8cf8 --- /dev/null +++ b/app/db/publishing_repository.go @@ -0,0 +1,36 @@ +package db + +import ( + "context" + + "github.com/xlgmokha/x/pkg/event" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" +) + +type PublishingRepository[T domain.Entity] struct { + aggregator *event.TypedAggregator[T] + repository domain.Repository[T] +} + +func NewPublishingRepository[T domain.Entity](aggregator *event.TypedAggregator[T], repository domain.Repository[T]) domain.Repository[T] { + return &PublishingRepository[T]{ + aggregator: aggregator, + repository: repository, + } +} + +func (r *PublishingRepository[T]) All(ctx context.Context) []T { + return r.repository.All(ctx) +} + +func (r *PublishingRepository[T]) Find(ctx context.Context, id domain.ID) T { + return r.repository.Find(ctx, id) +} + +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 new file mode 100644 index 0000000..7bbc999 --- /dev/null +++ b/app/db/publishing_repository_test.go @@ -0,0 +1,35 @@ +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]() + repository := NewRepository[*domain.Sparkle]() + storage := NewPublishingRepository[*domain.Sparkle](aggregator, repository) + + 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/init.go b/app/init.go index ea67e48..f0d40b2 100644 --- a/app/init.go +++ b/app/init.go @@ -47,7 +47,12 @@ func init() { )) }) ioc.RegisterSingleton[domain.Repository[*domain.Sparkle]](c, func() domain.Repository[*domain.Sparkle] { - return db.NewRepository[*domain.Sparkle](ioc.MustResolve[*event.TypedAggregator[*domain.Sparkle]](c)) + aggregator := ioc.MustResolve[*event.TypedAggregator[*domain.Sparkle]](c) + repo := db.NewPublishingRepository[*domain.Sparkle](aggregator, db.NewRepository[*domain.Sparkle]()) + return db.NewSecureRepository[*domain.Sparkle]( + ioc.MustResolve[*authzed.Client](c), + repo, + ) }) ioc.RegisterSingleton[*http.ServeMux](c, func() *http.ServeMux { return http.NewServeMux() -- cgit v1.2.3 From 0626bc0cfffa89b73adc2f9576354e8462270eae Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 30 Jul 2025 10:54:20 -0600 Subject: refactor: add ctor option to add repository publishing --- app/db/in_memory_repository.go | 6 +- app/db/publishing_repository.go | 27 +++--- app/db/publishing_repository_test.go | 3 +- app/init.go | 3 +- go.mod | 8 +- go.sum | 16 ++-- vendor/github.com/coreos/go-oidc/v3/oidc/verify.go | 99 +++++++++------------- .../docker/docker/api/types/registry/authconfig.go | 2 + .../github.com/docker/docker/client/image_push.go | 11 ++- vendor/github.com/golang-jwt/jwt/v5/errors.go | 40 +++++++++ .../github.com/golang-jwt/jwt/v5/errors_go1_20.go | 47 ---------- .../golang-jwt/jwt/v5/errors_go_other.go | 78 ----------------- vendor/github.com/golang-jwt/jwt/v5/rsa_pss.go | 3 - vendor/github.com/xlgmokha/x/pkg/x/option.go | 5 +- vendor/modules.txt | 10 +-- 15 files changed, 131 insertions(+), 227 deletions(-) delete mode 100644 vendor/github.com/golang-jwt/jwt/v5/errors_go1_20.go delete mode 100644 vendor/github.com/golang-jwt/jwt/v5/errors_go_other.go (limited to 'app') diff --git a/app/db/in_memory_repository.go b/app/db/in_memory_repository.go index 2aa1fed..5ee4fea 100644 --- a/app/db/in_memory_repository.go +++ b/app/db/in_memory_repository.go @@ -15,11 +15,11 @@ type inMemoryRepository[T domain.Entity] struct { mu sync.RWMutex } -func NewRepository[T domain.Entity]() domain.Repository[T] { - return &inMemoryRepository[T]{ +func NewRepository[T domain.Entity](options ...x.Option[domain.Repository[T]]) domain.Repository[T] { + return x.NewWith[domain.Repository[T]](&inMemoryRepository[T]{ items: []T{}, mu: sync.RWMutex{}, - } + }, options...) } func (r *inMemoryRepository[T]) All(ctx context.Context) []T { diff --git a/app/db/publishing_repository.go b/app/db/publishing_repository.go index 6be8cf8..049129e 100644 --- a/app/db/publishing_repository.go +++ b/app/db/publishing_repository.go @@ -4,31 +4,26 @@ 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 { +type publishingRepository[T domain.Entity] struct { aggregator *event.TypedAggregator[T] - repository domain.Repository[T] + domain.Repository[T] } -func NewPublishingRepository[T domain.Entity](aggregator *event.TypedAggregator[T], repository domain.Repository[T]) domain.Repository[T] { - return &PublishingRepository[T]{ - aggregator: aggregator, - repository: repository, +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]) All(ctx context.Context) []T { - return r.repository.All(ctx) -} - -func (r *PublishingRepository[T]) Find(ctx context.Context, id domain.ID) T { - return r.repository.Find(ctx, id) -} - -func (r *PublishingRepository[T]) Save(ctx context.Context, item T) error { - err := r.repository.Save(ctx, item) +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) } diff --git a/app/db/publishing_repository_test.go b/app/db/publishing_repository_test.go index 7bbc999..4a2c05f 100644 --- a/app/db/publishing_repository_test.go +++ b/app/db/publishing_repository_test.go @@ -11,8 +11,7 @@ import ( func TestPublishingRepository(t *testing.T) { aggregator := event.New[*domain.Sparkle]() - repository := NewRepository[*domain.Sparkle]() - storage := NewPublishingRepository[*domain.Sparkle](aggregator, repository) + storage := NewRepository[*domain.Sparkle](WithPublishing(aggregator)) t.Run("Save", func(t *testing.T) { t.Run("publishes an event", func(t *testing.T) { diff --git a/app/init.go b/app/init.go index f0d40b2..3a771f8 100644 --- a/app/init.go +++ b/app/init.go @@ -48,7 +48,8 @@ func init() { }) ioc.RegisterSingleton[domain.Repository[*domain.Sparkle]](c, func() domain.Repository[*domain.Sparkle] { aggregator := ioc.MustResolve[*event.TypedAggregator[*domain.Sparkle]](c) - repo := db.NewPublishingRepository[*domain.Sparkle](aggregator, db.NewRepository[*domain.Sparkle]()) + 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, diff --git a/go.mod b/go.mod index 88756c6..a926312 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,15 @@ go 1.24 require ( github.com/authzed/authzed-go v1.4.1 github.com/containerd/log v0.1.0 - github.com/coreos/go-oidc/v3 v3.14.1 - github.com/docker/docker v28.3.2+incompatible + github.com/coreos/go-oidc/v3 v3.15.0 + github.com/docker/docker v28.3.3+incompatible github.com/envoyproxy/go-control-plane/envoy v1.32.4 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 github.com/oklog/ulid v1.3.1 github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.38.0 - github.com/xlgmokha/x v0.0.0-20250724192332-f79ef71d5cac + github.com/xlgmokha/x v0.0.0-20250730165105-1a2af5f242cf golang.org/x/oauth2 v0.30.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0 google.golang.org/grpc v1.74.2 @@ -101,7 +101,7 @@ require ( github.com/go-stack/stack v1.8.1 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.3 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/golobby/container/v3 v3.3.2 // indirect diff --git a/go.sum b/go.sum index 8183920..bcaeba8 100644 --- a/go.sum +++ b/go.sum @@ -153,8 +153,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk= -github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= +github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg= +github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= @@ -177,8 +177,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlmiddlecote/sqlstats v1.0.2 h1:gSU11YN23D/iY50A2zVYwgXgy072khatTsIW6UPjUtI= github.com/dlmiddlecote/sqlstats v1.0.2/go.mod h1:0CWaIh/Th+z2aI6Q9Jpfg/o21zmGxWhbByHgQSCUQvY= -github.com/docker/docker v28.3.2+incompatible h1:wn66NJ6pWB1vBZIilP8G3qQPqHy5XymfYn5vsqeA5oA= -github.com/docker/docker v28.3.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= +github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -267,8 +267,8 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= -github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -589,8 +589,8 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xlgmokha/minit v0.0.0-20250725204255-8e0834741617 h1:MM4yskiyvXBz8ufTTp3PWgpAZMY4BEGIIbuS3arc0Ws= github.com/xlgmokha/minit v0.0.0-20250725204255-8e0834741617/go.mod h1:2Kojbsk+Nuj/Rcf+kHkJOKnf+VstT/r1ZRS4pPqtVXY= -github.com/xlgmokha/x v0.0.0-20250724192332-f79ef71d5cac h1:QOFyBVBICO1PTF+az3tv3OWvkFUT7bAbDFmhftVy7/E= -github.com/xlgmokha/x v0.0.0-20250724192332-f79ef71d5cac/go.mod h1:zhU3cB9VYGt4IlTIaLVw3wsXIS5UzXiYmCB+q+DjfHs= +github.com/xlgmokha/x v0.0.0-20250730165105-1a2af5f242cf h1:XXOAL/L880uxlbwOMAr6WMKCQzYyZfaYg4ovehJniY0= +github.com/xlgmokha/x v0.0.0-20250730165105-1a2af5f242cf/go.mod h1:zhU3cB9VYGt4IlTIaLVw3wsXIS5UzXiYmCB+q+DjfHs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go index 52b27b7..a8bf107 100644 --- a/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go +++ b/vendor/github.com/coreos/go-oidc/v3/oidc/verify.go @@ -1,15 +1,11 @@ package oidc import ( - "bytes" "context" - "encoding/base64" "encoding/json" - "errors" "fmt" "io" "net/http" - "strings" "time" jose "github.com/go-jose/go-jose/v4" @@ -145,18 +141,6 @@ func (p *Provider) newVerifier(keySet KeySet, config *Config) *IDTokenVerifier { return NewVerifier(p.issuer, keySet, config) } -func parseJWT(p string) ([]byte, error) { - parts := strings.Split(p, ".") - if len(parts) < 2 { - return nil, fmt.Errorf("oidc: malformed jwt, expected 3 parts got %d", len(parts)) - } - payload, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return nil, fmt.Errorf("oidc: malformed jwt payload: %v", err) - } - return payload, nil -} - func contains(sli []string, ele string) bool { for _, s := range sli { if s == ele { @@ -219,12 +203,49 @@ func resolveDistributedClaim(ctx context.Context, verifier *IDTokenVerifier, src // // token, err := verifier.Verify(ctx, rawIDToken) func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) { - // Throw out tokens with invalid claims before trying to verify the token. This lets - // us do cheap checks before possibly re-syncing keys. - payload, err := parseJWT(rawIDToken) + var supportedSigAlgs []jose.SignatureAlgorithm + for _, alg := range v.config.SupportedSigningAlgs { + supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg)) + } + if len(supportedSigAlgs) == 0 { + // If no algorithms were specified by both the config and discovery, default + // to the one mandatory algorithm "RS256". + supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256} + } + if v.config.InsecureSkipSignatureCheck { + // "none" is a required value to even parse a JWT with the "none" algorithm + // using go-jose. + supportedSigAlgs = append(supportedSigAlgs, "none") + } + + // Parse and verify the signature first. This at least forces the user to have + // a valid, signed ID token before we do any other processing. + jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs) if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %v", err) } + switch len(jws.Signatures) { + case 0: + return nil, fmt.Errorf("oidc: id token not signed") + case 1: + default: + return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") + } + sig := jws.Signatures[0] + + var payload []byte + if v.config.InsecureSkipSignatureCheck { + // Yolo mode. + payload = jws.UnsafePayloadWithoutVerification() + } else { + // The JWT is attached here for the happy path to avoid the verifier from + // having to parse the JWT twice. + ctx = context.WithValue(ctx, parsedJWTKey, jws) + payload, err = v.keySet.VerifySignature(ctx, rawIDToken) + if err != nil { + return nil, fmt.Errorf("failed to verify signature: %v", err) + } + } var token idToken if err := json.Unmarshal(payload, &token); err != nil { return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) @@ -254,6 +275,7 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok AccessTokenHash: token.AtHash, claims: payload, distributedClaims: distributedClaims, + sigAlgorithm: sig.Header.Algorithm, } // Check issuer. @@ -306,45 +328,6 @@ func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDTok } } - if v.config.InsecureSkipSignatureCheck { - return t, nil - } - - var supportedSigAlgs []jose.SignatureAlgorithm - for _, alg := range v.config.SupportedSigningAlgs { - supportedSigAlgs = append(supportedSigAlgs, jose.SignatureAlgorithm(alg)) - } - if len(supportedSigAlgs) == 0 { - // If no algorithms were specified by both the config and discovery, default - // to the one mandatory algorithm "RS256". - supportedSigAlgs = []jose.SignatureAlgorithm{jose.RS256} - } - jws, err := jose.ParseSigned(rawIDToken, supportedSigAlgs) - if err != nil { - return nil, fmt.Errorf("oidc: malformed jwt: %v", err) - } - - switch len(jws.Signatures) { - case 0: - return nil, fmt.Errorf("oidc: id token not signed") - case 1: - default: - return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") - } - sig := jws.Signatures[0] - t.sigAlgorithm = sig.Header.Algorithm - - ctx = context.WithValue(ctx, parsedJWTKey, jws) - gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken) - if err != nil { - return nil, fmt.Errorf("failed to verify signature: %v", err) - } - - // Ensure that the payload returned by the square actually matches the payload parsed earlier. - if !bytes.Equal(gotPayload, payload) { - return nil, errors.New("oidc: internal error, payload parsed did not match previous payload") - } - return t, nil } diff --git a/vendor/github.com/docker/docker/api/types/registry/authconfig.go b/vendor/github.com/docker/docker/api/types/registry/authconfig.go index 70f7320..fa9037b 100644 --- a/vendor/github.com/docker/docker/api/types/registry/authconfig.go +++ b/vendor/github.com/docker/docker/api/types/registry/authconfig.go @@ -83,6 +83,8 @@ func DecodeAuthConfig(authEncoded string) (*AuthConfig, error) { // Like [DecodeAuthConfig], this function always returns an [AuthConfig], even if an // error occurs. It is up to the caller to decide if authentication is required, // and if the error can be ignored. +// +// Deprecated: this function is no longer used and will be removed in the next release. func DecodeAuthConfigBody(rdr io.ReadCloser) (*AuthConfig, error) { return decodeAuthConfigFromReader(rdr) } diff --git a/vendor/github.com/docker/docker/client/image_push.go b/vendor/github.com/docker/docker/client/image_push.go index cbbe9a2..8dbe0b1 100644 --- a/vendor/github.com/docker/docker/client/image_push.go +++ b/vendor/github.com/docker/docker/client/image_push.go @@ -66,7 +66,16 @@ func (cli *Client) ImagePush(ctx context.Context, image string, options image.Pu } func (cli *Client) tryImagePush(ctx context.Context, imageID string, query url.Values, registryAuth string) (*http.Response, error) { - return cli.post(ctx, "/images/"+imageID+"/push", query, nil, http.Header{ + // Always send a body (which may be an empty JSON document ("{}")) to prevent + // EOF errors on older daemons which had faulty fallback code for handling + // authentication in the body when no auth-header was set, resulting in; + // + // Error response from daemon: bad parameters and missing X-Registry-Auth: invalid X-Registry-Auth header: EOF + // + // We use [http.NoBody], which gets marshaled to an empty JSON document. + // + // see: https://github.com/moby/moby/commit/ea29dffaa541289591aa44fa85d2a596ce860e16 + return cli.post(ctx, "/images/"+imageID+"/push", query, http.NoBody, http.Header{ registry.AuthHeader: {registryAuth}, }) } diff --git a/vendor/github.com/golang-jwt/jwt/v5/errors.go b/vendor/github.com/golang-jwt/jwt/v5/errors.go index 23bb616..14e0075 100644 --- a/vendor/github.com/golang-jwt/jwt/v5/errors.go +++ b/vendor/github.com/golang-jwt/jwt/v5/errors.go @@ -2,6 +2,7 @@ package jwt import ( "errors" + "fmt" "strings" ) @@ -47,3 +48,42 @@ func joinErrors(errs ...error) error { errs: errs, } } + +// Unwrap implements the multiple error unwrapping for this error type, which is +// possible in Go 1.20. +func (je joinedError) Unwrap() []error { + return je.errs +} + +// newError creates a new error message with a detailed error message. The +// message will be prefixed with the contents of the supplied error type. +// Additionally, more errors, that provide more context can be supplied which +// will be appended to the message. This makes use of Go 1.20's possibility to +// include more than one %w formatting directive in [fmt.Errorf]. +// +// For example, +// +// newError("no keyfunc was provided", ErrTokenUnverifiable) +// +// will produce the error string +// +// "token is unverifiable: no keyfunc was provided" +func newError(message string, err error, more ...error) error { + var format string + var args []any + if message != "" { + format = "%w: %s" + args = []any{err, message} + } else { + format = "%w" + args = []any{err} + } + + for _, e := range more { + format += ": %w" + args = append(args, e) + } + + err = fmt.Errorf(format, args...) + return err +} diff --git a/vendor/github.com/golang-jwt/jwt/v5/errors_go1_20.go b/vendor/github.com/golang-jwt/jwt/v5/errors_go1_20.go deleted file mode 100644 index a893d35..0000000 --- a/vendor/github.com/golang-jwt/jwt/v5/errors_go1_20.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build go1.20 -// +build go1.20 - -package jwt - -import ( - "fmt" -) - -// Unwrap implements the multiple error unwrapping for this error type, which is -// possible in Go 1.20. -func (je joinedError) Unwrap() []error { - return je.errs -} - -// newError creates a new error message with a detailed error message. The -// message will be prefixed with the contents of the supplied error type. -// Additionally, more errors, that provide more context can be supplied which -// will be appended to the message. This makes use of Go 1.20's possibility to -// include more than one %w formatting directive in [fmt.Errorf]. -// -// For example, -// -// newError("no keyfunc was provided", ErrTokenUnverifiable) -// -// will produce the error string -// -// "token is unverifiable: no keyfunc was provided" -func newError(message string, err error, more ...error) error { - var format string - var args []any - if message != "" { - format = "%w: %s" - args = []any{err, message} - } else { - format = "%w" - args = []any{err} - } - - for _, e := range more { - format += ": %w" - args = append(args, e) - } - - err = fmt.Errorf(format, args...) - return err -} diff --git a/vendor/github.com/golang-jwt/jwt/v5/errors_go_other.go b/vendor/github.com/golang-jwt/jwt/v5/errors_go_other.go deleted file mode 100644 index 2ad542f..0000000 --- a/vendor/github.com/golang-jwt/jwt/v5/errors_go_other.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build !go1.20 -// +build !go1.20 - -package jwt - -import ( - "errors" - "fmt" -) - -// Is implements checking for multiple errors using [errors.Is], since multiple -// error unwrapping is not possible in versions less than Go 1.20. -func (je joinedError) Is(err error) bool { - for _, e := range je.errs { - if errors.Is(e, err) { - return true - } - } - - return false -} - -// wrappedErrors is a workaround for wrapping multiple errors in environments -// where Go 1.20 is not available. It basically uses the already implemented -// functionality of joinedError to handle multiple errors with supplies a -// custom error message that is identical to the one we produce in Go 1.20 using -// multiple %w directives. -type wrappedErrors struct { - msg string - joinedError -} - -// Error returns the stored error string -func (we wrappedErrors) Error() string { - return we.msg -} - -// newError creates a new error message with a detailed error message. The -// message will be prefixed with the contents of the supplied error type. -// Additionally, more errors, that provide more context can be supplied which -// will be appended to the message. Since we cannot use of Go 1.20's possibility -// to include more than one %w formatting directive in [fmt.Errorf], we have to -// emulate that. -// -// For example, -// -// newError("no keyfunc was provided", ErrTokenUnverifiable) -// -// will produce the error string -// -// "token is unverifiable: no keyfunc was provided" -func newError(message string, err error, more ...error) error { - // We cannot wrap multiple errors here with %w, so we have to be a little - // bit creative. Basically, we are using %s instead of %w to produce the - // same error message and then throw the result into a custom error struct. - var format string - var args []any - if message != "" { - format = "%s: %s" - args = []any{err, message} - } else { - format = "%s" - args = []any{err} - } - errs := []error{err} - - for _, e := range more { - format += ": %s" - args = append(args, e) - errs = append(errs, e) - } - - err = &wrappedErrors{ - msg: fmt.Sprintf(format, args...), - joinedError: joinedError{errs: errs}, - } - return err -} diff --git a/vendor/github.com/golang-jwt/jwt/v5/rsa_pss.go b/vendor/github.com/golang-jwt/jwt/v5/rsa_pss.go index 7c216ae..f17590c 100644 --- a/vendor/github.com/golang-jwt/jwt/v5/rsa_pss.go +++ b/vendor/github.com/golang-jwt/jwt/v5/rsa_pss.go @@ -1,6 +1,3 @@ -//go:build go1.4 -// +build go1.4 - package jwt import ( diff --git a/vendor/github.com/xlgmokha/x/pkg/x/option.go b/vendor/github.com/xlgmokha/x/pkg/x/option.go index b0bf638..156e28c 100644 --- a/vendor/github.com/xlgmokha/x/pkg/x/option.go +++ b/vendor/github.com/xlgmokha/x/pkg/x/option.go @@ -5,7 +5,10 @@ type Option[T any] func(T) T type Factory[T any] func() T func New[T any](options ...Option[T]) T { - item := Default[T]() + return NewWith[T](Default[T](), options...) +} + +func NewWith[T any](item T, options ...Option[T]) T { for _, option := range options { item = option(item) } diff --git a/vendor/modules.txt b/vendor/modules.txt index b2fff48..e5a79bc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -343,7 +343,7 @@ github.com/containerd/log # github.com/containerd/platforms v0.2.1 ## explicit; go 1.20 github.com/containerd/platforms -# github.com/coreos/go-oidc/v3 v3.14.1 +# github.com/coreos/go-oidc/v3 v3.15.0 ## explicit; go 1.23.0 github.com/coreos/go-oidc/v3/oidc # github.com/cpuguy83/dockercfg v0.3.2 @@ -367,7 +367,7 @@ github.com/deckarep/golang-set/v2 # github.com/distribution/reference v0.6.0 ## explicit; go 1.20 github.com/distribution/reference -# github.com/docker/docker v28.3.2+incompatible +# github.com/docker/docker v28.3.3+incompatible ## explicit github.com/docker/docker/api github.com/docker/docker/api/types @@ -524,8 +524,8 @@ github.com/godbus/dbus # github.com/gogo/protobuf v1.3.2 ## explicit; go 1.15 github.com/gogo/protobuf/proto -# github.com/golang-jwt/jwt/v5 v5.2.3 -## explicit; go 1.18 +# github.com/golang-jwt/jwt/v5 v5.3.0 +## explicit; go 1.21 github.com/golang-jwt/jwt/v5 # github.com/golang/protobuf v1.5.4 ## explicit; go 1.17 @@ -932,7 +932,7 @@ github.com/xlab/treeprint # github.com/xlgmokha/minit v0.0.0-20250725204255-8e0834741617 ## explicit; go 1.24 github.com/xlgmokha/minit -# github.com/xlgmokha/x v0.0.0-20250724192332-f79ef71d5cac +# github.com/xlgmokha/x v0.0.0-20250730165105-1a2af5f242cf ## explicit; go 1.24 github.com/xlgmokha/x/pkg/context github.com/xlgmokha/x/pkg/convert -- cgit v1.2.3 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 From 2c4c6cb7fe868229ba3c3a5eddc80d5581574b19 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 30 Jul 2025 11:52:04 -0600 Subject: fix: return a global id for an anonymous user --- app/db/authorization.go | 9 --------- app/domain/user.go | 4 ++++ 2 files changed, 4 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/db/authorization.go b/app/db/authorization.go index 3041f51..ec8e47b 100644 --- a/app/db/authorization.go +++ b/app/db/authorization.go @@ -108,14 +108,5 @@ func (r *authorization[T]) Save(ctx context.Context, item T) error { 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/domain/user.go b/app/domain/user.go index 198fafc..97bacdd 100644 --- a/app/domain/user.go +++ b/app/domain/user.go @@ -2,6 +2,7 @@ package domain import ( v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + "github.com/xlgmokha/x/pkg/x" ) type User struct { @@ -33,6 +34,9 @@ func (self *User) Sparkle(sparklee string, reason string) *Sparkle { } func (self *User) ToGID() GlobalID { + if x.IsZero(self.Username) { + return GlobalID("gid://sparkle/User/*") + } return GlobalID("gid://sparkle/User/" + self.Username) } -- cgit v1.2.3 From d4f4f72b5ff586f2ff440f19f61d2fe48be89a37 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 30 Jul 2025 12:04:12 -0600 Subject: refactor: extract Boot() function to kickstart background job queue API --- app/init.go | 9 ++++----- app/jobs/init.go | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 app/jobs/init.go (limited to 'app') diff --git a/app/init.go b/app/init.go index 398c8a9..8e5e0e5 100644 --- a/app/init.go +++ b/app/init.go @@ -19,6 +19,7 @@ import ( "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/jobs" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/authz" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/pls" "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/pkg/web" ) @@ -89,9 +90,7 @@ func init() { logger := ioc.MustResolve[*zerolog.Logger](c) ctx := logger.WithContext(context.Background()) - client := ioc.MustResolve[*authzed.Client](c) - - ioc. - MustResolve[*event.TypedAggregator[*domain.Sparkle]](c). - SubscribeTo("after.create", jobs.NewCreateSparkle(ctx, client).Run) + if err := jobs.Boot(ctx, c); err != nil { + pls.LogErrorNow(ctx, err) + } } diff --git a/app/jobs/init.go b/app/jobs/init.go new file mode 100644 index 0000000..abc7dd1 --- /dev/null +++ b/app/jobs/init.go @@ -0,0 +1,25 @@ +package jobs + +import ( + "context" + + "github.com/authzed/authzed-go/v1" + "github.com/golobby/container/v3" + "github.com/xlgmokha/x/pkg/event" + "github.com/xlgmokha/x/pkg/ioc" + "gitlab.com/gitlab-org/software-supply-chain-security/authorization/sparkled/app/domain" +) + +func Boot(ctx context.Context, c container.Container) error { + client, err := ioc.Resolve[*authzed.Client](c) + if err != nil { + return err + } + aggregator, err := ioc.Resolve[*event.TypedAggregator[*domain.Sparkle]](c) + if err != nil { + return err + } + + aggregator.SubscribeTo("after.create", NewCreateSparkle(ctx, client).Run) + return nil +} -- cgit v1.2.3