@@ -17,6 +17,7 @@ package biz_test
1717
1818import (
1919 "context"
20+ "fmt"
2021 "testing"
2122
2223 v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
@@ -196,6 +197,93 @@ func (s *OrgIntegrationTestSuite) TestUpdate() {
196197 })
197198}
198199
200+ // TestUpdateRequiresAdminMembership verifies that changing organization-wide
201+ // security settings requires an admin/owner membership in the organization
202+ // being updated. Holding any membership is not enough: the settings gated here
203+ // (policy enforcement, allowed policy hostnames, runner env-var capture) are
204+ // security controls for the whole org.
205+ func (s * OrgIntegrationTestSuite ) TestUpdateRequiresAdminMembership () {
206+ ctx := context .Background ()
207+
208+ testCases := []struct {
209+ name string
210+ role authz.Role
211+ allowed bool
212+ }{
213+ {name : "owner can update" , role : authz .RoleOwner , allowed : true },
214+ {name : "admin can update" , role : authz .RoleAdmin , allowed : true },
215+ {name : "viewer cannot update" , role : authz .RoleViewer },
216+ {name : "member cannot update" , role : authz .RoleOrgMember },
217+ {name : "contributor cannot update" , role : authz .RoleOrgContributor },
218+ }
219+
220+ for _ , tc := range testCases {
221+ s .Run (tc .name , func () {
222+ org , err := s .Organization .CreateWithRandomName (ctx )
223+ require .NoError (s .T (), err )
224+
225+ user , err := s .User .UpsertByEmail (ctx , fmt .Sprintf ("%s@test.com" , uuid .NewString ()), nil )
226+ require .NoError (s .T (), err )
227+
228+ _ , err = s .Membership .Create (ctx , org .ID , user .ID , biz .WithMembershipRole (tc .role ))
229+ require .NoError (s .T (), err )
230+
231+ got , err := s .Organization .Update (ctx , user .ID , org .Name , & biz.OrganizationUpdateOpts {
232+ BlockOnPolicyViolation : toPtrBool (false ),
233+ })
234+
235+ if tc .allowed {
236+ s .NoError (err )
237+ s .False (got .BlockOnPolicyViolation )
238+ return
239+ }
240+
241+ s .Error (err )
242+ s .True (biz .IsErrUnauthorized (err ), "want unauthorized, got %v" , err )
243+ s .Nil (got )
244+ })
245+ }
246+ }
247+
248+ // TestUpdateCrossOrgTampering is a regression test for CP-N1: a user who is an
249+ // owner of their own organization but only a viewer of a victim organization
250+ // must not be able to change the victim's security settings. The authz
251+ // middleware evaluates the caller's role against the organization in the
252+ // request header, so the biz layer has to authorize against the organization
253+ // actually being updated.
254+ func (s * OrgIntegrationTestSuite ) TestUpdateCrossOrgTampering () {
255+ ctx := context .Background ()
256+
257+ victimOrg , err := s .Organization .CreateWithRandomName (ctx )
258+ require .NoError (s .T (), err )
259+ attackerOrg , err := s .Organization .CreateWithRandomName (ctx )
260+ require .NoError (s .T (), err )
261+
262+ attacker , err := s .User .UpsertByEmail (ctx , "attacker@test.com" , nil )
263+ require .NoError (s .T (), err )
264+
265+ // Owner of their own org, which is what gets them past the authz middleware
266+ _ , err = s .Membership .Create (ctx , attackerOrg .ID , attacker .ID , biz .WithMembershipRole (authz .RoleOwner ), biz .WithCurrentMembership ())
267+ require .NoError (s .T (), err )
268+ // ...but only a viewer of the victim org
269+ _ , err = s .Membership .Create (ctx , victimOrg .ID , attacker .ID , biz .WithMembershipRole (authz .RoleViewer ))
270+ require .NoError (s .T (), err )
271+
272+ got , err := s .Organization .Update (ctx , attacker .ID , victimOrg .Name , & biz.OrganizationUpdateOpts {
273+ BlockOnPolicyViolation : toPtrBool (false ),
274+ PoliciesAllowedHostnames : []string {"evil.example.com" },
275+ SkipRunnerEnvVars : toPtrBool (false ),
276+ })
277+ s .Error (err )
278+ s .True (biz .IsErrUnauthorized (err ), "want unauthorized, got %v" , err )
279+ s .Nil (got )
280+
281+ // The victim org keeps its settings
282+ victim , err := s .Organization .FindByName (ctx , victimOrg .Name )
283+ s .NoError (err )
284+ s .Empty (victim .PoliciesAllowedHostnames )
285+ }
286+
199287// We are doing an integration test here because there are some database constraints
200288// and delete cascades that we want to validate that they work too
201289func (s * OrgIntegrationTestSuite ) TestDeleteOrg () {
@@ -288,7 +376,7 @@ func (s *OrgIntegrationTestSuite) SetupTest() {
288376
289377 s .user , err = s .User .UpsertByEmail (ctx , "foo@test.com" , nil )
290378 assert .NoError (err )
291- _ , err = s .Membership .Create (ctx , s .org .ID , s .user .ID , biz .WithCurrentMembership ())
379+ _ , err = s .Membership .Create (ctx , s .org .ID , s .user .ID , biz .WithMembershipRole ( authz . RoleOwner ), biz . WithCurrentMembership ())
292380 assert .NoError (err )
293381
294382 // Integration
0 commit comments