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 402b552dc..cf0e08c48 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context.go +++ b/app/controlplane/internal/sentrycontext/sentry_context.go @@ -33,29 +33,55 @@ 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" +) + +// 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 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. + if org != nil { + scope.SetTag(tagOrgID, org.ID) + scope.SetTag(tagOrgName, org.Name) + } } // 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..e6afeb0c7 100644 --- a/app/controlplane/internal/sentrycontext/sentry_context_test.go +++ b/app/controlplane/internal/sentrycontext/sentry_context_test.go @@ -21,8 +21,11 @@ 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" ) @@ -65,6 +68,100 @@ func TestNewSentryContext(t *testing.T) { assert.NoError(t, err) } +// 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"} + orgTags := map[string]string{tagOrgID: "org-id", tagOrgName: "my-org"} + + testCases := []struct { + name string + middlewares []middleware.Middleware + org *entities.Org + wantTags map[string]string + }{ + { + name: "request with an org", + middlewares: []middleware.Middleware{NewSentryHub(), NewSentryContext()}, + org: org, + wantTags: orgTags, + }, + { + name: "request without an org", + middlewares: []middleware.Middleware{NewSentryHub(), NewSentryContext()}, + 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) { + 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 + } + + _, 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"]) + }) + } +}