@@ -18,8 +18,10 @@ package biz
1818import (
1919 "context"
2020 "fmt"
21+ "slices"
2122 "time"
2223
24+ "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2325 "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
2426 "github.com/chainloop-dev/chainloop/pkg/servicelogger"
2527 "github.com/go-kratos/kratos/v2/log"
@@ -34,59 +36,62 @@ type CASMapping struct {
3436 Digest string
3537 CreatedAt * time.Time
3638 // A public mapping means that the material/attestation can be downloaded by anyone
37- Public bool
39+ Public bool
40+ ProjectID uuid.UUID
41+ }
42+
43+ type CASMappingFindOptions struct {
44+ Orgs []uuid.UUID
45+ ProjectIDs []uuid.UUID
3846}
3947
4048type CASMappingRepo interface {
4149 // Create a mapping with an optional workflow run id
42- Create (ctx context.Context , digest string , casBackendID uuid.UUID , workflowRunID * uuid. UUID ) (* CASMapping , error )
50+ Create (ctx context.Context , digest string , casBackendID uuid.UUID , opts * CASMappingCreateOpts ) (* CASMapping , error )
4351 // List all the CAS mappings for the given digest
4452 FindByDigest (ctx context.Context , digest string ) ([]* CASMapping , error )
4553}
4654
4755type CASMappingUseCase struct {
4856 repo CASMappingRepo
4957 membershipRepo MembershipRepo
58+ projectsRepo ProjectsRepo
5059 logger * log.Helper
5160}
5261
53- func NewCASMappingUseCase (repo CASMappingRepo , mRepo MembershipRepo , logger log.Logger ) * CASMappingUseCase {
54- return & CASMappingUseCase {repo , mRepo , servicelogger .ScopedHelper (logger , "cas-mapping-usecase" )}
62+ func NewCASMappingUseCase (repo CASMappingRepo , mRepo MembershipRepo , pRepo ProjectsRepo , logger log.Logger ) * CASMappingUseCase {
63+ return & CASMappingUseCase {repo , mRepo , pRepo , servicelogger .ScopedHelper (logger , "cas-mapping-usecase" )}
64+ }
65+
66+ type CASMappingCreateOpts struct {
67+ WorkflowRunID * uuid.UUID
68+ ProjectID * uuid.UUID
5569}
5670
5771// Create a mapping with an optional workflow run id
58- func (uc * CASMappingUseCase ) Create (ctx context.Context , digest string , casBackendID string , workflowRunID string ) (* CASMapping , error ) {
72+ func (uc * CASMappingUseCase ) Create (ctx context.Context , digest string , casBackendID string , opts * CASMappingCreateOpts ) (* CASMapping , error ) {
5973 casBackendUUID , err := uuid .Parse (casBackendID )
6074 if err != nil {
6175 return nil , NewErrInvalidUUID (err )
6276 }
6377
64- var workflowRunUUID * uuid.UUID
65- if workflowRunID != "" {
66- runUUID , err := uuid .Parse (workflowRunID )
67- if err != nil {
68- return nil , NewErrInvalidUUID (err )
69- }
70- workflowRunUUID = & runUUID
71- }
72-
7378 // parse the digest to make sure is a valid sha256 sum
7479 if _ , err = cr_v1 .NewHash (digest ); err != nil {
7580 return nil , NewErrValidation (fmt .Errorf ("invalid digest format: %w" , err ))
7681 }
7782
78- return uc .repo .Create (ctx , digest , casBackendUUID , workflowRunUUID )
83+ return uc .repo .Create (ctx , digest , casBackendUUID , opts )
7984}
8085
8186func (uc * CASMappingUseCase ) FindByDigest (ctx context.Context , digest string ) ([]* CASMapping , error ) {
8287 return uc .repo .FindByDigest (ctx , digest )
8388}
8489
85- // FindCASMappingForDownloadByUser returns the CASMapping appropriate for the given digest and user
86- // This means, in order
87- // 1 - Any mapping that points to an organization which the user is member of
88- // 1.1 If there are multiple mappings, it will pick the default one or the first one
89- // 2 - Any mapping that is public
90+ // FindCASMappingForDownloadByUser returns the CASMapping appropriate for the given digest and user.
91+ // This means, in order:
92+ // 1 - Any mapping that points to an organization which the user is member of.
93+ // 1.1 If there are multiple mappings, it will pick the default one or the first one.
94+ // 2 - Any mapping that is public.
9095func (uc * CASMappingUseCase ) FindCASMappingForDownloadByUser (ctx context.Context , digest string , userID string ) (* CASMapping , error ) {
9196 uc .logger .Infow ("msg" , "finding cas mapping for download" , "digest" , digest , "user" , userID )
9297
@@ -95,26 +100,42 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByUser(ctx context.Context
95100 return nil , NewErrInvalidUUID (err )
96101 }
97102
98- // Load organizations for the given user
99- memberships , err := uc .membershipRepo .FindByUser (ctx , userUUID )
103+ // Load ALL memberships for the given user
104+ memberships , err := uc .membershipRepo .ListAllByUser (ctx , userUUID )
100105 if err != nil {
101106 return nil , fmt .Errorf ("failed to list memberships: %w" , err )
102107 }
103108
104- userOrgs := make ([]string , 0 , len (memberships ))
109+ userOrgs := make ([]uuid.UUID , 0 )
110+ // This map holds the list of project IDs by org with RBAC active (user is org "member")
111+ projectIDs := make (map [uuid.UUID ][]uuid.UUID )
105112 for _ , m := range memberships {
106- userOrgs = append (userOrgs , m .OrganizationID .String ())
113+ if m .ResourceType == authz .ResourceTypeOrganization {
114+ userOrgs = append (userOrgs , m .ResourceID )
115+ // If the role in the org is member, we must enable RBAC for projects.
116+ if m .Role == authz .RoleOrgMember {
117+ // get list of projects in org, and match it with the memberships to build a filter
118+ orgProjects , err := getProjectsWithMembership (ctx , uc .projectsRepo , m .ResourceID , memberships )
119+ if err != nil {
120+ return nil , err
121+ }
122+ // note that appending an empty slice to a nil slice doesn't change it (it's still nil)
123+ projectIDs [m .ResourceID ] = orgProjects
124+ }
125+ }
107126 }
108127
109- mapping , err := uc .FindCASMappingForDownloadByOrg (ctx , digest , userOrgs )
128+ mapping , err := uc .FindCASMappingForDownloadByOrg (ctx , digest , userOrgs , projectIDs )
110129 if err != nil {
111130 return nil , fmt .Errorf ("failed to find cas mapping for download: %w" , err )
112131 }
113132
114133 return mapping , nil
115134}
116135
117- func (uc * CASMappingUseCase ) FindCASMappingForDownloadByOrg (ctx context.Context , digest string , orgs []string ) (result * CASMapping , err error ) {
136+ // FindCASMappingForDownloadByOrg looks for the CAS mapping to download the referenced artifact in one of the passed organizations.
137+ // The result will get filtered out if RBAC is enabled (projectIDs is not Nil)
138+ func (uc * CASMappingUseCase ) FindCASMappingForDownloadByOrg (ctx context.Context , digest string , orgs []uuid.UUID , projectIDs map [uuid.UUID ][]uuid.UUID ) (result * CASMapping , err error ) {
118139 if _ , err := cr_v1 .NewHash (digest ); err != nil {
119140 return nil , NewErrValidation (fmt .Errorf ("invalid digest format: %w" , err ))
120141 }
@@ -143,8 +164,8 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context,
143164 return nil , NewErrNotFound ("digest not found in any mapping" )
144165 }
145166
146- // 2 - CAS mappings associated with the given list of orgs
147- orgMappings , err := filterByOrgs (mappings , orgs )
167+ // 2 - CAS mappings associated with the given list of orgs and project IDs
168+ orgMappings , err := filterByOrgs (mappings , orgs , projectIDs )
148169 if err != nil {
149170 return nil , fmt .Errorf ("failed to load mappings associated to an user: %w" , err )
150171 } else if len (orgMappings ) > 0 {
@@ -163,14 +184,20 @@ func (uc *CASMappingUseCase) FindCASMappingForDownloadByOrg(ctx context.Context,
163184 return defaultOrFirst (publicMappings ), nil
164185}
165186
166- // Extract only the mappings associated with a list of orgs
167- func filterByOrgs (mappings []* CASMapping , orgs []string ) ([]* CASMapping , error ) {
187+ // Extract only the mappings associated with a list of orgs and optionally a list of projects
188+ func filterByOrgs (mappings []* CASMapping , orgs []uuid. UUID , projectIDs map [uuid. UUID ][]uuid. UUID ) ([]* CASMapping , error ) {
168189 result := make ([]* CASMapping , 0 )
169190
170191 for _ , mapping := range mappings {
171192 for _ , o := range orgs {
172- if mapping .OrgID .String () == o {
173- result = append (result , mapping )
193+ if mapping .OrgID == o {
194+ if visibleProjects , ok := projectIDs [mapping .OrgID ]; ok {
195+ if slices .Contains (visibleProjects , mapping .ProjectID ) {
196+ result = append (result , mapping )
197+ }
198+ } else {
199+ result = append (result , mapping )
200+ }
174201 }
175202 }
176203 }
0 commit comments