Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/controlplane/pkg/biz/.mockery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ packages:
interfaces:
APITokenRepo:
CASBackendRepo:
CASMappingRepo:
OrganizationRepo:
WorkflowRunRepo:
99 changes: 19 additions & 80 deletions app/controlplane/pkg/biz/casmapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package biz
import (
"context"
"fmt"
"slices"
"time"

"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
Expand Down Expand Up @@ -50,8 +49,12 @@ type CASMappingFindOptions struct {
type CASMappingRepo interface {
// Create a mapping with an optional workflow run id
Create(ctx context.Context, digest string, casBackendID uuid.UUID, opts *CASMappingCreateOpts) (*CASMapping, error)
// List all the CAS mappings for the given digest
FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error)
// FindByDigestInOrgs returns a single accessible mapping for the digest within the given orgs
// (honouring project RBAC), preferring the default backend. Returns (nil, nil) when none exists.
FindByDigestInOrgs(ctx context.Context, digest string, orgs []uuid.UUID, projectIDs map[uuid.UUID][]uuid.UUID) (*CASMapping, error)
// FindPublicByDigest returns a single public mapping for the digest, preferring the default
// backend. Returns (nil, nil) when no public mapping exists.
FindPublicByDigest(ctx context.Context, digest string) (*CASMapping, error)
}

type CASMappingUseCase struct {
Expand Down Expand Up @@ -87,13 +90,6 @@ func (uc *CASMappingUseCase) Create(ctx context.Context, digest string, casBacke
return uc.repo.Create(ctx, digest, casBackendUUID, opts)
}

func (uc *CASMappingUseCase) FindByDigest(ctx context.Context, digest string) ([]*CASMapping, error) {
ctx, span := otelx.Start(ctx, casMappingTracer, "CASMappingUseCase.FindByDigest")
defer span.End()

return uc.repo.FindByDigest(ctx, digest)
}

// FindCASMappingForDownloadByUser returns the CASMapping appropriate for the given digest and user.
// This means, in order:
// 1 - Any mapping that points to an organization which the user is member of.
Expand Down Expand Up @@ -146,84 +142,27 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context,
return nil, NewErrValidationStr("no organizations provided")
}

// 1 - All CAS mappings for the given digest
mappings, err := uc.repo.FindByDigest(ctx, digest)
// 1 - A mapping reachable through one of the user's orgs (honouring project RBAC), selected and
// bounded in the database. This is the common path and stays cheap regardless of how many
// mappings a digest has accumulated.
mapping, err := uc.repo.FindByDigestInOrgs(ctx, digest, orgs, projectIDs)
if err != nil {
return nil, fmt.Errorf("failed to list cas mappings: %w", err)
}

uc.logger.Debugw("msg", fmt.Sprintf("found %d entries globally", len(mappings)), "digest", digest, "orgs", orgs)
if len(mappings) == 0 {
return nil, NewErrNotFound("digest not found in any mapping")
return nil, fmt.Errorf("failed to find cas mapping in orgs: %w", err)
} else if mapping != nil {
return mapping, nil
}

// 2 - CAS mappings associated with the given list of orgs and project IDs
orgMappings, err := filterByOrgs(mappings, orgs, projectIDs)
// 2 - Otherwise, fall back to a public mapping. This only runs when the requester has no
// org-level access to the digest.
mapping, err = uc.repo.FindPublicByDigest(ctx, digest)
if err != nil {
return nil, fmt.Errorf("failed to load mappings associated to an user: %w", err)
} else if len(orgMappings) > 0 {
return defaultOrFirst(orgMappings), nil
}

// 3 - mappings that are public
publicMappings := filterByPublic(mappings)
// The user has not access to neither proprietary nor public mappings
if len(publicMappings) == 0 {
return nil, fmt.Errorf("failed to find public cas mapping: %w", err)
} else if mapping == nil {
uc.logger.Warnw("msg", "digest exist but user does not have access to it", "digest", digest, "orgs", orgs)
return nil, NewErrNotFound("digest not found in any mapping")
}

// Pick the appropriate mapping from multiple ones
return defaultOrFirst(publicMappings), nil
}

// Extract only the mappings associated with a list of orgs and optionally a list of projects
func filterByOrgs(mappings []*CASMapping, orgs []uuid.UUID, projectIDs map[uuid.UUID][]uuid.UUID) ([]*CASMapping, error) {
result := make([]*CASMapping, 0)

for _, mapping := range mappings {
for _, o := range orgs {
if mapping.OrgID == o {
if visibleProjects, ok := projectIDs[mapping.OrgID]; ok {
if slices.Contains(visibleProjects, mapping.ProjectID) {
result = append(result, mapping)
}
} else {
result = append(result, mapping)
}
}
}
}

return result, nil
}

