|
1 | 1 | // |
2 | | -// Copyright 2024-2025 The Chainloop Authors. |
| 2 | +// Copyright 2024-2026 The Chainloop Authors. |
3 | 3 | // |
4 | 4 | // Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | // you may not use this file except in compliance with the License. |
@@ -30,6 +30,7 @@ import ( |
30 | 30 | "github.com/chainloop-dev/chainloop/pkg/attestation" |
31 | 31 | "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" |
32 | 32 | "github.com/chainloop-dev/chainloop/pkg/attestation/verifier" |
| 33 | + "github.com/chainloop-dev/chainloop/pkg/cache" |
33 | 34 | "github.com/secure-systems-lab/go-securesystemslib/dsse" |
34 | 35 | protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" |
35 | 36 | "github.com/sigstore/sigstore/pkg/cryptoutils" |
@@ -103,24 +104,50 @@ type WorkflowRunRepo interface { |
103 | 104 | Expire(ctx context.Context, id uuid.UUID) error |
104 | 105 | } |
105 | 106 |
|
| 107 | +// AttestationBundleCache wraps cache.Cache[[]byte] to disambiguate from other |
| 108 | +// []byte caches (e.g. policy evaluation bundles) in the wire dependency graph. |
| 109 | +type AttestationBundleCache struct { |
| 110 | + cache.Cache[[]byte] |
| 111 | +} |
| 112 | + |
106 | 113 | type WorkflowRunUseCase struct { |
107 | 114 | wfRunRepo WorkflowRunRepo |
108 | 115 | wfRepo WorkflowRepo |
109 | 116 | logger *log.Helper |
110 | 117 | auditorUC *AuditorUseCase |
111 | 118 |
|
112 | 119 | signingUseCase *SigningUseCase |
| 120 | + bundleCache *AttestationBundleCache |
| 121 | + casClient CASClient |
| 122 | + casMappingUC *CASMappingUseCase |
| 123 | +} |
| 124 | + |
| 125 | +type WorkflowRunUseCaseOpts struct { |
| 126 | + WfrRepo WorkflowRunRepo |
| 127 | + WfRepo WorkflowRepo |
| 128 | + SigningUC *SigningUseCase |
| 129 | + AuditorUC *AuditorUseCase |
| 130 | + Logger log.Logger |
| 131 | + BundleCache *AttestationBundleCache |
| 132 | + CASClient CASClient |
| 133 | + CASMappingUC *CASMappingUseCase |
113 | 134 | } |
114 | 135 |
|
115 | | -func NewWorkflowRunUseCase(wfrRepo WorkflowRunRepo, wfRepo WorkflowRepo, suc *SigningUseCase, auditorUC *AuditorUseCase, logger log.Logger) (*WorkflowRunUseCase, error) { |
| 136 | +func NewWorkflowRunUseCase(opts *WorkflowRunUseCaseOpts) (*WorkflowRunUseCase, error) { |
| 137 | + logger := opts.Logger |
116 | 138 | if logger == nil { |
117 | 139 | logger = log.NewStdLogger(io.Discard) |
118 | 140 | } |
119 | 141 |
|
120 | 142 | return &WorkflowRunUseCase{ |
121 | | - wfRunRepo: wfrRepo, wfRepo: wfRepo, auditorUC: auditorUC, |
122 | | - signingUseCase: suc, |
| 143 | + wfRunRepo: opts.WfrRepo, |
| 144 | + wfRepo: opts.WfRepo, |
| 145 | + auditorUC: opts.AuditorUC, |
| 146 | + signingUseCase: opts.SigningUC, |
123 | 147 | logger: log.NewHelper(logger), |
| 148 | + bundleCache: opts.BundleCache, |
| 149 | + casClient: opts.CASClient, |
| 150 | + casMappingUC: opts.CASMappingUC, |
124 | 151 | }, nil |
125 | 152 | } |
126 | 153 |
|
@@ -535,29 +562,97 @@ func (uc *WorkflowRunUseCase) GetByDigestInOrgOrPublic(ctx context.Context, orgI |
535 | 562 | return workflowRunInOrgOrPublic(wfrun, orgUUID) |
536 | 563 | } |
537 | 564 |
|
| 565 | +// addAttestationFromBundle resolves the attestation bundle using cache → DB → CAS fallback. |
| 566 | +// Bundles are being migrated from the workflow run DB column into CAS; the layered resolution |
| 567 | +// provides backward compatibility and prepares for dropping the DB column. |
538 | 568 | func (uc *WorkflowRunUseCase) addAttestationFromBundle(ctx context.Context, wfRun *WorkflowRun) error { |
539 | | - // missing workflow run or attestation already there, do nothing |
540 | 569 | if wfRun == nil || wfRun.State != string(WorkflowRunSuccess) { |
541 | 570 | return nil |
542 | 571 | } |
543 | 572 |
|
544 | | - var bundle protobundle.Bundle |
545 | | - bundleBytes, err := uc.wfRunRepo.GetBundle(ctx, wfRun.ID) |
546 | | - if err != nil { |
547 | | - if IsNotFound(err) { |
548 | | - return nil |
| 573 | + if wfRun.Attestation == nil || wfRun.Attestation.Digest == "" { |
| 574 | + return nil |
| 575 | + } |
| 576 | + |
| 577 | + digest := wfRun.Attestation.Digest |
| 578 | + |
| 579 | + // Layer 1: cache lookup by digest |
| 580 | + if uc.bundleCache != nil { |
| 581 | + cached, found, err := uc.bundleCache.Get(ctx, digest) |
| 582 | + if err != nil { |
| 583 | + uc.logger.Warnw("msg", "attestation bundle cache get error", "digest", digest, "error", err) |
549 | 584 | } |
| 585 | + if found { |
| 586 | + return uc.applyBundle(wfRun, cached) |
| 587 | + } |
| 588 | + } |
| 589 | + |
| 590 | + // Layer 2: DB fallback |
| 591 | + bundleBytes, err := uc.wfRunRepo.GetBundle(ctx, wfRun.ID) |
| 592 | + if err != nil && !IsNotFound(err) { |
550 | 593 | return fmt.Errorf("retrieving bundle from repo: %w", err) |
551 | 594 | } |
552 | | - if err = protojson.Unmarshal(bundleBytes, &bundle); err != nil { |
| 595 | + |
| 596 | + if len(bundleBytes) > 0 { |
| 597 | + uc.cacheBundle(ctx, digest, bundleBytes) |
| 598 | + return uc.applyBundle(wfRun, bundleBytes) |
| 599 | + } |
| 600 | + |
| 601 | + // Layer 3: CAS download by digest |
| 602 | + bundleBytes, err = uc.downloadBundleFromCAS(ctx, digest, wfRun.Workflow.OrgID) |
| 603 | + if err != nil { |
| 604 | + uc.logger.Warnw("msg", "failed to download bundle from CAS", "digest", digest, "error", err) |
| 605 | + return nil |
| 606 | + } |
| 607 | + |
| 608 | + if len(bundleBytes) > 0 { |
| 609 | + uc.cacheBundle(ctx, digest, bundleBytes) |
| 610 | + return uc.applyBundle(wfRun, bundleBytes) |
| 611 | + } |
| 612 | + |
| 613 | + return nil |
| 614 | +} |
| 615 | + |
| 616 | +func (uc *WorkflowRunUseCase) applyBundle(wfRun *WorkflowRun, bundleBytes []byte) error { |
| 617 | + var bundle protobundle.Bundle |
| 618 | + if err := protojson.Unmarshal(bundleBytes, &bundle); err != nil { |
553 | 619 | return fmt.Errorf("unmarshalling bundle: %w", err) |
554 | 620 | } |
555 | 621 | wfRun.Attestation.Envelope = attestation.DSSEEnvelopeFromBundle(&bundle) |
556 | 622 | wfRun.Attestation.Bundle = bundleBytes |
557 | | - |
558 | 623 | return nil |
559 | 624 | } |
560 | 625 |
|
| 626 | +func (uc *WorkflowRunUseCase) cacheBundle(ctx context.Context, digest string, data []byte) { |
| 627 | + if uc.bundleCache == nil { |
| 628 | + return |
| 629 | + } |
| 630 | + if err := uc.bundleCache.Set(ctx, digest, data); err != nil { |
| 631 | + uc.logger.Warnw("msg", "failed to cache attestation bundle", "digest", digest, "error", err) |
| 632 | + } |
| 633 | +} |
| 634 | + |
| 635 | +func (uc *WorkflowRunUseCase) downloadBundleFromCAS(ctx context.Context, digest string, orgID uuid.UUID) ([]byte, error) { |
| 636 | + if uc.casClient == nil || uc.casMappingUC == nil { |
| 637 | + return nil, nil |
| 638 | + } |
| 639 | + |
| 640 | + mapping, err := uc.casMappingUC.FindCASMappingForDownloadByOrg(ctx, digest, []uuid.UUID{orgID}, nil) |
| 641 | + if err != nil { |
| 642 | + return nil, fmt.Errorf("finding CAS mapping: %w", err) |
| 643 | + } |
| 644 | + if mapping == nil { |
| 645 | + return nil, nil |
| 646 | + } |
| 647 | + |
| 648 | + var buf bytes.Buffer |
| 649 | + if err := uc.casClient.Download(ctx, string(mapping.CASBackend.Provider), mapping.CASBackend.SecretName, &buf, digest); err != nil { |
| 650 | + return nil, fmt.Errorf("downloading attestation bundle: %w", err) |
| 651 | + } |
| 652 | + |
| 653 | + return buf.Bytes(), nil |
| 654 | +} |
| 655 | + |
561 | 656 | // filter the workflow runs that belong to the org or are public |
562 | 657 | func workflowRunInOrgOrPublic(wfRun *WorkflowRun, orgID uuid.UUID) (*WorkflowRun, error) { |
563 | 658 | if wfRun == nil || (wfRun.Workflow.OrgID != orgID && !wfRun.Workflow.Public) { |
|
0 commit comments