Skip to content

Commit e7ebb3e

Browse files
fix(authz): close DeleteMembership authorization bypass (#3245)
Signed-off-by: Matías Insaurralde <matias@chainloop.dev>
1 parent 2058563 commit e7ebb3e

2 files changed

Lines changed: 63 additions & 3 deletions

File tree

app/controlplane/pkg/authz/authz.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,10 @@ var (
160160
PolicyOrganizationCreate = &Policy{Organization, ActionCreate}
161161
PolicyOrganizationDelete = &Policy{Organization, ActionDelete}
162162
// User Membership
163-
PolicyOrganizationRead = &Policy{Organization, ActionRead}
164-
PolicyOrganizationListMemberships = &Policy{OrganizationMemberships, ActionList}
163+
PolicyOrganizationRead = &Policy{Organization, ActionRead}
164+
PolicyOrganizationListMemberships = &Policy{OrganizationMemberships, ActionList}
165+
PolicyOrganizationMembershipsDelete = &Policy{OrganizationMemberships, ActionDelete}
166+
PolicyOrganizationMembershipsUpdate = &Policy{OrganizationMemberships, ActionUpdate}
165167

166168
// Group Memberships
167169
PolicyGroupListPendingInvitations = &Policy{ResourceGroup, ActionList}
@@ -203,6 +205,8 @@ var RolesMap = map[Role][]*Policy{
203205
RoleOwner: {
204206
PolicyOrganizationDelete,
205207
PolicyOrganizationManageOwners,
208+
PolicyOrganizationMembershipsDelete,
209+
PolicyOrganizationMembershipsUpdate,
206210
},
207211
// RoleAdmin is an org-scoped role that provides super admin privileges (it's the higher role)
208212
RoleAdmin: {
@@ -213,6 +217,8 @@ var RolesMap = map[Role][]*Policy{
213217
PolicyOrganizationInvitationsCreate,
214218
// Being able to read from the default backend
215219
PolicyDefaultBackendArtifactRead,
220+
PolicyOrganizationMembershipsDelete,
221+
PolicyOrganizationMembershipsUpdate,
216222
// + all the policies from the viewer role inherited automatically
217223
},
218224
// RoleViewer is an org-scoped role that provides read-only access to all resources
@@ -417,7 +423,9 @@ var ServerOperationsMap = map[string]*OperationPolicy{
417423
"/controlplane.v1.OrganizationService/Delete": {},
418424

419425
// List global memberships
420-
"/controlplane.v1.OrganizationService/ListMemberships": {Policies: []*Policy{PolicyOrganizationListMemberships}},
426+
"/controlplane.v1.OrganizationService/ListMemberships": {Policies: []*Policy{PolicyOrganizationListMemberships}},
427+
"/controlplane.v1.OrganizationService/DeleteMembership": {Policies: []*Policy{PolicyOrganizationMembershipsDelete}},
428+
"/controlplane.v1.OrganizationService/UpdateMembership": {Policies: []*Policy{PolicyOrganizationMembershipsUpdate}},
421429

422430
// NOTE: this is about listing my own memberships, not about listing all the memberships in the organization
423431
"/controlplane.v1.UserService/ListMemberships": {},

app/controlplane/pkg/authz/middleware/middleware_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,14 @@ func TestPoliciesLookup(t *testing.T) {
214214
name: "contract apply operation found",
215215
operation: "/controlplane.v1.WorkflowContractService/Apply",
216216
},
217+
{
218+
name: "organization delete membership operation found",
219+
operation: "/controlplane.v1.OrganizationService/DeleteMembership",
220+
},
221+
{
222+
name: "organization update membership operation found",
223+
operation: "/controlplane.v1.OrganizationService/UpdateMembership",
224+
},
217225
}
218226

219227
for _, tc := range testCases {
@@ -234,3 +242,47 @@ func TestPoliciesLookupContractApply(t *testing.T) {
234242
assert.NoError(t, err)
235243
assert.Equal(t, []*authz.Policy{authz.PolicyWorkflowContractCreate, authz.PolicyWorkflowContractUpdate}, policies)
236244
}
245+
246+
func TestPoliciesLookupDeleteMembership(t *testing.T) {
247+
policies, err := policiesLookup("/controlplane.v1.OrganizationService/DeleteMembership")
248+
assert.NoError(t, err)
249+
assert.Equal(t, []*authz.Policy{authz.PolicyOrganizationMembershipsDelete}, policies)
250+
}
251+
252+
func TestPoliciesLookupUpdateMembership(t *testing.T) {
253+
policies, err := policiesLookup("/controlplane.v1.OrganizationService/UpdateMembership")
254+
assert.NoError(t, err)
255+
assert.Equal(t, []*authz.Policy{authz.PolicyOrganizationMembershipsUpdate}, policies)
256+
}
257+
258+
func TestViewerDeniedDeleteMembership(t *testing.T) {
259+
logger := log.NewHelper(log.NewStdLogger(io.Discard))
260+
261+
ctx := context.Background()
262+
ctx = usercontext.WithAuthzSubject(ctx, string(authz.RoleViewer))
263+
ctx = transport.NewServerContext(ctx, &mockTransport{operation: "/controlplane.v1.OrganizationService/DeleteMembership"})
264+
265+
e := NewMockEnforcer(t)
266+
e.On("Enforce", mock.Anything, string(authz.RoleViewer), authz.PolicyOrganizationMembershipsDelete).Return(false, nil)
267+
268+
m := WithAuthzMiddleware(e, logger)
269+
_, err := m(emptyHandler)(ctx, nil)
270+
assert.Error(t, err)
271+
assert.True(t, errors.IsForbidden(err))
272+
}
273+
274+
func TestViewerDeniedUpdateMembership(t *testing.T) {
275+
logger := log.NewHelper(log.NewStdLogger(io.Discard))
276+
277+
ctx := context.Background()
278+
ctx = usercontext.WithAuthzSubject(ctx, string(authz.RoleViewer))
279+
ctx = transport.NewServerContext(ctx, &mockTransport{operation: "/controlplane.v1.OrganizationService/UpdateMembership"})
280+
281+
e := NewMockEnforcer(t)
282+
e.On("Enforce", mock.Anything, string(authz.RoleViewer), authz.PolicyOrganizationMembershipsUpdate).Return(false, nil)
283+
284+
m := WithAuthzMiddleware(e, logger)
285+
_, err := m(emptyHandler)(ctx, nil)
286+
assert.Error(t, err)
287+
assert.True(t, errors.IsForbidden(err))
288+
}

0 commit comments

Comments
 (0)