func filterByPublic(mappings []*CASMapping) []*CASMapping {
result := make([]*CASMapping, 0)

for _, mapping := range mappings {
if mapping.Public {
result = append(result, mapping)
}
}

return result
}

func defaultOrFirst(mappings []*CASMapping) *CASMapping {
if len(mappings) == 0 {
return nil
}

result := mappings[0]
for _, mapping := range mappings {
if mapping.CASBackend.Default {
result = mapping
break
}
}

return result
return mapping, nil
}

type CASMappingLookupRef struct {
Expand Down
205 changes: 126 additions & 79 deletions app/controlplane/pkg/biz/casmapping_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,89 +158,131 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadByOrg() {
})
}

func (s *casMappingIntegrationSuite) TestFindByDigest() {
// 1. Digest: validDigest, CASBackend: casBackend1, WorkflowRunID: workflowRun
// 2. Digest: validDigest2, CASBackend: casBackend1, WorkflowRunID: workflowRun
// 3. Digest: validDigest, CASBackend: casBackend2, WorkflowRunID: workflowRun
// 4. Digest: validDigest, CASBackend: casBackend3, WorkflowRunID: publicWorkflowRun
_, err := s.CASMapping.Create(context.TODO(), validDigest, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(context.TODO(), validDigest2, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend2.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
// When a digest is reachable through several CAS backends, the download lookup must return the
// mapping stored in the default backend, regardless of the order the mappings were created in.
// This locks in the defaultOrFirst behaviour for both the org-scoped and the public fallback paths.
func (s *casMappingIntegrationSuite) TestCASMappingForDownloadPrefersDefaultBackend() {
ctx := context.Background()

// org1 already has casBackend1 as its default backend. Add a second, non-default backend.
nonDefaultBackend, err := s.CASBackend.Create(ctx, s.org1.ID, randomName(), "my-location", "non-default backend", backendType, nil, false, false, nil)
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(context.TODO(), validDigest, s.casBackend3.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID})
s.Require().False(nonDefaultBackend.Default)

s.Run("org download returns the default backend even when it is mapped last", func() {
// Map the digest to the non-default backend FIRST, then to the default one.
_, err := s.CASMapping.Create(ctx, validDigest, nonDefaultBackend.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(ctx, validDigest, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
require.NoError(s.T(), err)

mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil)
s.NoError(err)
s.Require().NotNil(mapping)
s.Equal(s.casBackend1.ID, mapping.CASBackend.ID)
})

s.Run("org download returns the non-default backend when no default mapping exists", func() {
_, err := s.CASMapping.Create(ctx, validDigest2, nonDefaultBackend.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
require.NoError(s.T(), err)

mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest2, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil)
s.NoError(err)
s.Require().NotNil(mapping)
s.Equal(nonDefaultBackend.ID, mapping.CASBackend.ID)
})

s.Run("public download returns the default backend even when it is mapped last", func() {
// Public mappings (workflow is public) across two backends, non-default created first.
_, err := s.CASMapping.Create(ctx, validDigestPublic, nonDefaultBackend.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID})
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(ctx, validDigestPublic, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID})
require.NoError(s.T(), err)

// A requester with no access to org1 falls back to the public mappings.
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestPublic, []uuid.UUID{uuid.New()}, nil)
s.NoError(err)
s.Require().NotNil(mapping)
s.Equal(s.casBackend1.ID, mapping.CASBackend.ID)
})
}

// When RBAC is enabled for an org (projectIDs carries an entry for it), only mappings whose project
// is in the visible set are reachable through that org.
func (s *casMappingIntegrationSuite) TestCASMappingForDownloadRBAC() {
ctx := context.Background()
orgUUID := uuid.MustParse(s.org1.ID)

// A mapping in org1 scoped to a specific project.
_, err := s.CASMapping.Create(ctx, validDigest, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{
WorkflowRunID: &s.workflowRun.ID,
ProjectID: &s.projectID,
})
require.NoError(s.T(), err)

testcases := []struct {
name string
digest string
want []*biz.CASMapping
wantErr bool
}{
{
name: "validDigest",
digest: validDigest,
want: []*biz.CASMapping{
{
Digest: validDigest,
CASBackend: &biz.CASBackend{ID: s.casBackend1.ID},
WorkflowRunID: s.workflowRun.ID,
OrgID: s.casBackend1.OrganizationID,
Public: false,
},
{
Digest: validDigest,
CASBackend: &biz.CASBackend{ID: s.casBackend2.ID},
WorkflowRunID: s.workflowRun.ID,
OrgID: s.casBackend2.OrganizationID,
Public: false,
},
{
Digest: validDigest,
CASBackend: &biz.CASBackend{ID: s.casBackend3.ID},
WorkflowRunID: s.publicWorkflowRun.ID,
OrgID: s.casBackend3.OrganizationID,
Public: true,
},
},
},
{
name: "validDigest2",
digest: validDigest2,
want: []*biz.CASMapping{
{
Digest: validDigest2,
CASBackend: &biz.CASBackend{ID: s.casBackend1.ID},
WorkflowRunID: s.workflowRun.ID,
OrgID: s.casBackend1.OrganizationID,
Public: false,
},
},
},
{
name: "invalidDigest",
digest: invalidDigest,
want: []*biz.CASMapping{},
},
}
s.Run("returned when the project is visible", func() {
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []uuid.UUID{orgUUID},
map[uuid.UUID][]uuid.UUID{orgUUID: {s.projectID}})
s.NoError(err)
s.Require().NotNil(mapping)
s.Equal(s.casBackend1.ID, mapping.CASBackend.ID)
})

