Skip to content
Open
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
2 changes: 1 addition & 1 deletion app/controlplane/pkg/biz/casmapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context,
if err != nil {
return nil, fmt.Errorf("failed to find cas mapping in orgs: %w", err)
} else if mapping == nil {
uc.logger.Warnw("msg", "digest not accessible to the requesting orgs", "digest", digest, "orgs", orgs)
uc.logger.Warnw("msg", "digest not accessible to the requesting orgs", "digest", digest, "orgs", orgs, "projectIDs", projectIDs)
return nil, NewErrNotFound("digest not found in any mapping")
}

Expand Down
59 changes: 58 additions & 1 deletion app/controlplane/pkg/biz/casmapping_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package biz_test
import (
"context"
"testing"
"time"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/testhelpers"
Expand Down Expand Up @@ -253,11 +254,29 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadSkipsSoftDeleted()
}

func (s *casMappingIntegrationSuite) TestCreate() {
ctx := context.Background()

// A project version ID is not a project ID. Storing one in cas_mappings.project_id makes the
// mapping unreachable for any role whose downloads are filtered by project RBAC.
projectVersion, err := s.ProjectVersion.Create(ctx, s.projectID.String(), "v1.0.0", false)
require.NoError(s.T(), err)

// A project living in a different organization than the CAS backend.
foreignProject, err := s.Project.Create(ctx, s.org2.ID, randomName())
require.NoError(s.T(), err)

// A soft-deleted project. Nothing sets projects.deleted_at today, so it is set directly here to
// cover the guard against a project that is no longer live.
deletedProject, err := s.Project.Create(ctx, s.org1.ID, randomName())
require.NoError(s.T(), err)
require.NoError(s.T(), s.Data.DB.Project.UpdateOneID(deletedProject.ID).SetDeletedAt(time.Now()).Exec(ctx))

testCases := []struct {
name string
digest string
casBackendID uuid.UUID
workflowRunID *uuid.UUID
projectID *uuid.UUID
wantErr bool
}{
{
Expand Down Expand Up @@ -311,6 +330,40 @@ func (s *casMappingIntegrationSuite) TestCreate() {
digest: validDigest,
casBackendID: s.casBackend1.ID,
},
{
name: "associated to a project",
digest: validDigest,
casBackendID: s.casBackend1.ID,
projectID: biz.ToPtr(s.projectID),
},
{
name: "non-existing project",
digest: validDigest,
casBackendID: s.casBackend1.ID,
projectID: biz.ToPtr(uuid.New()),
wantErr: true,
},
{
name: "a project version ID is not a valid project ID",
digest: validDigest,
casBackendID: s.casBackend1.ID,
projectID: biz.ToPtr(projectVersion.ID),
wantErr: true,
},
{
name: "project from another organization",
digest: validDigest,
casBackendID: s.casBackend1.ID,
projectID: biz.ToPtr(foreignProject.ID),
wantErr: true,
},
{
name: "soft-deleted project",
digest: validDigest,
casBackendID: s.casBackend1.ID,
projectID: biz.ToPtr(deletedProject.ID),
wantErr: true,
},
}

for _, tc := range testCases {
Expand All @@ -324,8 +377,12 @@ func (s *casMappingIntegrationSuite) TestCreate() {
want.WorkflowRunID = *tc.workflowRunID
}

if tc.projectID != nil {
want.ProjectID = *tc.projectID
}

s.Run(tc.name, func() {
got, err := s.CASMapping.Create(context.TODO(), tc.digest, tc.casBackendID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: tc.workflowRunID})
got, err := s.CASMapping.Create(ctx, tc.digest, tc.casBackendID.String(), &biz.CASMappingCreateOpts{WorkflowRunID: tc.workflowRunID, ProjectID: tc.projectID})
if tc.wantErr {
s.Error(err)
} else {
Expand Down
19 changes: 19 additions & 0 deletions app/controlplane/pkg/data/casmapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casbackend"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casmapping"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun"
"github.com/chainloop-dev/chainloop/pkg/otelx"
"github.com/go-kratos/kratos/v2/log"
Expand Down Expand Up @@ -69,6 +70,24 @@ func (r *CASMappingRepo) Create(ctx context.Context, digest string, casBackendID
}
}

// project_id is not enforced at the database level, so an ID that belongs to no project (a
// project version ID, for instance) writes cleanly. Validate it here: a mapping whose project_id
// is not a live project of the backend's organization can never match the
// "organization_id = <org> AND project_id IN (<visible projects>)" filter used by
// FindByDigestInOrgs, making the artifact undownloadable for every role with RBAC enabled.
if opts != nil && opts.ProjectID != nil {
exists, err := r.data.DB.Project.Query().Where(
project.ID(*opts.ProjectID),
project.OrganizationID(casBackend.OrganizationID),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
project.DeletedAtIsNil(),
).Exist(ctx)
if err != nil {
return nil, fmt.Errorf("failed to check project: %w", err)
} else if !exists {
return nil, biz.NewErrNotFound(fmt.Sprintf("project %s in organization %s", opts.ProjectID, casBackend.OrganizationID))
}
}

query := r.data.DB.CASMapping.Create().
SetDigest(digest).
SetCasBackendID(casBackendID).
Expand Down
Loading