@@ -158,89 +158,131 @@ func (s *casMappingIntegrationSuite) TestCASMappingForDownloadByOrg() {
158158 })
159159}
160160
161- func (s * casMappingIntegrationSuite ) TestFindByDigest () {
162- // 1. Digest: validDigest, CASBackend: casBackend1, WorkflowRunID: workflowRun
163- // 2. Digest: validDigest2, CASBackend: casBackend1, WorkflowRunID: workflowRun
164- // 3. Digest: validDigest, CASBackend: casBackend2, WorkflowRunID: workflowRun
165- // 4. Digest: validDigest, CASBackend: casBackend3, WorkflowRunID: publicWorkflowRun
166- _ , err := s .CASMapping .Create (context .TODO (), validDigest , s .casBackend1 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
167- require .NoError (s .T (), err )
168- _ , err = s .CASMapping .Create (context .TODO (), validDigest2 , s .casBackend1 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
169- require .NoError (s .T (), err )
170- _ , err = s .CASMapping .Create (context .TODO (), validDigest , s .casBackend2 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
161+ // When a digest is reachable through several CAS backends, the download lookup must return the
162+ // mapping stored in the default backend, regardless of the order the mappings were created in.
163+ // This locks in the defaultOrFirst behaviour for both the org-scoped and the public fallback paths.
164+ func (s * casMappingIntegrationSuite ) TestCASMappingForDownloadPrefersDefaultBackend () {
165+ ctx := context .Background ()
166+
167+ // org1 already has casBackend1 as its default backend. Add a second, non-default backend.
168+ nonDefaultBackend , err := s .CASBackend .Create (ctx , s .org1 .ID , randomName (), "my-location" , "non-default backend" , backendType , nil , false , false , nil )
171169 require .NoError (s .T (), err )
172- _ , err = s .CASMapping .Create (context .TODO (), validDigest , s .casBackend3 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .publicWorkflowRun .ID })
170+ s .Require ().False (nonDefaultBackend .Default )
171+
172+ s .Run ("org download returns the default backend even when it is mapped last" , func () {
173+ // Map the digest to the non-default backend FIRST, then to the default one.
174+ _ , err := s .CASMapping .Create (ctx , validDigest , nonDefaultBackend .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
175+ require .NoError (s .T (), err )
176+ _ , err = s .CASMapping .Create (ctx , validDigest , s .casBackend1 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
177+ require .NoError (s .T (), err )
178+
179+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest , []uuid.UUID {uuid .MustParse (s .org1 .ID )}, nil )
180+ s .NoError (err )
181+ s .Require ().NotNil (mapping )
182+ s .Equal (s .casBackend1 .ID , mapping .CASBackend .ID )
183+ })
184+
185+ s .Run ("org download returns the non-default backend when no default mapping exists" , func () {
186+ _ , err := s .CASMapping .Create (ctx , validDigest2 , nonDefaultBackend .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
187+ require .NoError (s .T (), err )
188+
189+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest2 , []uuid.UUID {uuid .MustParse (s .org1 .ID )}, nil )
190+ s .NoError (err )
191+ s .Require ().NotNil (mapping )
192+ s .Equal (nonDefaultBackend .ID , mapping .CASBackend .ID )
193+ })
194+
195+ s .Run ("public download returns the default backend even when it is mapped last" , func () {
196+ // Public mappings (workflow is public) across two backends, non-default created first.
197+ _ , err := s .CASMapping .Create (ctx , validDigestPublic , nonDefaultBackend .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .publicWorkflowRun .ID })
198+ require .NoError (s .T (), err )
199+ _ , err = s .CASMapping .Create (ctx , validDigestPublic , s .casBackend1 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .publicWorkflowRun .ID })
200+ require .NoError (s .T (), err )
201+
202+ // A requester with no access to org1 falls back to the public mappings.
203+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigestPublic , []uuid.UUID {uuid .New ()}, nil )
204+ s .NoError (err )
205+ s .Require ().NotNil (mapping )
206+ s .Equal (s .casBackend1 .ID , mapping .CASBackend .ID )
207+ })
208+ }
209+
210+ // When RBAC is enabled for an org (projectIDs carries an entry for it), only mappings whose project
211+ // is in the visible set are reachable through that org.
212+ func (s * casMappingIntegrationSuite ) TestCASMappingForDownloadRBAC () {
213+ ctx := context .Background ()
214+ orgUUID := uuid .MustParse (s .org1 .ID )
215+
216+ // A mapping in org1 scoped to a specific project.
217+ _ , err := s .CASMapping .Create (ctx , validDigest , s .casBackend1 .ID .String (), & biz.CASMappingCreateOpts {
218+ WorkflowRunID : & s .workflowRun .ID ,
219+ ProjectID : & s .projectID ,
220+ })
173221 require .NoError (s .T (), err )
174222
175- testcases := []struct {
176- name string
177- digest string
178- want []* biz.CASMapping
179- wantErr bool
180- }{
181- {
182- name : "validDigest" ,
183- digest : validDigest ,
184- want : []* biz.CASMapping {
185- {
186- Digest : validDigest ,
187- CASBackend : & biz.CASBackend {ID : s .casBackend1 .ID },
188- WorkflowRunID : s .workflowRun .ID ,
189- OrgID : s .casBackend1 .OrganizationID ,
190- Public : false ,
191- },
192- {
193- Digest : validDigest ,
194- CASBackend : & biz.CASBackend {ID : s .casBackend2 .ID },
195- WorkflowRunID : s .workflowRun .ID ,
196- OrgID : s .casBackend2 .OrganizationID ,
197- Public : false ,
198- },
199- {
200- Digest : validDigest ,
201- CASBackend : & biz.CASBackend {ID : s .casBackend3 .ID },
202- WorkflowRunID : s .publicWorkflowRun .ID ,
203- OrgID : s .casBackend3 .OrganizationID ,
204- Public : true ,
205- },
206- },
207- },
208- {
209- name : "validDigest2" ,
210- digest : validDigest2 ,
211- want : []* biz.CASMapping {
212- {
213- Digest : validDigest2 ,
214- CASBackend : & biz.CASBackend {ID : s .casBackend1 .ID },
215- WorkflowRunID : s .workflowRun .ID ,
216- OrgID : s .casBackend1 .OrganizationID ,
217- Public : false ,
218- },
219- },
220- },
221- {
222- name : "invalidDigest" ,
223- digest : invalidDigest ,
224- want : []* biz.CASMapping {},
225- },
226- }
223+ s .Run ("returned when the project is visible" , func () {
224+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest , []uuid.UUID {orgUUID },
225+ map [uuid.UUID ][]uuid.UUID {orgUUID : {s .projectID }})
226+ s .NoError (err )
227+ s .Require ().NotNil (mapping )
228+ s .Equal (s .casBackend1 .ID , mapping .CASBackend .ID )
229+ })
227230
228- for _ , tc := range testcases {
229- s .Run (tc .name , func () {
230- got , err := s .CASMapping .FindByDigest (context .Background (), tc .digest )
231- if tc .wantErr {
232- s .Error (err )
233- } else {
234- s .NoError (err )
235- if diff := cmp .Diff (tc .want , got ,
236- cmpopts .IgnoreFields (biz.CASMapping {}, "CreatedAt" , "ID" ),
237- cmpopts .IgnoreTypes (biz.CASBackend {}),
238- ); diff != "" {
239- assert .Failf (s .T (), "mismatch (-want +got):\n %s" , diff )
240- }
241- }
242- })
243- }
231+ s .Run ("not returned when the project is not visible" , func () {
232+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest , []uuid.UUID {orgUUID },
233+ map [uuid.UUID ][]uuid.UUID {orgUUID : {uuid .New ()}})
234+ s .Error (err )
235+ s .Nil (mapping )
236+ })
237+
238+ s .Run ("not returned when RBAC is enabled with no visible projects" , func () {
239+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest , []uuid.UUID {orgUUID },
240+ map [uuid.UUID ][]uuid.UUID {orgUUID : {}})
241+ s .Error (err )
242+ s .Nil (mapping )
243+ })
244+ }
245+
246+ // Mappings pointing to a soft-deleted backend, or produced by a soft-deleted workflow, must not be
247+ // served for download.
248+ func (s * casMappingIntegrationSuite ) TestCASMappingForDownloadSkipsSoftDeleted () {
249+ ctx := context .Background ()
250+
251+ s .Run ("org download skips a mapping whose backend is soft-deleted" , func () {
252+ backend , err := s .CASBackend .Create (ctx , s .org1 .ID , randomName (), "my-location" , "to be deleted" , backendType , nil , false , false , nil )
253+ require .NoError (s .T (), err )
254+ _ , err = s .CASMapping .Create (ctx , validDigest3 , backend .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .workflowRun .ID })
255+ require .NoError (s .T (), err )
256+
257+ // Reachable before the backend is deleted.
258+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest3 , []uuid.UUID {uuid .MustParse (s .org1 .ID )}, nil )
259+ s .NoError (err )
260+ s .Require ().NotNil (mapping )
261+
262+ require .NoError (s .T (), s .CASBackend .SoftDelete (ctx , s .org1 .ID , backend .ID .String ()))
263+
264+ // The only mapping points to a deleted backend, so it is no longer served.
265+ mapping , err = s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest3 , []uuid.UUID {uuid .MustParse (s .org1 .ID )}, nil )
266+ s .Error (err )
267+ s .Nil (mapping )
268+ })
269+
270+ s .Run ("public download skips a mapping whose workflow is soft-deleted" , func () {
271+ _ , err := s .CASMapping .Create (ctx , validDigest2 , s .casBackend1 .ID .String (), & biz.CASMappingCreateOpts {WorkflowRunID : & s .publicWorkflowRun .ID })
272+ require .NoError (s .T (), err )
273+
274+ // A non-member can reach it through the public fallback while the workflow is public.
275+ mapping , err := s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest2 , []uuid.UUID {uuid .New ()}, nil )
276+ s .NoError (err )
277+ s .Require ().NotNil (mapping )
278+
279+ require .NoError (s .T (), s .Workflow .Delete (ctx , s .org1 .ID , s .publicWorkflow .ID .String ()))
280+
281+ // Once the workflow is soft-deleted the mapping is no longer public.
282+ mapping , err = s .CASMapping .FindCASMappingForDownloadByOrg (ctx , validDigest2 , []uuid.UUID {uuid .New ()}, nil )
283+ s .Error (err )
284+ s .Nil (mapping )
285+ })
244286}
245287
246288func (s * casMappingIntegrationSuite ) TestCreate () {
@@ -342,6 +384,8 @@ type casMappingIntegrationSuite struct {
342384 testhelpers.UseCasesEachTestSuite
343385 casBackend1 , casBackend2 , casBackend3 * biz.CASBackend
344386 workflowRun , publicWorkflowRun * biz.WorkflowRun
387+ publicWorkflow * biz.Workflow
388+ projectID uuid.UUID
345389 userOrg1And2 , userOrg2 * biz.User
346390 org1 , org2 , orgNoUsers * biz.Organization
347391}
@@ -379,8 +423,11 @@ func (s *casMappingIntegrationSuite) SetupTest() {
379423 workflow , err := s .Workflow .Create (ctx , & biz.WorkflowCreateOpts {Name : "test-workflow" , OrgID : s .org1 .ID , Project : "test-project" })
380424 assert .NoError (err )
381425
426+ s .projectID = workflow .ProjectID
427+
382428 publicWorkflow , err := s .Workflow .Create (ctx , & biz.WorkflowCreateOpts {Name : "test-workflow-public" , OrgID : s .org1 .ID , Public : true , Project : "test-project" })
383429 assert .NoError (err )
430+ s .publicWorkflow = publicWorkflow
384431
385432 // Find contract revision
386433 contractVersion , err := s .WorkflowContract .Describe (ctx , s .org1 .ID , workflow .ContractID .String (), 0 )
0 commit comments