From aed4022f7eca0b773d89449d192a813bc5fd8ebf Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 9 Oct 2026 09:25:35 +0200 Subject: [PATCH 1/2] feat(controlplane): add the organization name to Sentry errors Tag each control plane Sentry event with org.id and org.name so the organization of an error is searchable without an ID lookup. The values come from the org already loaded in the request context, and the tags are removed for requests without an org so they do not carry over from an earlier request on the shared scope. Tags do not take part in issue grouping. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: c171815c-1b90-43d7-b82c-f240bd634e3d --- .../internal/sentrycontext/sentry_context.go | 31 +++++++++++--- .../sentrycontext/sentry_context_test.go | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/app/controlplane/internal/sentrycontext/sentry_context.go b/app/controlplane/internal/sentrycontext/sentry_context.go index 402b552dc..a3a27c25a 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context.go +++ b/app/controlplane/internal/sentrycontext/sentry_context.go @@ -33,6 +33,12 @@ import ( // defaultTracingHeaders is a list of headers that are used to extract tracing information. var defaultTracingHeaders = []string{"X-Request-ID", "X-Correlation-ID", "X-Trace-ID"} +// Sentry tags that identify the organization of the current request +const ( + tagOrgID = "org.id" + tagOrgName = "org.name" +) + // NewSentryContext returns a middleware that adds context to Sentry for the current request // that will be sent along with the error if one occurs func NewSentryContext() middleware.Middleware { @@ -46,16 +52,31 @@ func NewSentryContext() middleware.Middleware { // addSentryContext adds context to Sentry for the current request func addSentryContext(ctx context.Context, req interface{}) { + // ConfigureScope allows to set context that will be sent along with the error if one occurs + sentry.ConfigureScope(func(scope *sentry.Scope) { + configureScope(ctx, scope, req) + }) +} + +// configureScope sets the account and request information of the current request on the scope +func configureScope(ctx context.Context, scope *sentry.Scope, req interface{}) { org := entities.CurrentOrg(ctx) user := entities.CurrentUser(ctx) apiToken := entities.CurrentAPIToken(ctx) role := usercontext.CurrentAuthzSubject(ctx) - // ConfigureScope allows to set context that will be sent along with the error if one occurs - sentry.ConfigureScope(func(scope *sentry.Scope) { - scope.SetContext("Account", buildAuthContext(user, apiToken, org, role)) - scope.SetContext("Request", buildRequestContext(ctx, req)) - }) + scope.SetContext("Account", buildAuthContext(user, apiToken, org, role)) + scope.SetContext("Request", buildRequestContext(ctx, req)) + + // Tags make the organization searchable in Sentry. They do not take part in issue grouping. + // The scope outlives the request, so remove the tags of a previous request when there is no org. + if org != nil { + scope.SetTag(tagOrgID, org.ID) + scope.SetTag(tagOrgName, org.Name) + } else { + scope.RemoveTag(tagOrgID) + scope.RemoveTag(tagOrgName) + } } // buildAuthContext creates a map of the user and membership information diff --git a/app/controlplane/internal/sentrycontext/sentry_context_test.go b/app/controlplane/internal/sentrycontext/sentry_context_test.go index 8c78cf82f..a8b606910 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context_test.go +++ b/app/controlplane/internal/sentrycontext/sentry_context_test.go @@ -21,6 +21,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/usercontext/entities" + "github.com/getsentry/sentry-go" "github.com/go-kratos/kratos/v2/transport" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/trace" @@ -65,6 +66,45 @@ func TestNewSentryContext(t *testing.T) { assert.NoError(t, err) } +func TestConfigureScopeOrgTags(t *testing.T) { + org := &entities.Org{ID: "org-id", Name: "my-org"} + withOrg := entities.WithCurrentOrg(context.Background(), org) + + testCases := []struct { + name string + requests []context.Context + wantTags map[string]string + }{ + { + name: "request with an org", + requests: []context.Context{withOrg}, + wantTags: map[string]string{"org.id": "org-id", "org.name": "my-org"}, + }, + { + name: "request without an org", + requests: []context.Context{context.Background()}, + wantTags: map[string]string{}, + }, + { + name: "request without an org clears the tags of a previous request", + requests: []context.Context{withOrg, context.Background()}, + wantTags: map[string]string{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + scope := sentry.NewScope() + for _, ctx := range tc.requests { + configureScope(ctx, scope, "request") + } + + event := scope.ApplyToEvent(sentry.NewEvent(), nil, nil) + assert.Equal(t, tc.wantTags, event.Tags) + }) + } +} + func TestBuildAuthContext(t *testing.T) { org := &entities.Org{ID: "org1", Name: "OrgName"} user := &entities.User{ID: "user1"} From 3a5fe4132702a1a6ddd6844283b0224031df246d Mon Sep 17 00:00:00 2001 From: Miguel Martinez Trivino Date: Fri, 9 Oct 2026 09:48:21 +0200 Subject: [PATCH 2/2] fix(controlplane): report Sentry errors on a hub of their own request The Sentry context middleware wrote the account, request and org tags to the global scope, so concurrent requests could overwrite each other's values, and events from outside a request could carry the values of the last request. Give each gRPC request its own hub on its context, ahead of the recovery middleware, and set the request scope on that hub. LogAndMaskErr and handleUseCaseErr now take the request context and capture on its hub, falling back to the global hub when the context has none. The capture site does not change, so the stack traces and issue grouping stay the same. Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino Chainloop-Trace-Sessions: c171815c-1b90-43d7-b82c-f240bd634e3d --- .../internal/service/bytestream.go | 18 ++-- app/artifact-cas/internal/service/download.go | 13 +-- app/artifact-cas/internal/service/resource.go | 4 +- .../internal/sentrycontext/sentry_context.go | 33 ++++--- .../sentrycontext/sentry_context_test.go | 95 +++++++++++++++---- app/controlplane/internal/server/grpc.go | 9 +- app/controlplane/internal/service/apitoken.go | 8 +- .../internal/service/attestation.go | 68 ++++++------- .../internal/service/attestationstate.go | 16 ++-- app/controlplane/internal/service/auth.go | 16 ++-- .../internal/service/casbackend.go | 16 ++-- .../internal/service/cascredential.go | 12 +-- .../internal/service/casredirect.go | 8 +- app/controlplane/internal/service/context.go | 8 +- app/controlplane/internal/service/group.go | 36 +++---- .../internal/service/integration.go | 34 +++---- .../internal/service/organization.go | 22 ++--- .../internal/service/orginvitation.go | 8 +- .../internal/service/orgmetric.go | 12 +-- app/controlplane/internal/service/project.go | 46 ++++----- app/controlplane/internal/service/referrer.go | 2 +- app/controlplane/internal/service/service.go | 14 +-- .../internal/service/service_test.go | 8 +- app/controlplane/internal/service/signing.go | 6 +- app/controlplane/internal/service/user.go | 8 +- app/controlplane/internal/service/workflow.go | 22 ++--- .../internal/service/workflowcontract.go | 34 +++---- .../internal/service/workflowrun.go | 20 ++-- pkg/servicelogger/logger.go | 20 +++- pkg/servicelogger/logger_test.go | 85 +++++++++++++++++ 30 files changed, 433 insertions(+), 268 deletions(-) create mode 100644 pkg/servicelogger/logger_test.go diff --git a/app/artifact-cas/internal/service/bytestream.go b/app/artifact-cas/internal/service/bytestream.go index f2d40576d..01d0d42c4 100644 --- a/app/artifact-cas/internal/service/bytestream.go +++ b/app/artifact-cas/internal/service/bytestream.go @@ -138,7 +138,7 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro if backendErr, ok := errors.AsType[*backendUploadError](err); ok { // the reused client may hold stale credentials s.dropUploadBackend(info) - return sl.LogAndMaskErr(backendErr.err, s.log) + return sl.LogAndMaskErr(ctx, backendErr.err, s.log) } if isClientDisconnect(err) { s.log.Infow("msg", "upload canceled", "digest", req.resource.Digest, "name", req.resource.FileName) @@ -147,7 +147,7 @@ func (s *ByteStreamService) Write(stream bytestream.ByteStream_WriteServer) erro if backend.IsUploadSizeExceeded(err) { return status.Error(codes.ResourceExhausted, err.Error()) } - return sl.LogAndMaskErr(err, s.log) + return sl.LogAndMaskErr(ctx, err, s.log) } s.log.Infow("msg", "upload finished", "name", req.resource.FileName, "digest", req.resource.Digest, "size", committedSize) @@ -174,7 +174,7 @@ func (s *ByteStreamService) backendExists(ctx context.Context, info *casJWT.Clai if err != nil && kerrors.IsNotFound(err) { return nil, false, err } else if err != nil { - return nil, false, sl.LogAndMaskErr(err, s.log) + return nil, false, sl.LogAndMaskErr(ctx, err, s.log) } exists, err := storageBackend.Exists(ctx, digest) @@ -184,7 +184,7 @@ func (s *ByteStreamService) backendExists(ctx context.Context, info *casJWT.Clai s.dropUploadBackend(info) if !cached || attempt > 0 { - return nil, false, sl.LogAndMaskErr(err, s.log) + return nil, false, sl.LogAndMaskErr(ctx, err, s.log) } s.log.Warnw("msg", "reused backend client failed, loading it again", "digest", digest, "error", err) } @@ -331,7 +331,7 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream. if err != nil && kerrors.IsNotFound(err) { return err } else if err != nil { - return sl.LogAndMaskErr(err, s.log) + return sl.LogAndMaskErr(ctx, err, s.log) } // Stage the backend egress on local disk and verify it against the requested @@ -340,12 +340,12 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream. // because the artifact lives on disk. f, size, err := s.stageDownload(ctx, backend, req.ResourceName) if err != nil { - return s.downloadError(err, req.ResourceName) + return s.downloadError(ctx, err, req.ResourceName) } defer s.closeStagingFile(f) if err := copyStaged(sendWriter{stream}, f); err != nil { - return s.downloadError(err, req.ResourceName) + return s.downloadError(ctx, err, req.ResourceName) } s.log.Infow("msg", "download finished", "digest", req.ResourceName, "size", size) @@ -365,7 +365,7 @@ func (s *ByteStreamService) Read(req *bytestream.ReadRequest, stream bytestream. // key, corrupt or tampered, so it is reported as DataLoss with both digests // rather than blamed on the caller. A client that went away is not an error. // Anything else is masked. -func (s *ByteStreamService) downloadError(err error, digest string) error { +func (s *ByteStreamService) downloadError(ctx context.Context, err error, digest string) error { if _, ok := errors.AsType[*digestMismatchError](err); ok { return status.Error(codes.DataLoss, err.Error()) } @@ -374,7 +374,7 @@ func (s *ByteStreamService) downloadError(err error, digest string) error { return nil } - return sl.LogAndMaskErr(err, s.log) + return sl.LogAndMaskErr(ctx, err, s.log) } // checkUploadSize returns an ErrUploadSizeExceeded when total exceeds maxSize. diff --git a/app/artifact-cas/internal/service/download.go b/app/artifact-cas/internal/service/download.go index 85b9bcf24..5afa71434 100644 --- a/app/artifact-cas/internal/service/download.go +++ b/app/artifact-cas/internal/service/download.go @@ -16,6 +16,7 @@ package service import ( + "context" "errors" "fmt" "io" @@ -82,7 +83,7 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "backend not found", http.StatusNotFound) return } else if err != nil { - http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) + http.Error(w, sl.LogAndMaskErr(ctx, err, s.log).Error(), http.StatusInternalServerError) return } @@ -91,7 +92,7 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "artifact not found", http.StatusNotFound) return } else if err != nil { - http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) + http.Error(w, sl.LogAndMaskErr(ctx, err, s.log).Error(), http.StatusInternalServerError) return } @@ -118,7 +119,7 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { // memory stays bounded because the artifact lives on disk, not in a buffer. f, size, err := s.stageDownload(ctx, b, wantChecksum.Hex) if err != nil { - s.writeDownloadError(w, err, wantChecksum.Hex) + s.writeDownloadError(ctx, w, err, wantChecksum.Hex) return } defer s.closeStagingFile(f) @@ -131,7 +132,7 @@ func (s *DownloadService) ServeHTTP(w http.ResponseWriter, r *http.Request) { // A plain io.Copy lets the response writer pull the file with sendfile, so // the verified bytes go kernel-to-kernel without a user-space buffer. if _, err := io.Copy(w, f); err != nil { - s.writeDownloadError(w, err, wantChecksum.Hex) + s.writeDownloadError(ctx, w, err, wantChecksum.Hex) return } @@ -171,7 +172,7 @@ func etagMatches(ifNoneMatch, etag string) bool { // to its key, corrupt or tampered, so it is a 500 carrying both digests rather // than blamed on the caller. A client that went away gets nothing. Anything // else is masked. -func (s *DownloadService) writeDownloadError(w http.ResponseWriter, err error, digest string) { +func (s *DownloadService) writeDownloadError(ctx context.Context, w http.ResponseWriter, err error, digest string) { if _, ok := errors.AsType[*digestMismatchError](err); ok { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -181,5 +182,5 @@ func (s *DownloadService) writeDownloadError(w http.ResponseWriter, err error, d return } - http.Error(w, sl.LogAndMaskErr(err, s.log).Error(), http.StatusInternalServerError) + http.Error(w, sl.LogAndMaskErr(ctx, err, s.log).Error(), http.StatusInternalServerError) } diff --git a/app/artifact-cas/internal/service/resource.go b/app/artifact-cas/internal/service/resource.go index 6360d307d..e8defd0dd 100644 --- a/app/artifact-cas/internal/service/resource.go +++ b/app/artifact-cas/internal/service/resource.go @@ -53,14 +53,14 @@ func (s *ResourceService) Describe(ctx context.Context, req *v1.ResourceServiceD if err != nil && errors.IsNotFound(err) { return nil, err } else if err != nil { - return nil, sl.LogAndMaskErr(err, s.log) + return nil, sl.LogAndMaskErr(ctx, err, s.log) } res, err := b.Describe(ctx, req.Digest) if err != nil && backend.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } else if err != nil { - return nil, sl.LogAndMaskErr(err, s.log) + return nil, sl.LogAndMaskErr(ctx, err, s.log) } return &v1.ResourceServiceDescribeResponse{ diff --git a/app/controlplane/internal/sentrycontext/sentry_context.go b/app/controlplane/internal/sentrycontext/sentry_context.go index a3a27c25a..cf0e08c48 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context.go +++ b/app/controlplane/internal/sentrycontext/sentry_context.go @@ -39,25 +39,34 @@ const ( tagOrgName = "org.name" ) +// NewSentryHub returns a middleware that gives each request its own Sentry hub on its context, +// so concurrent requests do not share a scope. Put it before any middleware that reports to Sentry. +func NewSentryHub() middleware.Middleware { + return func(handler middleware.Handler) middleware.Handler { + return func(ctx context.Context, req interface{}) (interface{}, error) { + return handler(sentry.SetHubOnContext(ctx, sentry.CurrentHub().Clone()), req) + } + } +} + // NewSentryContext returns a middleware that adds context to Sentry for the current request -// that will be sent along with the error if one occurs +// that will be sent along with the error if one occurs. +// It sets the scope of the request hub that NewSentryHub added, and adds that hub if it is missing. func NewSentryContext() middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { - addSentryContext(ctx, req) + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + hub = sentry.CurrentHub().Clone() + ctx = sentry.SetHubOnContext(ctx, hub) + } + + configureScope(ctx, hub.Scope(), req) return handler(ctx, req) } } } -// addSentryContext adds context to Sentry for the current request -func addSentryContext(ctx context.Context, req interface{}) { - // ConfigureScope allows to set context that will be sent along with the error if one occurs - sentry.ConfigureScope(func(scope *sentry.Scope) { - configureScope(ctx, scope, req) - }) -} - // configureScope sets the account and request information of the current request on the scope func configureScope(ctx context.Context, scope *sentry.Scope, req interface{}) { org := entities.CurrentOrg(ctx) @@ -69,13 +78,9 @@ func configureScope(ctx context.Context, scope *sentry.Scope, req interface{}) { scope.SetContext("Request", buildRequestContext(ctx, req)) // Tags make the organization searchable in Sentry. They do not take part in issue grouping. - // The scope outlives the request, so remove the tags of a previous request when there is no org. if org != nil { scope.SetTag(tagOrgID, org.ID) scope.SetTag(tagOrgName, org.Name) - } else { - scope.RemoveTag(tagOrgID) - scope.RemoveTag(tagOrgName) } } diff --git a/app/controlplane/internal/sentrycontext/sentry_context_test.go b/app/controlplane/internal/sentrycontext/sentry_context_test.go index a8b606910..e6afeb0c7 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context_test.go +++ b/app/controlplane/internal/sentrycontext/sentry_context_test.go @@ -22,8 +22,10 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/usercontext/entities" "github.com/getsentry/sentry-go" + "github.com/go-kratos/kratos/v2/middleware" "github.com/go-kratos/kratos/v2/transport" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" "google.golang.org/grpc/metadata" ) @@ -66,45 +68,100 @@ func TestNewSentryContext(t *testing.T) { assert.NoError(t, err) } -func TestConfigureScopeOrgTags(t *testing.T) { +// scopeTags returns the tags that the scope of the hub in ctx adds to an event +func scopeTags(ctx context.Context) map[string]string { + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + return nil + } + + return hub.Scope().ApplyToEvent(sentry.NewEvent(), nil, nil).Tags +} + +func TestNewSentryContextOrgTags(t *testing.T) { org := &entities.Org{ID: "org-id", Name: "my-org"} - withOrg := entities.WithCurrentOrg(context.Background(), org) + orgTags := map[string]string{tagOrgID: "org-id", tagOrgName: "my-org"} testCases := []struct { - name string - requests []context.Context - wantTags map[string]string + name string + middlewares []middleware.Middleware + org *entities.Org + wantTags map[string]string }{ { - name: "request with an org", - requests: []context.Context{withOrg}, - wantTags: map[string]string{"org.id": "org-id", "org.name": "my-org"}, + name: "request with an org", + middlewares: []middleware.Middleware{NewSentryHub(), NewSentryContext()}, + org: org, + wantTags: orgTags, }, { - name: "request without an org", - requests: []context.Context{context.Background()}, - wantTags: map[string]string{}, + name: "request without an org", + middlewares: []middleware.Middleware{NewSentryHub(), NewSentryContext()}, + wantTags: map[string]string{}, }, { - name: "request without an org clears the tags of a previous request", - requests: []context.Context{withOrg, context.Background()}, - wantTags: map[string]string{}, + name: "request without a hub from NewSentryHub", + middlewares: []middleware.Middleware{NewSentryContext()}, + org: org, + wantTags: orgTags, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - scope := sentry.NewScope() - for _, ctx := range tc.requests { - configureScope(ctx, scope, "request") + ctx := context.Background() + if tc.org != nil { + ctx = entities.WithCurrentOrg(ctx, tc.org) + } + + var gotTags map[string]string + handler := func(ctx context.Context, _ interface{}) (interface{}, error) { + gotTags = scopeTags(ctx) + return nil, nil } - event := scope.ApplyToEvent(sentry.NewEvent(), nil, nil) - assert.Equal(t, tc.wantTags, event.Tags) + _, err := middleware.Chain(tc.middlewares...)(handler)(ctx, "request") + require.NoError(t, err) + assert.Equal(t, tc.wantTags, gotTags) + assert.Empty(t, scopeTags(sentry.SetHubOnContext(context.Background(), sentry.CurrentHub())), "the global scope must not change") }) } } +// Two requests of different orgs overlap: the first one is still running when the second one +// sets its scope. Each request must keep the tags of its own org. +func TestNewSentryContextConcurrentRequests(t *testing.T) { + chain := middleware.Chain(NewSentryHub(), NewSentryContext()) + orgA := &entities.Org{ID: "org-a", Name: "org-a-name"} + orgB := &entities.Org{ID: "org-b", Name: "org-b-name"} + + started, resume := make(chan struct{}), make(chan struct{}) + tagsA := make(chan map[string]string, 1) + errA := make(chan error, 1) + go func() { + _, err := chain(func(ctx context.Context, _ interface{}) (interface{}, error) { + close(started) + <-resume + tagsA <- scopeTags(ctx) + return nil, nil + })(entities.WithCurrentOrg(context.Background(), orgA), "request-a") + errA <- err + }() + + <-started + var tagsB map[string]string + _, err := chain(func(ctx context.Context, _ interface{}) (interface{}, error) { + tagsB = scopeTags(ctx) + return nil, nil + })(entities.WithCurrentOrg(context.Background(), orgB), "request-b") + require.NoError(t, err) + close(resume) + require.NoError(t, <-errA) + + assert.Equal(t, map[string]string{tagOrgID: "org-a", tagOrgName: "org-a-name"}, <-tagsA) + assert.Equal(t, map[string]string{tagOrgID: "org-b", tagOrgName: "org-b-name"}, tagsB) +} + func TestBuildAuthContext(t *testing.T) { org := &entities.Org{ID: "org1", Name: "OrgName"} user := &entities.User{ID: "user1"} diff --git a/app/controlplane/internal/server/grpc.go b/app/controlplane/internal/server/grpc.go index ede33825f..faca055b0 100644 --- a/app/controlplane/internal/server/grpc.go +++ b/app/controlplane/internal/server/grpc.go @@ -35,7 +35,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/internal/service" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext" "github.com/chainloop-dev/chainloop/pkg/credentials" - "github.com/getsentry/sentry-go" + "github.com/chainloop-dev/chainloop/pkg/servicelogger" "github.com/golang-jwt/jwt/v5" "github.com/go-kratos/kratos/v2/errors" @@ -187,9 +187,12 @@ func NewGRPCServer(opts *Opts) (*grpc.Server, error) { func craftMiddleware(opts *Opts) []middleware.Middleware { middlewares := []middleware.Middleware{ + // Give each request its own Sentry hub before anything can report to Sentry + sentrycontext.NewSentryHub(), recovery.Recovery( - recovery.WithHandler(func(_ context.Context, req, err interface{}) error { - sentry.CaptureMessage(fmt.Sprintf("%v", err)) + recovery.WithHandler(func(ctx context.Context, _, err interface{}) error { + // The hub is set by NewSentryHub, so the panic carries the scope of its request + servicelogger.SentryHub(ctx).CaptureMessage(fmt.Sprintf("%v", err)) return errors.InternalServer("internal error", "there was an internal error") }), ), diff --git a/app/controlplane/internal/service/apitoken.go b/app/controlplane/internal/service/apitoken.go index 66e9dc4bc..f0857ac03 100644 --- a/app/controlplane/internal/service/apitoken.go +++ b/app/controlplane/internal/service/apitoken.go @@ -79,7 +79,7 @@ func (s *APITokenService) Create(ctx context.Context, req *pb.APITokenServiceCre token, err := s.APITokenUseCase.Create(ctx, req.Name, req.Description, expiresIn, ¤tOrg.ID, biz.APITokenWithProject(project)) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.APITokenServiceCreateResponse{ @@ -117,7 +117,7 @@ func (s *APITokenService) List(ctx context.Context, req *pb.APITokenServiceListR tokens, err := s.APITokenUseCase.List(ctx, currentOrg.ID, biz.WithAPITokenStatusFilter(mapTokenStatusFilter(req.GetStatusFilter())), biz.WithAPITokenProjectFilter(defaultProjectFilter), biz.WithAPITokenScope(scope)) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.APITokenItem, 0, len(tokens)) @@ -158,7 +158,7 @@ func (s *APITokenService) Revoke(ctx context.Context, req *pb.APITokenServiceRev t, err := s.APITokenUseCase.FindByIDInOrg(ctx, currentOrg.ID, req.GetId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // System tokens are internal and must not be reachable through the public API. @@ -195,7 +195,7 @@ func (s *APITokenService) Revoke(ctx context.Context, req *pb.APITokenServiceRev } if err := s.APITokenUseCase.Revoke(ctx, currentOrg.ID, t.ID.String()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.APITokenServiceRevokeResponse{}, nil diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index d422ad552..f76bedea5 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -129,7 +129,7 @@ func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.Attesta wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, req.GetProjectName(), req.GetWorkflowName(), "") if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -140,7 +140,7 @@ func (s *AttestationService) GetContract(ctx context.Context, req *cpAPI.Attesta // Find contract revision contractVersion, err := s.workflowContractUseCase.Describe(ctx, wf.OrgID.String(), wf.ContractID.String(), int(req.ContractRevision), biz.WithoutReferences()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if contractVersion == nil { return nil, errors.NotFound("not found", "contract not found") } @@ -167,7 +167,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer org, err := s.orgUseCase.FindByID(ctx, robotAccount.OrgID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if err := checkAuthRequirements(robotAccount, req.GetWorkflowName()); err != nil { @@ -176,7 +176,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, req.GetProjectName(), req.GetWorkflowName(), "") if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -198,7 +198,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer } else if biz.IsErrValidation(err) { return nil, cpAPI.ErrorCasBackendErrorReasonInvalid("CAS backend is unreachable or misconfigured: %s", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Create workflowRun @@ -217,7 +217,7 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer run, err := s.wrUseCase.Create(ctx, opts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } wRun := bizWorkFlowRunToPb(run) @@ -261,7 +261,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe // This will make sure the provided workflowRunID belongs to the org encoded in the robot account wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -271,7 +271,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe wRun, err := s.wrUseCase.GetByIDInOrg(ctx, robotAccount.OrgID, req.WorkflowRunId) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if wRun == nil { return nil, errors.NotFound("not found", "workflow run not found") } @@ -282,7 +282,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe digest, err := s.storeAttestation(ctx, bundle, robotAccount, wf, wRun, req.MarkVersionAsReleased) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationServiceStoreResponse{ @@ -317,7 +317,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte // extract structured envelope for integrations dsseEnv, err := attestation.DSSEEnvelopeFromBundleBytes(bundle) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Inline backends have no external CAS to fall back to, so the bundle @@ -334,7 +334,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte if skipDB { digestHash, _, hashErr := v1.SHA256(bytes.NewReader(bundle)) if hashErr != nil { - return nil, handleUseCaseErr(hashErr, s.log) + return nil, handleUseCaseErr(ctx, hashErr, s.log) } // On this path the bundle reaches CAS before SaveAttestation gets to @@ -342,21 +342,21 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte // attestation rejected for violating its contract would still have left // a blob behind in the CAS backend. if err = s.wrUseCase.ValidateAttestationContract(ctx, workflowRunID, bundle); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if err = s.uploadAttestationToCASWithRetry(ctx, bundle, casBackend, workflowRunID, digestHash); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } digest, err = s.wrUseCase.SaveAttestation(ctx, workflowRunID, bundle, biz.WithSkipBundlePersistence()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } else { digest, err = s.wrUseCase.SaveAttestation(ctx, workflowRunID, bundle) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if !casBackend.Inline { @@ -364,7 +364,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte dgst := *digest panicguard.Go(s.log, "attestation-cas-upload", func() { if err := s.uploadAttestationToCASWithRetry(context.Background(), bundle, casBackend, workflowRunID, dgst); err != nil { - _ = handleUseCaseErr(err, s.log) + _ = handleUseCaseErr(ctx, err, s.log) } }) } @@ -372,14 +372,14 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte // Store the exploded attestation referrer information in the DB if err := s.referrerUseCase.ExtractAndPersist(ctx, dsseEnv, *digest, wf.ID.String()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if !casBackend.Inline { // Store the mappings in the DB references, err := s.casMappingUseCase.LookupDigestsInAttestation(dsseEnv, *digest) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } for _, ref := range references { @@ -388,7 +388,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte WorkflowRunID: &wfRun.ID, ProjectID: &wf.ProjectID, }); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } } @@ -403,7 +403,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte DownloadSecretName: secretName, WorkflowRunID: workflowRunID, }); err != nil { - _ = handleUseCaseErr(err, s.log) + _ = handleUseCaseErr(ctx, err, s.log) } }) @@ -411,12 +411,12 @@ func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte if markAsReleased != nil && *markAsReleased { // Update the project version to mark it as a release if _, err := s.projectVersionUseCase.UpdateReleaseStatus(ctx, wfRun.ProjectVersion.ID.String(), true); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } if err := s.wrUseCase.MarkAsFinished(ctx, workflowRunID, biz.WorkflowRunSuccess, ""); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Record the attestation in the prometheus registry @@ -434,7 +434,7 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS // This will make sure the provided workflowRunID belongs to the org encoded in the robot account wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -458,7 +458,7 @@ func (s *AttestationService) Cancel(ctx context.Context, req *cpAPI.AttestationS wRun, err := s.wrUseCase.GetByIDInOrg(ctx, robotAccount.OrgID, req.WorkflowRunId) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if wRun == nil { return nil, errors.NotFound("not found", "workflow run not found") } @@ -482,7 +482,7 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte // This is the new mode, where the CAS backend ref is stored in the workflow run since initialization wRun, err := s.wrUseCase.GetByIDInOrg(ctx, robotAccount.OrgID, req.WorkflowRunId) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if wRun == nil { return nil, errors.NotFound("not found", "workflow run not found") } @@ -515,7 +515,7 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, req *cpAPI.Atte ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Uploader, MaxBytes: backend.Limits.MaxBytes, OrgID: backend.OrganizationID} t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } resp.Token = t @@ -537,7 +537,7 @@ func (s *AttestationService) GetPolicy(ctx context.Context, req *cpAPI.Attestati remotePolicy, err := s.workflowContractUseCase.GetPolicy(ctx, req.GetProvider(), req.GetPolicyName(), req.GetOrgName(), org.Name, token.Token) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationServiceGetPolicyResponse{Policy: remotePolicy.Policy, Reference: &cpAPI.RemotePolicyReference{ @@ -559,7 +559,7 @@ func (s *AttestationService) GetPolicyGroup(ctx context.Context, req *cpAPI.Atte remoteGroup, err := s.workflowContractUseCase.GetPolicyGroup(ctx, req.GetProvider(), req.GetGroupName(), req.GetOrgName(), org.Name, token.Token) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationServiceGetPolicyGroupResponse{Group: remoteGroup.PolicyGroup, Reference: &cpAPI.RemotePolicyReference{ @@ -810,14 +810,14 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP // contract validation if req.GetContractBytes() != nil { if err = s.workflowContractUseCase.ValidateContractPolicies(ctx, req.GetContractBytes(), token, nil, nil); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } // Check if the workflow already exists, if it does we might just need to update the contract if wf, err := s.workflowUseCase.FindByNameInOrg(ctx, apiToken.OrgID, req.GetProjectName(), req.GetWorkflowName()); err != nil { if !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } else if wf != nil { // We might need to update the contract @@ -825,7 +825,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP if _, err := s.workflowContractUseCase.Update(ctx, apiToken.OrgID, wf.ContractName, &biz.WorkflowContractUpdateOpts{ RawSchema: req.GetContractBytes(), }); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Check if the contract name the user wants to use is the same as the one in the workflow } else if req.GetContractName() != "" && req.GetContractName() != wf.ContractName { @@ -838,7 +838,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP // Get organization org, err := s.orgUseCase.FindByID(ctx, apiToken.OrgID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // the workflow does not exist, let's create it alongside its project and contract @@ -857,7 +857,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP if user != nil { userID, err := uuid.Parse(user.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } createOpts.Owner = &userID @@ -871,7 +871,7 @@ func (s *AttestationService) FindOrCreateWorkflow(ctx context.Context, req *cpAP return nil, errors.Forbidden("forbidden", "creating workflows during the attestation process is disabled for this organization. Please create them in advance or contact your administrator") } - return nil, handleUseCaseErr(fmt.Errorf("failed to initialize the attestation: %w", err), s.log) + return nil, handleUseCaseErr(ctx, fmt.Errorf("failed to initialize the attestation: %w", err), s.log) } // reset RBAC cache, since we might have created a new project diff --git a/app/controlplane/internal/service/attestationstate.go b/app/controlplane/internal/service/attestationstate.go index 3aaa52f0f..ebcffe7b2 100644 --- a/app/controlplane/internal/service/attestationstate.go +++ b/app/controlplane/internal/service/attestationstate.go @@ -65,7 +65,7 @@ func (s *AttestationStateService) Initialized(ctx context.Context, req *cpAPI.At wf, err := s.findWorkflowFromTokenOrRunID(ctx, robotAccount.OrgID, robotAccount.WorkflowID, req.GetWorkflowRunId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -75,7 +75,7 @@ func (s *AttestationStateService) Initialized(ctx context.Context, req *cpAPI.At initialized, err := s.attestationStateUseCase.Initialized(ctx, wf.ID.String(), req.WorkflowRunId) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationStateServiceInitializedResponse{ @@ -92,7 +92,7 @@ func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.Attestati wf, err := s.findWorkflowFromTokenOrRunID(ctx, robotAccount.OrgID, robotAccount.WorkflowID, req.GetWorkflowRunId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -111,7 +111,7 @@ func (s *AttestationStateService) Save(ctx context.Context, req *cpAPI.Attestati return nil, cpAPI.ErrorAttestationStateErrorConflict("saving attestation: %s", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationStateServiceSaveResponse{Digest: digest}, nil @@ -125,7 +125,7 @@ func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.Attestati wf, err := s.findWorkflowFromTokenOrRunID(ctx, robotAccount.OrgID, robotAccount.WorkflowID, req.GetWorkflowRunId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -140,7 +140,7 @@ func (s *AttestationStateService) Read(ctx context.Context, req *cpAPI.Attestati state, err := s.attestationStateUseCase.Read(ctx, wf.ID.String(), req.WorkflowRunId, encryptionPassphrase) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationStateServiceReadResponse{ @@ -159,7 +159,7 @@ func (s *AttestationStateService) Reset(ctx context.Context, req *cpAPI.Attestat wf, err := s.findWorkflowFromTokenOrRunID(ctx, robotAccount.OrgID, robotAccount.WorkflowID, req.GetWorkflowRunId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC on the project @@ -168,7 +168,7 @@ func (s *AttestationStateService) Reset(ctx context.Context, req *cpAPI.Attestat } if err := s.attestationStateUseCase.Reset(ctx, wf.ID.String(), req.WorkflowRunId); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &cpAPI.AttestationStateServiceResetResponse{}, nil diff --git a/app/controlplane/internal/service/auth.go b/app/controlplane/internal/service/auth.go index bf66ae7ce..0f8bfa14f 100644 --- a/app/controlplane/internal/service/auth.go +++ b/app/controlplane/internal/service/auth.go @@ -82,11 +82,11 @@ func newOauthResp(code int, err error, showErrToUser bool) *oauthResp { // ErrorMessage is used to provide by default a generic error message to the user // unless showErrToUser is true -func (e *oauthResp) ErrorMessage(l *log.Helper) string { +func (e *oauthResp) ErrorMessage(ctx context.Context, l *log.Helper) string { if e.err != nil { // If the error is an internal server error, log it and raise it masked if e.code == http.StatusInternalServerError { - return sl.LogAndMaskErr(e.err, l).Error() + return sl.LogAndMaskErr(ctx, e.err, l).Error() } // otherwise return the error message to the user // or the default status text @@ -284,7 +284,7 @@ func (h oauthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, resp.redirectURL.String(), http.StatusTemporaryRedirect) return } - http.Error(w, resp.ErrorMessage(h.svc.log), resp.code) + http.Error(w, resp.ErrorMessage(r.Context(), h.svc.log), resp.code) } } @@ -540,16 +540,18 @@ func (svc *AuthService) setOauthCookie(w http.ResponseWriter, name, value string } func generateAndLogDevUser(userUC *biz.UserUseCase, log *log.Helper, authConfig *conf.Auth) error { + // Runs at startup, outside of any request + ctx := context.Background() // Create user if needed - u, err := userUC.UpsertByEmail(context.Background(), authConfig.DevUser, nil) + u, err := userUC.UpsertByEmail(ctx, authConfig.DevUser, nil) if err != nil { - return sl.LogAndMaskErr(err, log) + return sl.LogAndMaskErr(ctx, err, log) } // Generate user token userToken, err := generateUserJWT(u.ID, authConfig.GeneratedJwsHmacSecret, devUserDuration) if err != nil { - return sl.LogAndMaskErr(err, log) + return sl.LogAndMaskErr(ctx, err, log) } log.Info("******************* DEVELOPMENT USER TOKEN *******************") @@ -566,7 +568,7 @@ func (svc *AuthService) DeleteAccount(ctx context.Context, _ *pb.AuthServiceDele } if err := svc.userUseCase.DeleteUser(ctx, user.ID); err != nil { - return nil, handleUseCaseErr(err, svc.log) + return nil, handleUseCaseErr(ctx, err, svc.log) } return &pb.AuthServiceDeleteAccountResponse{}, nil diff --git a/app/controlplane/internal/service/casbackend.go b/app/controlplane/internal/service/casbackend.go index 958ea0deb..0c46c5614 100644 --- a/app/controlplane/internal/service/casbackend.go +++ b/app/controlplane/internal/service/casbackend.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -50,7 +50,7 @@ func (s *CASBackendService) List(ctx context.Context, _ *pb.CASBackendServiceLis backends, err := s.uc.List(ctx, currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } res := []*pb.CASBackendItem{} @@ -98,7 +98,7 @@ func (s *CASBackendService) Create(ctx context.Context, req *pb.CASBackendServic // For now we only support one backend which is set as default res, err := s.uc.Create(ctx, currentOrg.ID, req.Name, req.Location, req.Description, biz.CASBackendProvider(req.Provider), creds, req.Default, req.Fallback, maxBytes) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.CASBackendServiceCreateResponse{Result: bizCASBackendToPb(res)}, nil @@ -112,7 +112,7 @@ func (s *CASBackendService) Update(ctx context.Context, req *pb.CASBackendServic backend, err := s.uc.FindByNameInOrg(ctx, currentOrg.ID, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // if we are updating credentials we need to validate them @@ -145,7 +145,7 @@ func (s *CASBackendService) Update(ctx context.Context, req *pb.CASBackendServic // For now we only support one backend which is set as default res, err := s.uc.Update(ctx, currentOrg.ID, backend.ID.String(), req.Description, creds, req.Default, req.Fallback, maxBytes) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.CASBackendServiceUpdateResponse{Result: bizCASBackendToPb(res)}, nil @@ -160,12 +160,12 @@ func (s *CASBackendService) Delete(ctx context.Context, req *pb.CASBackendServic backend, err := s.uc.FindByNameInOrg(ctx, currentOrg.ID, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // In fact we soft-delete the backend instead if err := s.uc.SoftDelete(ctx, currentOrg.ID, backend.ID.String()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.CASBackendServiceDeleteResponse{}, nil @@ -180,7 +180,7 @@ func (s *CASBackendService) Revalidate(ctx context.Context, req *pb.CASBackendSe backend, err := s.uc.FindByNameInOrg(ctx, currentOrg.ID, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Only allow revalidation for non-inline backends diff --git a/app/controlplane/internal/service/cascredential.go b/app/controlplane/internal/service/cascredential.go index d4723d637..df2e1d49d 100644 --- a/app/controlplane/internal/service/cascredential.go +++ b/app/controlplane/internal/service/cascredential.go @@ -84,7 +84,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS // Enforce required role if ok, err := s.authzUC.Enforce(ctx, currentAuthzSubject, policyToCheck); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if !ok { return nil, errors.Forbidden("forbidden", "not allowed to perform this operation") } @@ -92,7 +92,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS // Get default backend defaultBackend, err := s.casBackendUC.FindDefaultBackend(ctx, currentOrg.ID) if err != nil && !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if defaultBackend == nil { return nil, errors.NotFound("not found", "main CAS backend not found") } @@ -112,7 +112,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS var orgID uuid.UUID orgID, err = uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []uuid.UUID{orgID}, s.rbacScopesForOrg(ctx, orgID)) @@ -120,7 +120,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS if biz.IsErrValidation(err) { return nil, errors.BadRequest("invalid", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } @@ -129,7 +129,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS } else { // fallback to default backend if the user or the token is allowed to if ok, err := s.authzUC.Enforce(ctx, currentAuthzSubject, authz.PolicyDefaultBackendArtifactRead); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if ok { backend = defaultBackend } @@ -151,7 +151,7 @@ func (s *CASCredentialsService) Get(ctx context.Context, req *pb.CASCredentialsS ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: role, MaxBytes: backend.Limits.MaxBytes, OrgID: backend.OrganizationID, SourceInternal: sourceInternal} t, err := s.casUC.GenerateTemporaryCredentials(ref) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.CASCredentialsServiceGetResponse{ diff --git a/app/controlplane/internal/service/casredirect.go b/app/controlplane/internal/service/casredirect.go index 8f7a02d76..bc751411f 100644 --- a/app/controlplane/internal/service/casredirect.go +++ b/app/controlplane/internal/service/casredirect.go @@ -86,7 +86,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown var orgID uuid.UUID orgID, err = uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } mapping, err = s.casMappingUC.FindCASMappingForDownloadByOrg(ctx, req.Digest, []uuid.UUID{orgID}, s.rbacScopesForOrg(ctx, orgID)) } @@ -100,7 +100,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown return nil, kerrors.BadRequest("invalid", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } backend := mapping.CASBackend @@ -118,7 +118,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown // Create an URL to download the artifact from the CAS backend downloadBase, err := url.Parse(s.casServerConf.GetDownloadUrl()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // 1 - append the digest /download/[digest] @@ -129,7 +129,7 @@ func (s *CASRedirectService) GetDownloadURL(ctx context.Context, req *pb.GetDown ref := &biz.CASCredsOpts{BackendType: string(backend.Provider), SecretPath: backend.SecretName, Role: casJWT.Downloader, MaxBytes: backend.Limits.MaxBytes, OrgID: backend.OrganizationID} t, err := s.casCredsUseCase.GenerateTemporaryCredentials(ref) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } q := downloadURL.Query() diff --git a/app/controlplane/internal/service/context.go b/app/controlplane/internal/service/context.go index 13adcd5d6..4082164e1 100644 --- a/app/controlplane/internal/service/context.go +++ b/app/controlplane/internal/service/context.go @@ -77,14 +77,14 @@ func (s *ContextService) Current(ctx context.Context, _ *pb.ContextServiceCurren // when there is no organization or membership gracefully orgName, err := entities.GetOrganizationNameFromHeader(ctx) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // It might not be set in the header, so we load it from the DB if orgName == "" { membership, err := s.userUC.CurrentMembership(ctx, currentUser.ID) if err != nil && !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if membership != nil { orgName = membership.Org.Name } @@ -93,7 +93,7 @@ func (s *ContextService) Current(ctx context.Context, _ *pb.ContextServiceCurren if orgName != "" { m, err := s.userUC.MembershipInOrg(ctx, currentUser.ID, orgName) if err != nil && !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if err != nil { return nil, pb.ErrorUserNotMemberOfOrgErrorNotInOrg("user is not a member of organization %s", orgName) } @@ -107,7 +107,7 @@ func (s *ContextService) Current(ctx context.Context, _ *pb.ContextServiceCurren // Add cas backend backend, err := s.uc.FindDefaultBackend(ctx, currentOrg.ID) if err != nil && !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if backend != nil { diff --git a/app/controlplane/internal/service/group.go b/app/controlplane/internal/service/group.go index c71663264..4fcd63d8a 100644 --- a/app/controlplane/internal/service/group.go +++ b/app/controlplane/internal/service/group.go @@ -1,5 +1,5 @@ // -// Copyright 2025 The Chainloop Authors. +// Copyright 2025-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ func (g *GroupService) Create(ctx context.Context, req *pb.GroupServiceCreateReq gr, err := g.groupUseCase.Create(ctx, orgUUID, req.Name, req.Description, &userUUID) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceCreateResponse{ @@ -105,7 +105,7 @@ func (g *GroupService) Get(ctx context.Context, req *pb.GroupServiceGetRequest) gr, err := g.groupUseCase.Get(ctx, orgUUID, opts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceGetResponse{ @@ -129,7 +129,7 @@ func (g *GroupService) List(ctx context.Context, req *pb.GroupServiceListRequest // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Initialize the filters @@ -149,7 +149,7 @@ func (g *GroupService) List(ctx context.Context, req *pb.GroupServiceListRequest grs, count, err := g.groupUseCase.List(ctx, orgUUID, filters, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Convert the groups to protobuf messages @@ -191,7 +191,7 @@ func (g *GroupService) Update(ctx context.Context, req *pb.GroupServiceUpdateReq NewName: req.NewName, }) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceUpdateResponse{ @@ -223,7 +223,7 @@ func (g *GroupService) Delete(ctx context.Context, req *pb.GroupServiceDeleteReq err = g.groupUseCase.Delete(ctx, orgUUID, idReference) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceDeleteResponse{}, nil @@ -284,12 +284,12 @@ func (g *GroupService) ListMembers(ctx context.Context, req *pb.GroupServiceList // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } grs, count, err := g.groupUseCase.ListMembers(ctx, orgUUID, opts, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Convert the group members to protobuf messages @@ -347,7 +347,7 @@ func (g *GroupService) AddMember(ctx context.Context, req *pb.GroupServiceAddMem // Call the business logic to add the member _, err = g.groupUseCase.AddMemberToGroup(ctx, orgUUID, addOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceAddMemberResponse{}, nil @@ -397,7 +397,7 @@ func (g *GroupService) RemoveMember(ctx context.Context, req *pb.GroupServiceRem // Call the business logic to remove the member err = g.groupUseCase.RemoveMemberFromGroup(ctx, orgUUID, removeOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceRemoveMemberResponse{}, nil @@ -429,13 +429,13 @@ func (g *GroupService) ListPendingInvitations(ctx context.Context, req *pb.Group // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Call the business logic to list pending invitations invitations, count, err := g.groupUseCase.ListPendingInvitations(ctx, orgUUID, groupID, groupName, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Convert business objects to protobuf messages @@ -502,7 +502,7 @@ func (g *GroupService) UpdateMemberMaintainerStatus(ctx context.Context, req *pb // Call the business logic to update the member's maintainer status err = g.groupUseCase.UpdateMemberMaintainerStatus(ctx, orgUUID, updateOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } return &pb.GroupServiceUpdateMemberMaintainerStatusResponse{}, nil @@ -540,13 +540,13 @@ func (g *GroupService) ListProjects(ctx context.Context, req *pb.GroupServiceLis // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Retrieve the list of project info projectInfoList, count, err := g.groupUseCase.ListProjectsByGroup(ctx, orgUUID, groupOpts, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, g.log) + return nil, handleUseCaseErr(ctx, err, g.log) } // Convert the GroupProjectInfo to protobuf messages @@ -613,7 +613,7 @@ func (g *GroupService) userHasPermissionOnGroupMembershipsWithPolicy(ctx context // Resolve the group identifier to a valid group ID resolvedGroupID, err := g.groupUseCase.ValidateGroupIdentifier(ctx, orgUUID, groupID, groupName) if err != nil { - return handleUseCaseErr(err, g.log) + return handleUseCaseErr(ctx, err, g.log) } // Check the user's membership in the organization @@ -622,7 +622,7 @@ func (g *GroupService) userHasPermissionOnGroupMembershipsWithPolicy(ctx context if rm.ResourceType == authz.ResourceTypeGroup && rm.ResourceID == resolvedGroupID { pass, err := g.authz.Enforce(ctx, string(rm.Role), policy) if err != nil { - return handleUseCaseErr(err, g.log) + return handleUseCaseErr(ctx, err, g.log) } if pass { return nil diff --git a/app/controlplane/internal/service/integration.go b/app/controlplane/internal/service/integration.go index b7779fb35..09c511110 100644 --- a/app/controlplane/internal/service/integration.go +++ b/app/controlplane/internal/service/integration.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -96,7 +96,7 @@ func (s *IntegrationsService) Register(ctx context.Context, req *pb.Integrations return nil, errors.BadRequest("wrong validation", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.IntegrationsServiceRegisterResponse{Result: bizIntegrationToPb(i)}, nil @@ -114,7 +114,7 @@ func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsSe if biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // lookup the integration @@ -128,7 +128,7 @@ func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsSe if biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC if needed @@ -149,12 +149,12 @@ func (s *IntegrationsService) Attach(ctx context.Context, req *pb.IntegrationsSe return nil, errors.BadRequest("wrong validation", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result, err := s.bizIntegrationAttachmentToPb(ctx, res, org.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.IntegrationsServiceAttachResponse{Result: result}, nil @@ -168,7 +168,7 @@ func (s *IntegrationsService) ListRegistrations(ctx context.Context, _ *pb.Integ integrations, err := s.integrationUC.List(ctx, org.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.RegisteredIntegrationItem, 0, len(integrations)) @@ -187,7 +187,7 @@ func (s *IntegrationsService) DescribeRegistration(ctx context.Context, req *pb. i, err := s.integrationUC.FindByNameInOrg(ctx, org.ID, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if i == nil { return nil, errors.NotFound("not found", "integration not found") } @@ -203,7 +203,7 @@ func (s *IntegrationsService) Deregister(ctx context.Context, req *pb.Integratio integration, err := s.integrationUC.FindByNameInOrg(ctx, org.ID, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if integration == nil { return nil, errors.NotFound("not found", "integration not found") } @@ -212,7 +212,7 @@ func (s *IntegrationsService) Deregister(ctx context.Context, req *pb.Integratio if err != nil && biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } else if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.IntegrationsServiceDeregisterResponse{}, nil @@ -232,7 +232,7 @@ func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListA if biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } opts.WorkflowID = &wf.ID } @@ -242,14 +242,14 @@ func (s *IntegrationsService) ListAttachments(ctx context.Context, req *pb.ListA integrations, err := s.integrationUC.ListAttachments(ctx, org.ID, opts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.IntegrationAttachmentItem, 0, len(integrations)) for _, i := range integrations { r, err := s.bizIntegrationAttachmentToPb(ctx, i.IntegrationAttachment, org.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result = append(result, r) } @@ -275,17 +275,17 @@ func (s *IntegrationsService) Detach(ctx context.Context, req *pb.IntegrationsSe att, err := s.integrationUC.GetAttachment(ctx, orgID, attID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } wf, err := s.workflowUC.FindByIDInOrg(ctx, org.ID, att.WorkflowID.String()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Apply RBAC if err = s.authorizeResource(ctx, authz.PolicyAttachedIntegrationDetach, authz.ResourceTypeProject, wf.ProjectID); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if err := s.integrationUC.Detach(ctx, org.ID, req.Id); err != nil { @@ -293,7 +293,7 @@ func (s *IntegrationsService) Detach(ctx context.Context, req *pb.IntegrationsSe return nil, errors.NotFound("not found", err.Error()) } - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.IntegrationsServiceDetachResponse{}, nil diff --git a/app/controlplane/internal/service/organization.go b/app/controlplane/internal/service/organization.go index 3554cc6eb..04e566366 100644 --- a/app/controlplane/internal/service/organization.go +++ b/app/controlplane/internal/service/organization.go @@ -63,13 +63,13 @@ func (s *OrganizationService) Create(ctx context.Context, req *pb.OrganizationSe // Create an organization with an associated inline CAS backend org, err := s.orgUC.Create(ctx, req.Name, biz.WithCreateInlineBackend()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Add membership if invoker is a user if currentUser != nil { if _, err := s.membershipUC.Create(ctx, org.ID, currentUser.ID, biz.WithMembershipRole(authz.RoleOwner), biz.WithCurrentMembership()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } @@ -126,7 +126,7 @@ func (s *OrganizationService) Update(ctx context.Context, req *pb.OrganizationSe SkipRunnerEnvVars: req.SkipRunnerEnvVars, }) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrganizationServiceUpdateResponse{Result: bizOrgToPb(org)}, nil @@ -140,12 +140,12 @@ func (s *OrganizationService) Delete(ctx context.Context, req *pb.OrganizationSe // Find the organization to get its UUID for authorization org, err := s.orgUC.FindByName(ctx, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } orgUUID, err := uuid.Parse(org.ID) if err != nil { - return nil, handleUseCaseErr(biz.NewErrInvalidUUID(err), s.log) + return nil, handleUseCaseErr(ctx, biz.NewErrInvalidUUID(err), s.log) } // Check if user has permission to delete this specific organization @@ -155,7 +155,7 @@ func (s *OrganizationService) Delete(ctx context.Context, req *pb.OrganizationSe } if err := s.orgUC.Delete(ctx, orgUUID.String()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrganizationServiceDeleteResponse{}, nil @@ -170,7 +170,7 @@ func (s *OrganizationService) ListMemberships(ctx context.Context, req *pb.Organ // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } opts := &biz.ListByOrgOpts{ @@ -181,7 +181,7 @@ func (s *OrganizationService) ListMemberships(ctx context.Context, req *pb.Organ if req.MembershipId != nil { membershipUUID, err := uuid.Parse(req.GetMembershipId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } opts.MembershipID = &membershipUUID @@ -194,7 +194,7 @@ func (s *OrganizationService) ListMemberships(ctx context.Context, req *pb.Organ memberships, count, err := s.membershipUC.ByOrg(ctx, currentOrg.ID, opts, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.OrgMembershipItem, 0, len(memberships)) @@ -221,7 +221,7 @@ func (s *OrganizationService) DeleteMembership(ctx context.Context, req *pb.Orga callerRole := authz.Role(usercontext.CurrentAuthzSubject(ctx)) if err := s.membershipUC.DeleteOther(ctx, currentOrg.ID, currentUser.ID, req.MembershipId, callerRole); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrganizationServiceDeleteMembershipResponse{}, nil @@ -241,7 +241,7 @@ func (s *OrganizationService) UpdateMembership(ctx context.Context, req *pb.Orga callerRole := authz.Role(usercontext.CurrentAuthzSubject(ctx)) m, err := s.membershipUC.UpdateRole(ctx, currentOrg.ID, currentUser.ID, req.MembershipId, biz.PbRoleToBiz(req.Role), callerRole) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrganizationServiceUpdateMembershipResponse{Result: bizMembershipToPb(m)}, nil diff --git a/app/controlplane/internal/service/orginvitation.go b/app/controlplane/internal/service/orginvitation.go index c8230b22e..17241ae4a 100644 --- a/app/controlplane/internal/service/orginvitation.go +++ b/app/controlplane/internal/service/orginvitation.go @@ -55,7 +55,7 @@ func (s *OrgInvitationService) Create(ctx context.Context, req *pb.OrgInvitation if user != nil { userID, err := uuid.Parse(user.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } opts = append(opts, biz.WithSender(userID)) } @@ -65,7 +65,7 @@ func (s *OrgInvitationService) Create(ctx context.Context, req *pb.OrgInvitation // Validations are done in the biz layer i, err := s.useCase.Create(ctx, org.ID, req.ReceiverEmail, callerRole, opts...) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrgInvitationServiceCreateResponse{Result: bizInvitationToPB(i)}, nil @@ -78,7 +78,7 @@ func (s *OrgInvitationService) Revoke(ctx context.Context, req *pb.OrgInvitation } if err := s.useCase.Revoke(ctx, org.ID, req.Id); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrgInvitationServiceRevokeResponse{}, nil @@ -92,7 +92,7 @@ func (s *OrgInvitationService) ListSent(ctx context.Context, _ *pb.OrgInvitation invitations, err := s.useCase.ListByOrg(ctx, org.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } res := []*pb.OrgInvitationItem{} diff --git a/app/controlplane/internal/service/orgmetric.go b/app/controlplane/internal/service/orgmetric.go index 10b8c288d..de337ba8c 100644 --- a/app/controlplane/internal/service/orgmetric.go +++ b/app/controlplane/internal/service/orgmetric.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -52,17 +52,17 @@ func (s *OrgMetricsService) Totals(ctx context.Context, req *pb.OrgMetricsServic // TODO: Merge it to a single request totals, err := s.uc.RunsTotal(ctx, currentOrg.ID, timeWindow, projectIDs) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } totalsByStatus, err := s.uc.RunsTotalByStatus(ctx, currentOrg.ID, timeWindow, projectIDs) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } totalsByRunnerType, err := s.uc.RunsTotalByRunnerType(ctx, currentOrg.ID, timeWindow, projectIDs) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.OrgMetricsServiceTotalsResponse{Result: &pb.OrgMetricsServiceTotalsResponse_Result{ @@ -85,7 +85,7 @@ func (s *OrgMetricsService) TopWorkflowsByRunsCount(ctx context.Context, req *pb res, err := s.uc.TopWorkflowsByRunsCount(ctx, currentOrg.ID, int(req.GetNumWorkflows()), timeWindow, projectIDs) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } var result = []*pb.TopWorkflowsByRunsCountResponse_TotalByStatus{} @@ -112,7 +112,7 @@ func (s *OrgMetricsService) DailyRunsCount(ctx context.Context, req *pb.DailyRun metricsByDay, err := s.uc.DailyRunsCount(ctx, org.ID, req.WorkflowId, timeWindow, projectIDs) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } var res = make([]*pb.DailyRunsCountResponse_TotalByDay, 0, len(metricsByDay)) diff --git a/app/controlplane/internal/service/project.go b/app/controlplane/internal/service/project.go index bfe8be24f..8ada85de2 100644 --- a/app/controlplane/internal/service/project.go +++ b/app/controlplane/internal/service/project.go @@ -1,5 +1,5 @@ // -// Copyright 2025 The Chainloop Authors. +// Copyright 2025-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -48,13 +48,13 @@ func (s *ProjectService) List(ctx context.Context, req *pb.ProjectServiceListReq orgUUID, err := uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // visibleProjects is nil when RBAC does not apply, which the use case reads as @@ -65,20 +65,20 @@ func (s *ProjectService) List(ctx context.Context, req *pb.ProjectServiceListReq VisibleProjects: s.visibleProjects(ctx), }, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Resolve, once per listing, which projects the caller may add a workflow to. writable, err := s.projectsAllowing(ctx, authz.PolicyWorkflowCreate, projects) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Whether a new project is an option at all depends on the organization role // rather than on any project, so it is answered once for the listing. canCreateProject, err := s.canCreateProject(ctx) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.ProjectServiceListResponse_ProjectItem, 0, len(projects)) @@ -111,7 +111,7 @@ func (s *ProjectService) ListMembers(ctx context.Context, req *pb.ProjectService // Convert organization ID from string to UUID orgUUID, err := uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Create the identity reference for the project @@ -126,13 +126,13 @@ func (s *ProjectService) ListMembers(ctx context.Context, req *pb.ProjectService // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Call the business logic to list members members, total, err := s.projectUseCase.ListMembers(ctx, orgUUID, identityRef, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Convert the project members to protobuf messages @@ -168,12 +168,12 @@ func (s *ProjectService) AddMember(ctx context.Context, req *pb.ProjectServiceAd requesterUUID, err := uuid.Parse(currentUser.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } orgUUID, err := uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Create the identity reference for the project @@ -188,7 +188,7 @@ func (s *ProjectService) AddMember(ctx context.Context, req *pb.ProjectServiceAd // Extract the user email and group reference from the membership reference field userEmail, groupReference, err := s.extractMembershipReference(req.GetMemberReference()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Convert from protobuf role to internal authorization role @@ -206,7 +206,7 @@ func (s *ProjectService) AddMember(ctx context.Context, req *pb.ProjectServiceAd // Call the business logic to add the member _, err = s.projectUseCase.AddMemberToProject(ctx, orgUUID, opts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.ProjectServiceAddMemberResponse{}, nil @@ -233,12 +233,12 @@ func (s *ProjectService) RemoveMember(ctx context.Context, req *pb.ProjectServic requesterUUID, err := uuid.Parse(currentUser.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } orgUUID, err := uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Create the identity reference for the project @@ -253,7 +253,7 @@ func (s *ProjectService) RemoveMember(ctx context.Context, req *pb.ProjectServic // Extract the user email and group reference from the membership reference field userEmail, groupReference, err := s.extractMembershipReference(req.GetMemberReference()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Prepare options for removing a member @@ -267,7 +267,7 @@ func (s *ProjectService) RemoveMember(ctx context.Context, req *pb.ProjectServic // Call the business logic to remove the member err = s.projectUseCase.RemoveMemberFromProject(ctx, orgUUID, opts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.ProjectServiceRemoveMemberResponse{}, nil @@ -294,18 +294,18 @@ func (s *ProjectService) UpdateMemberRole(ctx context.Context, req *pb.ProjectSe requesterUUID, err := uuid.Parse(currentUser.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } orgUUID, err := uuid.Parse(currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Extract the user email and group reference from the membership reference field userEmail, groupReference, err := s.extractMembershipReference(req.GetMemberReference()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Convert from protobuf role to internal authorization role @@ -328,7 +328,7 @@ func (s *ProjectService) UpdateMemberRole(ctx context.Context, req *pb.ProjectSe // Call the business logic to update the member's role if err := s.projectUseCase.UpdateMemberRole(ctx, orgUUID, opts); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.ProjectServiceUpdateMemberRoleResponse{}, nil @@ -362,7 +362,7 @@ func (s *ProjectService) ListPendingInvitations(ctx context.Context, req *pb.Pro // Initialize the pagination options, with default values paginationOpts, err := initializePaginationOpts(req.GetPagination()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Call the business logic to list pending invitations @@ -371,7 +371,7 @@ func (s *ProjectService) ListPendingInvitations(ctx context.Context, req *pb.Pro Name: projectName, }, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Convert business objects to protobuf messages diff --git a/app/controlplane/internal/service/referrer.go b/app/controlplane/internal/service/referrer.go index 883aca7db..107bca445 100644 --- a/app/controlplane/internal/service/referrer.go +++ b/app/controlplane/internal/service/referrer.go @@ -87,7 +87,7 @@ func (s *ReferrerService) DiscoverPrivate(ctx context.Context, req *pb.ReferrerS referrer, nextCursor, err = s.referrerUC.GetFromRoot(ctx, req.GetDigest(), req.GetKind(), []uuid.UUID{orgUUID}, orgsProjectsMap, paginationOpts, extraFilters...) } if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.ReferrerServiceDiscoverPrivateResponse{ diff --git a/app/controlplane/internal/service/service.go b/app/controlplane/internal/service/service.go index ac4a74d15..386a0af63 100644 --- a/app/controlplane/internal/service/service.go +++ b/app/controlplane/internal/service/service.go @@ -231,7 +231,7 @@ func (s *service) authorizeResource(ctx context.Context, op *authz.Policy, resou for _, rm := range matchingResources { pass, err := s.authz.Enforce(ctx, string(rm.Role), op) if err != nil { - return handleUseCaseErr(err, s.log) + return handleUseCaseErr(ctx, err, s.log) } if pass { @@ -354,7 +354,7 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, // Find the project by its reference p, err := s.projectUseCase.FindProjectByReference(ctx, orgID, &biz.IdentityReference{ID: entityID, Name: entityName}) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // if RBAC is not enabled, we return the project @@ -372,7 +372,7 @@ func (s *service) userHasPermissionOnProject(ctx context.Context, orgID string, func (s *service) userCanCreateProject(ctx context.Context) error { pass, err := s.canCreateProject(ctx) if err != nil { - return handleUseCaseErr(err, s.log) + return handleUseCaseErr(ctx, err, s.log) } if !pass { @@ -457,7 +457,7 @@ func (s *service) checkPolicy(ctx context.Context, policy *authz.Policy) error { if sub != "" { ok, err := s.authz.Enforce(ctx, sub, policy) if err != nil { - return handleUseCaseErr(err, s.log) + return handleUseCaseErr(ctx, err, s.log) } if ok { return nil @@ -472,7 +472,7 @@ func (s *service) checkPolicy(ctx context.Context, policy *authz.Policy) error { for _, rm := range m.Resources { pass, err := s.authz.Enforce(ctx, string(rm.Role), authz.PolicyOrganizationCreate) if err != nil { - return handleUseCaseErr(err, s.log) + return handleUseCaseErr(ctx, err, s.log) } if pass { return nil @@ -526,7 +526,7 @@ func rbacEnabled(ctx context.Context) bool { // NOTE: some of these http errors get automatically translated to gRPC status codes // because they implement the gRPC status error interface // so it is safe to return either a gRPC status error or a kratos error -func handleUseCaseErr(err error, l *log.Helper) error { +func handleUseCaseErr(ctx context.Context, err error, l *log.Helper) error { switch { case errors.Is(err, context.Canceled): return errors.ClientClosed("client closed", err.Error()) @@ -556,7 +556,7 @@ func handleUseCaseErr(err error, l *log.Helper) error { } } - return servicelogger.LogAndMaskErr(err, l) + return servicelogger.LogAndMaskErr(ctx, err, l) } } diff --git a/app/controlplane/internal/service/service_test.go b/app/controlplane/internal/service/service_test.go index 6dc43c233..2bf2a8bae 100644 --- a/app/controlplane/internal/service/service_test.go +++ b/app/controlplane/internal/service/service_test.go @@ -68,19 +68,19 @@ func TestHandleUseCaseErr(t *testing.T) { }, { name: "already converted error is propagated unchanged when processed again", - err: handleUseCaseErr(fmt.Errorf("saving attestation digest: %w", biz.NewErrReleasedVersionImmutable("v1.83.2+next")), nil), + err: handleUseCaseErr(context.Background(), fmt.Errorf("saving attestation digest: %w", biz.NewErrReleasedVersionImmutable("v1.83.2+next")), nil), wantCode: codes.FailedPrecondition, wantMessage: `saving attestation digest: version "v1.83.2+next" is released and immutable: attestations cannot be added`, }, { name: "already converted not found error is propagated unchanged when processed again", - err: handleUseCaseErr(biz.NewErrNotFound("workflow"), nil), + err: handleUseCaseErr(context.Background(), biz.NewErrNotFound("workflow"), nil), wantCode: codes.NotFound, wantMessage: "workflow not found", }, { name: "already converted already exists error is propagated unchanged when processed again", - err: handleUseCaseErr(biz.NewErrAlreadyExists(errors.New("name taken")), nil), + err: handleUseCaseErr(context.Background(), biz.NewErrAlreadyExists(errors.New("name taken")), nil), wantCode: codes.AlreadyExists, wantMessage: "duplicated: name taken", }, @@ -139,7 +139,7 @@ func TestHandleUseCaseErr(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got := handleUseCaseErr(tc.err, nil) + got := handleUseCaseErr(context.Background(), tc.err, nil) require.Error(t, got) assert.Equal(t, tc.wantCode, status.Code(got)) assert.Equal(t, tc.wantMessage, kerrors.FromError(got).GetMessage()) diff --git a/app/controlplane/internal/service/signing.go b/app/controlplane/internal/service/signing.go index 7f1c77a3a..2a98af8b7 100644 --- a/app/controlplane/internal/service/signing.go +++ b/app/controlplane/internal/service/signing.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ func (s *SigningService) GenerateSigningCert(ctx context.Context, req *v1.Genera certs, err := s.signing.CreateSigningCert(ctx, ra.OrgID, req.GetCertificateSigningRequest()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &v1.GenerateSigningCertResponse{Chain: &v1.CertificateChain{Certificates: certs}}, nil @@ -57,7 +57,7 @@ func (s *SigningService) GenerateSigningCert(ctx context.Context, req *v1.Genera func (s *SigningService) GetTrustedRoot(ctx context.Context, _ *v1.GetTrustedRootRequest) (*v1.GetTrustedRootResponse, error) { tr, err := s.signing.GetTrustedRoot(ctx) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } resp := &v1.GetTrustedRootResponse{Keys: make(map[string]*v1.CertificateChain), TimestampAuthorities: make(map[string]*v1.CertificateChain)} for k, v := range tr.Keys { diff --git a/app/controlplane/internal/service/user.go b/app/controlplane/internal/service/user.go index c97f103f1..4031a9241 100644 --- a/app/controlplane/internal/service/user.go +++ b/app/controlplane/internal/service/user.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -52,7 +52,7 @@ func (s *UserService) ListMemberships(ctx context.Context, _ *pb.UserServiceList if err != nil && biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } else if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.OrgMembershipItem, 0, len(memberships)) @@ -73,7 +73,7 @@ func (s *UserService) SetCurrentMembership(ctx context.Context, req *pb.SetCurre if err != nil && biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } else if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.SetCurrentMembershipResponse{Result: bizMembershipToPb(m)}, nil @@ -89,7 +89,7 @@ func (s *UserService) DeleteMembership(ctx context.Context, req *pb.DeleteMember if err != nil && biz.IsNotFound(err) { return nil, errors.NotFound("not found", err.Error()) } else if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.DeleteMembershipResponse{}, nil diff --git a/app/controlplane/internal/service/workflow.go b/app/controlplane/internal/service/workflow.go index f77a10ed2..6f3fb9b0f 100644 --- a/app/controlplane/internal/service/workflow.go +++ b/app/controlplane/internal/service/workflow.go @@ -66,7 +66,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre // Get organization org, err := s.orgUseCase.FindByID(ctx, currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if _, err = s.userHasPermissionOnProject(ctx, currentOrg.ID, &pb.IdentityReference{Name: &req.ProjectName}, authz.PolicyWorkflowCreate); err != nil { @@ -107,7 +107,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre if user != nil { userID, err := uuid.Parse(user.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } createOpts.Owner = &userID @@ -117,7 +117,7 @@ func (s *WorkflowService) Create(ctx context.Context, req *pb.WorkflowServiceCre p, err := s.useCase.Create(ctx, createOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowServiceCreateResponse{Result: bizWorkflowToPb(p)}, nil @@ -135,14 +135,14 @@ func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpd wf, err := s.useCase.FindByNameInOrg(ctx, currentOrg.ID, req.ProjectName, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } var contractID *string if req.ContractName != nil { c, err := s.contractUC.FindByNameInOrg(ctx, currentOrg.ID, *req.ContractName) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if c == nil { return nil, biz.NewErrNotFound("contract") } @@ -159,7 +159,7 @@ func (s *WorkflowService) Update(ctx context.Context, req *pb.WorkflowServiceUpd p, err := s.useCase.Update(ctx, currentOrg.ID, wf.ID.String(), updateOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowServiceUpdateResponse{Result: bizWorkflowToPb(p)}, nil @@ -182,7 +182,7 @@ func (s *WorkflowService) List(ctx context.Context, req *pb.WorkflowServiceListR int(req.GetPagination().GetPageSize()), ) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } @@ -245,7 +245,7 @@ func (s *WorkflowService) List(ctx context.Context, req *pb.WorkflowServiceListR workflows, count, err := s.useCase.List(ctx, currentOrg.ID, filters, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.WorkflowItem, 0, len(workflows)) @@ -271,11 +271,11 @@ func (s *WorkflowService) Delete(ctx context.Context, req *pb.WorkflowServiceDel wf, err := s.useCase.FindByNameInOrg(ctx, currentOrg.ID, req.ProjectName, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if err := s.useCase.Delete(ctx, currentOrg.ID, wf.ID.String()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowServiceDeleteResponse{}, nil @@ -296,7 +296,7 @@ func (s *WorkflowService) View(ctx context.Context, req *pb.WorkflowServiceViewR wf, err = s.useCase.FindByNameInOrg(ctx, currentOrg.ID, req.ProjectName, req.Name) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowServiceViewResponse{Result: bizWorkflowToPb(wf)}, nil diff --git a/app/controlplane/internal/service/workflowcontract.go b/app/controlplane/internal/service/workflowcontract.go index 7a604b4a1..f1530e56b 100644 --- a/app/controlplane/internal/service/workflowcontract.go +++ b/app/controlplane/internal/service/workflowcontract.go @@ -56,7 +56,7 @@ func (s *WorkflowContractService) List(ctx context.Context, _ *pb.WorkflowContra contracts, err := s.contractUseCase.List(ctx, currentOrg.ID, biz.WithProjectFilter(s.visibleProjects(ctx))) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.WorkflowContractItem, 0, len(contracts)) @@ -75,7 +75,7 @@ func (s *WorkflowContractService) Describe(ctx context.Context, req *pb.Workflow contract, err := s.contractUseCase.FindByNameInOrg(ctx, currentOrg.ID, req.GetName()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if contract == nil { return nil, errors.NotFound("not found", "contract not found") } @@ -89,7 +89,7 @@ func (s *WorkflowContractService) Describe(ctx context.Context, req *pb.Workflow // 2 - Get the contract version contractWithVersion, err := s.contractUseCase.Describe(ctx, currentOrg.ID, contract.ID.String(), int(req.GetRevision())) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if contractWithVersion == nil { return nil, errors.NotFound("not found", "contract not found") } @@ -117,7 +117,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo // Check organization settings for contract creation restriction org, err := s.orgUseCase.FindByID(ctx, currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // If setting is enabled, only org admins can create contracts (org-level or project-level) @@ -147,7 +147,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo if len(req.RawContract) != 0 { if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } @@ -165,7 +165,7 @@ func (s *WorkflowContractService) Create(ctx context.Context, req *pb.WorkflowCo ProjectID: projectID, }) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowContractServiceCreateResponse{Result: bizWorkFlowContractToPb(schema)}, nil @@ -198,7 +198,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo contract, err := s.contractUseCase.FindByNameInOrg(ctx, currentOrg.ID, contractName) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if contract == nil { return nil, errors.NotFound("not found", "contract not found") } @@ -215,7 +215,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo // Validate the contract policies if the raw contract is provided if len(req.RawContract) != 0 { if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawContract, token, nil, nil); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } } @@ -225,7 +225,7 @@ func (s *WorkflowContractService) Update(ctx context.Context, req *pb.WorkflowCo RawSchema: req.RawContract, }) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := &pb.WorkflowContractServiceUpdateResponse_Result{ @@ -265,13 +265,13 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon } if err = s.contractUseCase.ValidateContractPolicies(ctx, req.RawSchema, token, batchPolicyNames, batchPolicyGroupNames); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Check if the contract already exists contract, err := s.contractUseCase.FindByNameInOrg(ctx, currentOrg.ID, contractName) if err != nil && !biz.IsNotFound(err) { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if contract != nil { @@ -284,7 +284,7 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon if dryRun { changed, err := s.contractUseCase.RevisionWouldChange(ctx, currentOrg.ID, contract.ID.String(), req.RawSchema) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } status := pb.WorkflowContractServiceApplyResponse_APPLY_STATUS_UNCHANGED @@ -306,7 +306,7 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon RawSchema: req.RawSchema, }) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } status := pb.WorkflowContractServiceApplyResponse_APPLY_STATUS_UNCHANGED @@ -326,7 +326,7 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon // Check organization settings for contract creation restriction org, err := s.orgUseCase.FindByID(ctx, currentOrg.ID) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if org.RestrictContractCreationToOrgAdmins { @@ -357,7 +357,7 @@ func (s *WorkflowContractService) Apply(ctx context.Context, req *pb.WorkflowCon RawSchema: req.RawSchema, }) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowContractServiceApplyResponse{ @@ -376,7 +376,7 @@ func (s *WorkflowContractService) Delete(ctx context.Context, req *pb.WorkflowCo contract, err := s.contractUseCase.FindByNameInOrg(ctx, currentOrg.ID, req.GetName()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if contract == nil { return nil, errors.NotFound("not found", "contract not found") } @@ -386,7 +386,7 @@ func (s *WorkflowContractService) Delete(ctx context.Context, req *pb.WorkflowCo } if err := s.contractUseCase.Delete(ctx, currentOrg.ID, contract.ID.String()); err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } return &pb.WorkflowContractServiceDeleteResponse{}, nil diff --git a/app/controlplane/internal/service/workflowrun.go b/app/controlplane/internal/service/workflowrun.go index 344efba3b..052f8ef8c 100644 --- a/app/controlplane/internal/service/workflowrun.go +++ b/app/controlplane/internal/service/workflowrun.go @@ -243,7 +243,7 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic if req.GetWorkflowName() != "" && req.GetProjectName() != "" { wf, err := s.workflowUseCase.FindByNameInOrg(ctx, currentOrg.ID, req.GetProjectName(), req.GetWorkflowName()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } else if wf == nil { return nil, errors.NotFound("not found", "workflow not found") } @@ -254,7 +254,7 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic // by project name only pID, err := s.validateAndGetProjectID(ctx, currentOrg.ID, req.GetProjectName(), visibleProjectIDs) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } // Override the filter to only include this specific project @@ -273,7 +273,7 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic pv, err := s.projectVersionUseCase.FindByProjectAndVersion(ctx, projectID.String(), req.GetProjectVersionName()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } filters.VersionID = &pv.ID @@ -328,7 +328,7 @@ func (s *WorkflowRunService) List(ctx context.Context, req *pb.WorkflowRunServic workflowRuns, nextCursor, err := s.wrUseCase.List(ctx, currentOrg.ID, filters, paginationOpts) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } result := make([]*pb.WorkflowRunItem, 0, len(workflowRuns)) @@ -357,12 +357,12 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic case req.GetId() != "": run, err = s.wrUseCase.GetByIDInOrg(ctx, currentOrg.ID, req.GetId()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } case req.GetDigest() != "": run, err = s.wrUseCase.GetByDigestInOrg(ctx, currentOrg.ID, req.GetDigest()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } default: return nil, errors.BadRequest("invalid", "id or digest required") @@ -378,7 +378,7 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic // it might be nil if it doesn't apply vr, err := s.wrUseCase.VerifyRun(ctx, run) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } verificationResult = bizVerificationToPb(vr) } @@ -388,7 +388,7 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic if run.Attestation != nil && run.Attestation.Envelope != nil { predicate, err = chainloop.ExtractPredicate(run.Attestation.Envelope) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if resolved := s.resolvePolicyEvaluations(ctx, predicate.GetPolicyEvaluationsRef(), run.Workflow.OrgID); resolved != nil { @@ -403,7 +403,7 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic attestation, err := bizAttestationToPb(run.Attestation, predicate) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } if attestation != nil { @@ -412,7 +412,7 @@ func (s *WorkflowRunService) View(ctx context.Context, req *pb.WorkflowRunServic contractAndVersion, err := s.workflowContractUseCase.FindVersionByID(ctx, run.ContractVersionID.String()) if err != nil { - return nil, handleUseCaseErr(err, s.log) + return nil, handleUseCaseErr(ctx, err, s.log) } wr := bizWorkFlowRunToPb(run) diff --git a/pkg/servicelogger/logger.go b/pkg/servicelogger/logger.go index 9246d62bb..67dd15cd1 100644 --- a/pkg/servicelogger/logger.go +++ b/pkg/servicelogger/logger.go @@ -1,5 +1,5 @@ // -// Copyright 2023 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package servicelogger import ( + "context" "io" "go.uber.org/zap" @@ -51,16 +52,27 @@ func InitZapLogger(env string) (log.Logger, error) { } // LogAndMaskErr records an error but masks it with a generic one -// to avoid leaking sensitive information -func LogAndMaskErr(in error, logger *log.Helper) error { +// to avoid leaking sensitive information. +// The error is reported on the Sentry hub of ctx when the request has one, so it carries +// the scope of its own request, and on the global hub otherwise. +func LogAndMaskErr(ctx context.Context, in error, logger *log.Helper) error { if logger != nil { logger.Error(in) } - sentry.CaptureException(in) + SentryHub(ctx).CaptureException(in) return errors.InternalServer("internal error", "server error") } +// SentryHub returns the Sentry hub of the request in ctx, or the global hub when ctx has none +func SentryHub(ctx context.Context) *sentry.Hub { + if hub := sentry.GetHubFromContext(ctx); hub != nil { + return hub + } + + return sentry.CurrentHub() +} + // ScopedHelper returns a new helper with information about the current component func ScopedHelper(logger log.Logger, scope string) *log.Helper { if logger == nil { diff --git a/pkg/servicelogger/logger_test.go b/pkg/servicelogger/logger_test.go new file mode 100644 index 000000000..df61cf6f8 --- /dev/null +++ b/pkg/servicelogger/logger_test.go @@ -0,0 +1,85 @@ +// +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package servicelogger + +import ( + "context" + "errors" + "testing" + + "github.com/getsentry/sentry-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newRecordingHub returns a hub tagged with org whose events are appended to events +func newRecordingHub(t *testing.T, org string, events *[]*sentry.Event) *sentry.Hub { + t.Helper() + + client, err := sentry.NewClient(sentry.ClientOptions{ + BeforeSend: func(event *sentry.Event, _ *sentry.EventHint) *sentry.Event { + *events = append(*events, event) + return nil + }, + }) + require.NoError(t, err) + + scope := sentry.NewScope() + scope.SetTag("org.name", org) + return sentry.NewHub(client, scope) +} + +func TestLogAndMaskErrCapturesOnTheRequestHub(t *testing.T) { + var requestEvents, globalEvents []*sentry.Event + requestHub := newRecordingHub(t, "request-org", &requestEvents) + + // Swap the global hub for the test so that events that fall back to it are recorded + global := sentry.CurrentHub() + globalHub := newRecordingHub(t, "global-org", &globalEvents) + global.BindClient(globalHub.Client()) + t.Cleanup(func() { global.BindClient(nil) }) + + testCases := []struct { + name string + ctx context.Context + wantEvents *[]*sentry.Event + wantOrg string + }{ + { + name: "context with a request hub", + ctx: sentry.SetHubOnContext(context.Background(), requestHub), + wantEvents: &requestEvents, + wantOrg: "request-org", + }, + { + name: "context without a hub falls back to the global hub", + ctx: context.Background(), + wantEvents: &globalEvents, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + requestEvents, globalEvents = requestEvents[:0], globalEvents[:0] + + require.Error(t, LogAndMaskErr(tc.ctx, errors.New("boom"), nil)) + + require.Len(t, *tc.wantEvents, 1) + assert.Equal(t, 1, len(requestEvents)+len(globalEvents), "the error must be reported on one hub only") + assert.Equal(t, tc.wantOrg, (*tc.wantEvents)[0].Tags["org.name"]) + }) + } +}