Issue
PATCH /v3/organizations/:guid with body {"suspended": true} succeeds when the caller is an OrgManager (not a Cloud Controller admin). After the org transitions to suspended: true, the same OrgManager is then blocked from issuing the inverse PATCH {"suspended": false} because the suspension gate (CF-OrgSuspended, code 10017) fires on every write to a suspended org, including the un-suspend itself.
The result is a one-way trap: any OrgManager can suspend their own organization, after which only a Cloud Controller admin can recover it. This appears to invert the intended trust model of the suspended flag (a platform/operator control) and almost certainly creates an unintended self-DoS path for tenants.
Context
Observed against cloud_controller_ng API version 3.221.0 on the v3 endpoints. The v2 path (PUT /v2/organizations/:guid with {"status": "suspended"}) was not retested for this specific authority bug, so this report is scoped to v3.
Steps to Reproduce
Pre-conditions: a CC admin and a regular UAA user t-orgmgr who holds the OrgManager role on the org under test (call its GUID $ORG). The org starts active (suspended: false).
- As CC admin, confirm initial state:
cf curl /v3/organizations/$ORG | jq '{suspended}'
# {"suspended": false}
- Authenticate as t-orgmgr (OrgManager on the org, no admin scope):
cf auth t-orgmgr <password>
- As t-orgmgr, suspend the org via the v3 PATCH:
cf curl -X PATCH /v3/organizations/$ORG -d '{"suspended": true}'
→ Returns 200 OK with "suspended": true in the body. No error, no authorization rejection.
- Confirm the state took effect:
cf curl /v3/organizations/$ORG | jq '{suspended}'
# {"suspended": true}
- Still as t-orgmgr, attempt to un-suspend:
cf curl -X PATCH /v3/organizations/$ORG -d '{"suspended": false}'
→ Returns 403 with:
{
"errors": [
{"detail": "The organization is suspended", "title": "CF-OrgSuspended", "code": 10017}
]
}
- Recovery now requires a Cloud Controller admin to issue the un-suspend PATCH.
For comparison, the same OrgManager identity on an active sibling org receives 403 CF-NotAuthorized (10003) only when the org is already suspended and they try to perform any other write, which is the documented behavior. The bug is specifically that the transition into suspended is not gated for non-admin OrgManagers.
Expected Result
Restrict mutation of suspended to Cloud Controller admins. OrgManagers should receive 403 CF-NotAuthorized when attempting PATCH /v3/organizations/:guid with a suspended field, regardless of the requested value. This matches the conventional model where suspended is an operator/platform control, and is consistent with how v2's PUT /v2/organizations/:guid {"status": ...} is generally treated as a privileged operation.
Current Result
OrgManagers can transition their own organization into suspended: true via PATCH /v3/organizations/:guid and receive 200 OK. Once suspended, the same OrgManager (and every other non-admin role) is blocked by CF-OrgSuspended (10017) from any further write including the inverse PATCH that would clear the flag. Only a Cloud Controller admin can recover the org.
The suspension is also recorded as an audit.organization.update event with actor.type: "user" and actor.name: "<orgmgr-username>" and data.request: {"suspended": true}, so the trail is preserved but the audit event does not unlock recovery.
Reproducing exchange:
# As OrgManager t-orgmgr — org is active
$ cf curl -X PATCH /v3/organizations/$ORG -d '{"suspended": true}'
{ "guid": "...", "name": "...", "suspended": true, ... } # 200
# Now suspended. Same OrgManager tries to recover:
$ cf curl -X PATCH /v3/organizations/$ORG -d '{"suspended": false}'
{
"errors": [
{"detail": "The organization is suspended", "title": "CF-OrgSuspended", "code": 10017}
]
} # 403
Possible Fix
admin-only suspended mutation. Add a field-level authorization check on PATCH /v3/organizations/:guid that rejects requests touching the suspended key unless the caller has the cloud_controller.admin (or equivalent) scope. Returns 403 CF-NotAuthorized. This restores the operator-control model and avoids the trap entirely. Apply the same restriction to v2's PUT /v2/organizations/:guid {"status": ...} for consistency.
Issue
PATCH /v3/organizations/:guidwith body{"suspended": true}succeeds when the caller is an OrgManager (not a Cloud Controller admin). After the org transitions tosuspended: true, the same OrgManager is then blocked from issuing the inverse PATCH{"suspended": false}because the suspension gate (CF-OrgSuspended, code 10017) fires on every write to a suspended org, including the un-suspend itself.The result is a one-way trap: any OrgManager can suspend their own organization, after which only a Cloud Controller admin can recover it. This appears to invert the intended trust model of the
suspendedflag (a platform/operator control) and almost certainly creates an unintended self-DoS path for tenants.Context
Observed against
cloud_controller_ngAPI version3.221.0on the v3 endpoints. The v2 path (PUT /v2/organizations/:guidwith{"status": "suspended"}) was not retested for this specific authority bug, so this report is scoped to v3.Steps to Reproduce
Pre-conditions: a CC admin and a regular UAA user
t-orgmgrwho holds theOrgManagerrole on the org under test (call its GUID$ORG). The org starts active (suspended: false)."suspended": truein the body. No error, no authorization rejection.{ "errors": [ {"detail": "The organization is suspended", "title": "CF-OrgSuspended", "code": 10017} ] }For comparison, the same OrgManager identity on an active sibling org receives 403
CF-NotAuthorized(10003) only when the org is already suspended and they try to perform any other write, which is the documented behavior. The bug is specifically that the transition intosuspendedis not gated for non-admin OrgManagers.Expected Result
Restrict mutation of
suspendedto Cloud Controller admins. OrgManagers should receive403 CF-NotAuthorizedwhen attemptingPATCH /v3/organizations/:guidwith asuspendedfield, regardless of the requested value. This matches the conventional model wheresuspendedis an operator/platform control, and is consistent with how v2'sPUT /v2/organizations/:guid {"status": ...}is generally treated as a privileged operation.Current Result
OrgManagers can transition their own organization into
suspended: trueviaPATCH /v3/organizations/:guidand receive 200 OK. Once suspended, the same OrgManager (and every other non-admin role) is blocked byCF-OrgSuspended(10017) from any further write including the inverse PATCH that would clear the flag. Only a Cloud Controller admin can recover the org.The suspension is also recorded as an
audit.organization.updateevent withactor.type: "user"andactor.name: "<orgmgr-username>"anddata.request: {"suspended": true}, so the trail is preserved but the audit event does not unlock recovery.Reproducing exchange:
Possible Fix
admin-only
suspendedmutation. Add a field-level authorization check onPATCH /v3/organizations/:guidthat rejects requests touching thesuspendedkey unless the caller has thecloud_controller.admin(or equivalent) scope. Returns403 CF-NotAuthorized. This restores the operator-control model and avoids the trap entirely. Apply the same restriction to v2'sPUT /v2/organizations/:guid {"status": ...}for consistency.