Enforce owner-only member removal and ownership grants via policy

An organization ADMIN could hard-remove members, including OWNERs, because
removeUser (connect and MCP) only checked the weaker iam:membership-profile:delete
gate. Authorize the owner-only iam:membership:delete instead, and expose the
source attribute on MembershipProfile so the owner grant's non-SCIM condition
can match.

Consolidate ownership-grant authorization into policy for both createUser and
updateMembership: each resolver passes the requested role as a target_role
attribute and ADMIN is denied granting ownership via deny-create-owner /
deny-promote-owner. target_role is distinct from resource.role, which is the
target's current role and guards editing existing owners. With no callers left,
the iam:membership-role:set-owner action (grant and OAuth2 scope) is removed.

Also pass the authorized scope through to the RemoveUser/CreateUser services,
gate the console Remove action on iam:membership:delete, and add regression
tests plus a changelog entry.

Signed-off-by: Sacha Al Himdani <sacha@probo.com>
This commit is contained in:
Sacha Al Himdani
2026-07-06 16:21:51 +02:00
parent ddfabc5940
commit ff9cb881e8
13 changed files with 133 additions and 62 deletions

View File

@@ -35,6 +35,7 @@ import (
"go.probo.inc/probo/pkg/resourcealias"
"go.probo.inc/probo/pkg/riskmanagement"
"go.probo.inc/probo/pkg/server/api/authn"
"go.probo.inc/probo/pkg/server/api/authz"
"go.probo.inc/probo/pkg/thirdparty"
)
@@ -65,17 +66,21 @@ func markdownToProseMirrorJSON(markdown string) (string, error) {
return string(out), nil
}
func (r *Resolver) Authorize(ctx context.Context, entityID gid.GID, action iam.Action) (*coredata.Scope, error) {
func (r *Resolver) Authorize(ctx context.Context, entityID gid.GID, action iam.Action, opts ...authz.AuthorizeFuncOption) (*coredata.Scope, error) {
identity := authn.IdentityFromContext(ctx)
scope, err := r.iamSvc.Authorizer.Authorize(
ctx,
iam.AuthorizeParams{
Principal: identity.ID,
Resource: entityID,
Action: action,
},
)
params := iam.AuthorizeParams{
Principal: identity.ID,
Resource: entityID,
Action: action,
ResourceAttributes: make(map[string]string),
}
for _, opt := range opts {
opt(&params)
}
scope, err := r.iamSvc.Authorizer.Authorize(ctx, params)
if err == nil {
return scope, nil
}