fix(controlplane): restrict owner-role invitations to org owners (CP-1)#3263
Conversation
|
Re: the behavior-change note on instance-admin / owner distinction — in practice Admin and Owner currently behave the same in Chainloop, so this distinction has historically been moot. However, we have plans to enable explicit Owner-only features downstream (e.g. billing, org deletion, SSO management), so we're tightening the boundary now so the 🤖 Posted by Maximus bot (Claude Code) on behalf of @migmartri |
9988cd0 to
c08f461
Compare
AI Session Analysis
|
| Status | Attribution | File | Lines |
|---|---|---|---|
| modified | ai | app/controlplane/pkg/biz/orginvitation_integration_test.go |
+195 / -214 |
| modified | ai | app/controlplane/pkg/biz/membership.go |
+15 / -6 |
| modified | ai | app/controlplane/pkg/biz/orginvitation.go |
+1 / -17 |
| modified | ai | app/controlplane/pkg/biz/group.go |
+7 / -1 |
Policies (4, 1 failing)
| Status | Policy | Material | Messages |
|---|---|---|---|
| ✅ Passed | ai-config-ai-agents-allowed |
ai-coding-session-321ef4 |
- |
| ✅ Passed | ai-config-no-dangerous-commands |
ai-coding-session-321ef4 |
- |
ai-config-no-secrets |
ai-coding-session-321ef4 |
|
|
| ✅ Passed | ai-config-mcp-servers-allowed |
ai-coding-session-321ef4 |
- |
Powered by Chainloop and Chainloop Trace
f5650b9 to
e034f25
Compare
OrgInvitationUseCase.Create accepted any caller-supplied role verbatim, so an
Admin (who passes the Casbin middleware via IsAdmin() short-circuit) could
invite a new external email as MEMBERSHIP_ROLE_ORG_OWNER. AcceptPendingInvitations
then auto-creates an Owner membership, bypassing PolicyOrganizationManageOwners
(the Owner-only manage-owners gate) and bootstrapping a fresh Owner via the
invitation path.
Add an enforceManageOwners check in the biz layer: when the requested role is
RoleOwner, enforce PolicyOrganizationManageOwners against the caller's role.
This mirrors the existing guard in MembershipUseCase.UpdateRole/DeleteOther.
The service layer passes the caller's authz subject down via a new callerRole
parameter; the two existing biz callers (group/project invitation flows) pass
the requester's membership role. Non-owner callers now receive
ErrUnauthorized("only organization owners can invite owners").
Assisted-by: Claude Code
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
- share a single enforceManageOwners helper between MembershipUseCase and OrgInvitationUseCase, with the denial messages defined once - guard against a nil requester membership in GroupUseCase.handleNonExistingUser - rework TestCreateOwnerGuard as a table-driven test, drop unused setup and pin the intentional instance-admin denial - replace s.T().Run subtests with suite-style s.Run in the invitation tests Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev> Chainloop-Trace-Sessions: 321ef432-c960-4e92-bc01-9e70c8dba97d
e034f25 to
b116b18
Compare
Assisted-by: Claude Code Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
b116b18 to
c56d555
Compare
Fixes #3264
What
OrgInvitationUseCase.Createaccepted any caller-supplied role verbatim. The Casbin authz middleware gatesOrgInvitationService/CreatewithPolicyOrganizationInvitationsCreate(admin/owner only), butRoleAdmin.IsAdmin()short-circuits the middleware — so an Admin could invite a new external email asMEMBERSHIP_ROLE_ORG_OWNER.AcceptPendingInvitationsthen auto-creates an Owner membership, bypassingPolicyOrganizationManageOwners(the Owner-only manage-owners gate) and bootstrapping a fresh Owner via the invitation path.Change
RoleOwner, the biz layer now enforcesPolicyOrganizationManageOwnersagainst the caller's authz subject. The check is shared with the existing owner-scoped guards inMembershipUseCase.UpdateRole/DeleteOtherthrough a singleenforceManageOwnershelper, with the denial messages defined once. Non-owner callers receiveErrUnauthorized("only organization owners can invite owners").api-token:<id>for API tokens) down via a newcallerRoleparameter; the group/project invitation flows pass the requester's membership role.PolicyOrganizationInvitationsCreatebut deliberately notPolicyOrganizationManageOwners, so they can no longer seed owners into organizations they administer.GroupUseCase.handleNonExistingUsernow guards against a nil requester membership instead of dereferencing it.Parts of this change were produced with AI assistance (Claude Code); see the commit trailers.
🤖 Posted by Maximus bot (Claude Code) on behalf of @migmartri