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
58 changes: 35 additions & 23 deletions app/controlplane/internal/biz/casmapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,73 +79,85 @@ func (uc *CASMappingUseCase) FindByDigest(ctx context.Context, digest string) ([
return uc.repo.FindByDigest(ctx, digest)
}

// FindCASMappingForDownload returns the CASMapping appropriate for the given digest and user
// 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
// 1.1 If there are multiple mappings, it will pick the default one or the first one
// 2 - Any mapping that is public
func (uc *CASMappingUseCase) FindCASMappingForDownload(ctx context.Context, digest string, userID string) (*CASMapping, error) {
func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context, digest string, userID string) (*CASMapping, error) {
uc.logger.Infow("msg", "finding cas mapping for download", "digest", digest, "user", userID)

userUUID, err := uuid.Parse(userID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}

if _, err = cr_v1.NewHash(digest); err != nil {
// Load organizations for the given user
memberships, err := uc.membershipRepo.FindByUser(ctx, userUUID)
if err != nil {
return nil, fmt.Errorf("failed to list memberships: %w", err)
}

userOrgs := make([]string, 0, len(memberships))
for _, m := range memberships {
userOrgs = append(userOrgs, m.OrganizationID.String())
}

return uc.FindCASMappingForDownloadByOrg(ctx, digest, userOrgs)
}

func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context, digest string, orgs []string) (*CASMapping, error) {
if _, err := cr_v1.NewHash(digest); err != nil {
return nil, NewErrValidation(fmt.Errorf("invalid digest format: %w", err))
}

if len(orgs) == 0 {
return nil, NewErrValidationStr("no organizations provided")
}

// 1 - All CAS mappings for the given digest
mappings, err := uc.repo.FindByDigest(ctx, digest)
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, "user", userID)
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")
}

// 2 - CAS mappings that the user has access to.
// This means any mapping that points to an organization which the user is member of
userMappings, err := filterByUser(ctx, mappings, userUUID, uc.membershipRepo)
// 2 - CAS mappings associated with the given list of orgs
orgMappings, err := filterByOrgs(mappings, orgs)
if err != nil {
return nil, fmt.Errorf("failed to load mappings associated to an user: %w", err)
} else if len(userMappings) > 0 {
result := defaultOrFirst(userMappings)
uc.logger.Infow("msg", "mapping found!", "digest", digest, "user", userID, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
} else if len(orgMappings) > 0 {
result := defaultOrFirst(orgMappings)

uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
return result, nil
}

// 3 - mappings that are public
publicMappings := filterByPublic(mappings)
// The user has not access to neither proprietary nor public mappings
if len(publicMappings) == 0 {
uc.logger.Warnw("msg", "digest exist but user does not have access to it", "digest", digest, "user", userID)
uc.logger.Warnw("msg", "digest exist but user does not have access to it", "digest", digest, "orgs", orgs)
return nil, NewErrUnauthorized(errors.New("unauthorized access to the artifact"))
}

// Pick the appropriate mapping from multiple ones
result := defaultOrFirst(publicMappings)
uc.logger.Infow("msg", "mapping found!", "digest", digest, "user", userID, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
uc.logger.Infow("msg", "mapping found!", "digest", digest, "orgs", orgs, "casBackend", result.CASBackend.ID, "default", result.CASBackend.Default, "public", result.Public)
return result, nil
}

// get the casMapping based on
// 1 - the mapping is part of an organization an user has access to
// 2 - if there is more than one, pick the default if possible
func filterByUser(ctx context.Context, mappings []*CASMapping, userID uuid.UUID, mRepo MembershipRepo) ([]*CASMapping, error) {
// Extract only the mappings associated with a list of orgs
func filterByOrgs(mappings []*CASMapping, orgs []string) ([]*CASMapping, error) {
result := make([]*CASMapping, 0)

memberships, err := mRepo.FindByUser(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to list memberships: %w", err)
}

for _, mapping := range mappings {
for _, m := range memberships {
if mapping.OrgID == m.OrganizationID {
for _, o := range orgs {
if mapping.OrgID.String() == o {
result = append(result, mapping)
}
}
Expand Down
47 changes: 38 additions & 9 deletions app/controlplane/internal/biz/casmapping_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const (
invalidDigest = "sha256:deadbeef"
)

func (s *casMappingIntegrationSuite) TestCASMappingForDownload() {
func (s *casMappingIntegrationSuite) TestCASMappingForDownloadUser() {
// Let's create 3 CASMappings:
// 1. Digest: validDigest, CASBackend: casBackend1, WorkflowRunID: workflowRun
// 2. Digest: validDigest, CASBackend: casBackend2, WorkflowRunID: workflowRun
Expand All @@ -60,55 +60,84 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownload() {
// Since the userOrg1And2 is member of org1 and org2, she should be able to download
// both validDigest and validDigest2 from two different orgs
s.Run("userOrg1And2 can download validDigest from org1", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest, s.userOrg1And2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigest, s.userOrg1And2.ID)
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend1.ID, mapping.CASBackend.ID)
})

s.Run("userOrg1And2 can download validDigest2 from org2", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest2, s.userOrg1And2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigest2, s.userOrg1And2.ID)
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend2.ID, mapping.CASBackend.ID)
})

s.Run("userOrg1And2 can not download validDigest3 from org3", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest3, s.userOrg1And2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigest3, s.userOrg1And2.ID)
s.Error(err)
s.Nil(mapping)
})

s.Run("userOrg1And2 can download validDigestPublic from org3", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigestPublic, s.userOrg1And2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigestPublic, s.userOrg1And2.ID)
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend3.ID, mapping.CASBackend.ID)
})