for _, tc := range testcases {
s.Run(tc.name, func() {
got, err := s.CASMapping.FindByDigest(context.Background(), tc.digest)
if tc.wantErr {
s.Error(err)
} else {
s.NoError(err)
if diff := cmp.Diff(tc.want, got,
cmpopts.IgnoreFields(biz.CASMapping{}, "CreatedAt", "ID"),
cmpopts.IgnoreTypes(biz.CASBackend{}),
); diff != "" {
assert.Failf(s.T(), "mismatch (-want +got):\n%s", diff)
}
}
})
}
s.Run("not returned when the project is not visible", func() {
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []uuid.UUID{orgUUID},
map[uuid.UUID][]uuid.UUID{orgUUID: {uuid.New()}})
s.Error(err)
s.Nil(mapping)
})

s.Run("not returned when RBAC is enabled with no visible projects", func() {
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []uuid.UUID{orgUUID},
map[uuid.UUID][]uuid.UUID{orgUUID: {}})
s.Error(err)
s.Nil(mapping)
})
}

// Mappings pointing to a soft-deleted backend, or produced by a soft-deleted workflow, must not be
// served for download.
func (s *casMappingIntegrationSuite) TestCASMappingForDownloadSkipsSoftDeleted() {
ctx := context.Background()

s.Run("org download skips a mapping whose backend is soft-deleted", func() {
backend, err := s.CASBackend.Create(ctx, s.org1.ID, randomName(), "my-location", "to be deleted", backendType, nil, false, false, nil)
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(ctx, validDigest3, backend.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.workflowRun.ID})
require.NoError(s.T(), err)

// Reachable before the backend is deleted.
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest3, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil)
s.NoError(err)
s.Require().NotNil(mapping)

require.NoError(s.T(), s.CASBackend.SoftDelete(ctx, s.org1.ID, backend.ID.String()))

// The only mapping points to a deleted backend, so it is no longer served.
mapping, err = s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest3, []uuid.UUID{uuid.MustParse(s.org1.ID)}, nil)
s.Error(err)
s.Nil(mapping)
})

s.Run("public download skips a mapping whose workflow is soft-deleted", func() {
_, err := s.CASMapping.Create(ctx, validDigest2, s.casBackend1.ID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: &s.publicWorkflowRun.ID})
require.NoError(s.T(), err)

// A non-member can reach it through the public fallback while the workflow is public.
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest2, []uuid.UUID{uuid.New()}, nil)
s.NoError(err)
s.Require().NotNil(mapping)

require.NoError(s.T(), s.Workflow.Delete(ctx, s.org1.ID, s.publicWorkflow.ID.String()))

// Once the workflow is soft-deleted the mapping is no longer public.
mapping, err = s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest2, []uuid.UUID{uuid.New()}, nil)
s.Error(err)
s.Nil(mapping)
})
}

func (s *casMappingIntegrationSuite) TestCreate() {
Expand Down Expand Up @@ -342,6 +384,8 @@ type casMappingIntegrationSuite struct {
testhelpers.UseCasesEachTestSuite
casBackend1, casBackend2, casBackend3 *biz.CASBackend
workflowRun, publicWorkflowRun *biz.WorkflowRun
publicWorkflow *biz.Workflow
projectID uuid.UUID
userOrg1And2, userOrg2 *biz.User
org1, org2, orgNoUsers *biz.Organization
}
Expand Down Expand Up @@ -379,8 +423,11 @@ func (s *casMappingIntegrationSuite) SetupTest() {
workflow, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test-workflow", OrgID: s.org1.ID, Project: "test-project"})
assert.NoError(err)

s.projectID = workflow.ProjectID

publicWorkflow, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{Name: "test-workflow-public", OrgID: s.org1.ID, Public: true, Project: "test-project"})
assert.NoError(err)
s.publicWorkflow = publicWorkflow

// Find contract revision
contractVersion, err := s.WorkflowContract.Describe(ctx, s.org1.ID, workflow.ContractID.String(), 0)
Expand Down
Loading
Loading