s.Run("userOrg2 can download validDigest2 from org2", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest2, s.userOrg2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigest2, s.userOrg2.ID)
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend2.ID, mapping.CASBackend.ID)
})

s.Run("userOrg2 can download validDigestPublic from org3", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigestPublic, s.userOrg2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigestPublic, s.userOrg2.ID)
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend3.ID, mapping.CASBackend.ID)
})

s.Run("userOrg2 can download validDigest from org2", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), validDigest, s.userOrg2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), validDigest, s.userOrg2.ID)
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend2.ID, mapping.CASBackend.ID)
})

s.Run("userOrg2 can not download invalidDigest", func() {
mapping, err := s.CASMapping.FindCASMappingForDownload(context.TODO(), invalidDigest, s.userOrg2.ID)
mapping, err := s.CASMapping.FindCASMappingForDownloadByUser(context.TODO(), invalidDigest, s.userOrg2.ID)
s.Error(err)
s.Nil(mapping)
})
}

func (s *casMappingIntegrationSuite) TestCASMappingForDownloadByOrg() {
ctx := context.Background()
_, err := s.CASMapping.Create(ctx, validDigest, s.casBackend1.ID.String(), s.workflowRun.ID.String())
require.NoError(s.T(), err)
_, err = s.CASMapping.Create(ctx, validDigestPublic, s.casBackend3.ID.String(), s.publicWorkflowRun.ID.String())
require.NoError(s.T(), err)

// both validDigest and validDigest2 from two different orgs
s.Run("validDigest is in org1", func() {
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigest, []string{s.org1.ID})
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend1.ID, mapping.CASBackend.ID)
})

s.Run("validDigestPublic is available from any org", func() {
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, validDigestPublic, []string{uuid.NewString()})
s.NoError(err)
s.NotNil(mapping)
s.Equal(s.casBackend3.ID, mapping.CASBackend.ID)
})

s.Run("can't find an invalid digest", func() {
mapping, err := s.CASMapping.FindCASMappingForDownloadByOrg(ctx, invalidDigest, []string{s.org1.ID})
s.Error(err)
s.Nil(mapping)
})
Expand Down
8 changes: 6 additions & 2 deletions app/controlplane/internal/biz/referrer.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,10 @@ func (s *ReferrerUseCase) ExtractAndPersist(ctx context.Context, att *dsse.Envel
return nil
}

// GetFromRoot returns the referrer identified by the provided content digest, including its first-level references
// GetFromRootUser returns the referrer identified by the provided content digest, including its first-level references
// For example if sha:deadbeef represents an attestation, the result will contain the attestation + materials associated to it
// It only returns referrers that belong to organizations the user is member of
func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind, userID string) (*StoredReferrer, error) {
func (s *ReferrerUseCase) GetFromRootUser(ctx context.Context, digest, rootKind, userID string) (*StoredReferrer, error) {
userUUID, err := uuid.Parse(userID)
if err != nil {
return nil, NewErrInvalidUUID(err)
Expand All @@ -166,6 +166,10 @@ func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind, use
orgIDs = append(orgIDs, m.OrganizationID)
}

return s.GetFromRoot(ctx, digest, rootKind, orgIDs)
}

func (s *ReferrerUseCase) GetFromRoot(ctx context.Context, digest, rootKind string, orgIDs []uuid.UUID) (*StoredReferrer, error) {
filters := make([]GetFromRootFilter, 0)
if rootKind != "" {
filters = append(filters, WithKind(rootKind))
Expand Down
38 changes: 19 additions & 19 deletions app/controlplane/internal/biz/referrer_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (s *referrerIntegrationTestSuite) TestGetFromRootInPublicSharedIndex() {
s.T().Run("storing it associated with a private workflow keeps it private and not in the index", func(t *testing.T) {
err = s.sharedEnabledUC.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
require.NoError(s.T(), err)
ref, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
ref, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
s.False(ref.InPublicWorkflow)
res, err := s.sharedEnabledUC.GetFromRootInPublicSharedIndex(ctx, wantReferrerAtt.Digest, "")
Expand All @@ -69,7 +69,7 @@ func (s *referrerIntegrationTestSuite) TestGetFromRootInPublicSharedIndex() {
err = s.sharedEnabledUC.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
require.NoError(s.T(), err)
// It's marked as public in the internal index
ref, err := s.sharedEnabledUC.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
ref, err := s.sharedEnabledUC.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
s.True(ref.InPublicWorkflow)

Expand Down Expand Up @@ -165,21 +165,21 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
s.T().Run("it can store properly the first time", func(t *testing.T) {
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
s.NoError(err)
prevStoredRef, err = s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
prevStoredRef, err = s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
})

s.T().Run("and it's idempotent", func(t *testing.T) {
err := s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow1.ID.String())
s.NoError(err)
ref, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
ref, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
// Check it's the same referrer than previously retrieved, including timestamps
s.Equal(prevStoredRef, ref)
})

s.T().Run("contains all the info", func(t *testing.T) {
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
// parent i.e attestation
s.Equal(wantReferrerAtt.Digest, got.Digest)
Expand All @@ -198,22 +198,22 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
})

s.T().Run("can get sha1 digests too", func(t *testing.T) {
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerCommit.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerCommit.Digest, "", s.user.ID)
s.NoError(err)
s.Equal(wantReferrerCommit.Digest, got.Digest)
})

s.T().Run("can't be accessed by a second user in another org", func(t *testing.T) {
// the user2 has not access to org1
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user2.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user2.ID)
s.True(biz.IsNotFound(err))
s.Nil(got)
})

s.T().Run("but another workflow can be attached", func(t *testing.T) {
err = s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
s.NoError(err)
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
require.Len(t, got.OrgIDs, 2)
s.Contains(got.OrgIDs, s.org1UUID)
Expand All @@ -222,7 +222,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
// and it's idempotent (no new orgs added)
err = s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
s.NoError(err)
got, err = s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
got, err = s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
require.Len(t, got.OrgIDs, 2)
s.Equal([]uuid.UUID{s.org1UUID, s.org2UUID}, got.OrgIDs)
Expand All @@ -232,13 +232,13 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
s.T().Run("and now user2 has access to it since it has access to workflow2 in org2", func(t *testing.T) {
err = s.Referrer.ExtractAndPersist(ctx, envelope, s.workflow2.ID.String())
s.NoError(err)
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user2.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user2.ID)
s.NoError(err)
require.Len(t, got.OrgIDs, 2)
})

s.T().Run("you can ask for info about materials that are subjects", func(t *testing.T) {
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerContainerImage.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerContainerImage.Digest, "", s.user.ID)
s.NoError(err)
// parent i.e attestation
s.Equal(wantReferrerContainerImage.Digest, got.Digest)
Expand All @@ -252,7 +252,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
})

s.T().Run("it might not have references", func(t *testing.T) {
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerSarif.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerSarif.Digest, "", s.user.ID)
s.NoError(err)
// parent i.e attestation
s.Equal(wantReferrerSarif.Digest, got.Digest)
Expand All @@ -262,7 +262,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
})

s.T().Run("or it does not exist", func(t *testing.T) {
got, err := s.Referrer.GetFromRoot(ctx, "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "", s.user.ID)
s.True(biz.IsNotFound(err))
s.Nil(got)
})
Expand All @@ -287,20 +287,20 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
s.NoError(err)

// but retrieval should fail. In the future we will ask the user to provide the artifact type in these cases of ambiguity
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerSarif.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerSarif.Digest, "", s.user.ID)
s.Nil(got)
s.ErrorContains(err, "present in 2 kinds")
})

s.T().Run("it should not fail on retrieval if we filter out by one kind", func(t *testing.T) {
// but retrieval should fail. In the future we will ask the user to provide the artifact type in these cases of ambiguity
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerSarif.Digest, "SARIF", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerSarif.Digest, "SARIF", s.user.ID)
s.NoError(err)
s.Equal(wantReferrerSarif.Digest, got.Digest)
s.Equal(true, got.Downloadable)
s.Equal("SARIF", got.Kind)

got, err = s.Referrer.GetFromRoot(ctx, wantReferrerSarif.Digest, "ARTIFACT", s.user.ID)
got, err = s.Referrer.GetFromRootUser(ctx, wantReferrerSarif.Digest, "ARTIFACT", s.user.ID)
s.NoError(err)
s.Equal(wantReferrerSarif.Digest, got.Digest)
s.Equal(true, got.Downloadable)
Expand All @@ -309,7 +309,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {

s.T().Run("now there should a container image pointing to two attestations", func(t *testing.T) {
// but retrieval should fail. In the future we will ask the user to provide the artifact type in these cases of ambiguity
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerContainerImage.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerContainerImage.Digest, "", s.user.ID)
s.NoError(err)
// it should be referenced by two attestations since it's subject of both
require.Len(t, got.References, 2)
Expand All @@ -320,7 +320,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
})

s.T().Run("if all associated workflows are private, the referrer is private", func(t *testing.T) {
got, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
s.False(got.InPublicWorkflow)
s.Equal([]uuid.UUID{s.workflow1.ID, s.workflow2.ID}, got.WorkflowIDs)
Expand All @@ -334,7 +334,7 @@ func (s *referrerIntegrationTestSuite) TestExtractAndPersists() {
_, err := s.Workflow.Update(ctx, s.org1.ID, s.workflow1.ID.String(), &biz.WorkflowUpdateOpts{Public: toPtrBool(true)})
require.NoError(t, err)

got, err := s.Referrer.GetFromRoot(ctx, wantReferrerAtt.Digest, "", s.user.ID)
got, err := s.Referrer.GetFromRootUser(ctx, wantReferrerAtt.Digest, "", s.user.ID)
s.NoError(err)
s.True(got.InPublicWorkflow)
for _, r := range got.References {
Expand Down
Loading