Skip to content

Commit 0e8d3b7

Browse files
authored
Restrict suspending orgs to admins (#5173) (#5181)
1 parent 01e71b7 commit 0e8d3b7

7 files changed

Lines changed: 208 additions & 8 deletions

File tree

app/access/organization_access.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
module VCAP::CloudController
22
class OrganizationAccess < BaseAccess
3-
def create?(_org, _params=nil)
3+
def create?(_org, params=nil)
44
return true if context.queryer.can_write_globally?
5+
return false if params.present? && params[:status.to_s] == VCAP::CloudController::Organization::SUSPENDED
56

67
FeatureFlag.enabled?(:user_org_creation)
78
end
@@ -12,6 +13,7 @@ def read_for_update?(org, params=nil)
1213
return false unless context.queryer.can_write_to_active_org?(org.id)
1314

1415
return false if params.present? && (params.key?(:quota_definition_guid.to_s) || params.key?(:billing_enabled.to_s))
16+
return false if params.present? && params[:status.to_s] == VCAP::CloudController::Organization::SUSPENDED
1517

1618
true
1719
end

app/controllers/v3/organizations_controller.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def create
5050

5151
message = VCAP::CloudController::OrganizationCreateMessage.new(hashed_params[:body])
5252
unprocessable!(message.errors.full_messages) unless message.valid?
53+
54+
unauthorized! if suspended_by_non_admin?(message)
55+
5356
org = nil
5457

5558
Organization.db.transaction do
@@ -78,6 +81,8 @@ def update
7881
message = VCAP::CloudController::OrganizationUpdateMessage.new(hashed_params[:body])
7982
unprocessable!(message.errors.full_messages) unless message.valid?
8083

84+
unauthorized! if suspended_by_non_admin?(message)
85+
8186
org = OrganizationUpdate.new(user_audit_info).update(org, message)
8287

8388
render json: Presenters::V3::OrganizationPresenter.new(org), status: :ok
@@ -222,6 +227,10 @@ def isolation_segment_not_found!
222227
resource_not_found!(:isolation_segment)
223228
end
224229

230+
def suspended_by_non_admin?(message)
231+
message.requested?(:suspended) && message.suspended && !permission_queryer.can_write_globally?
232+
end
233+
225234
def fetch_org(guid)
226235
Organization.where(guid:).first
227236
end

docs/v3/source/includes/resources/organizations/_create.md.erb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ Name | Type | Description
4242
**metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the organization
4343

4444
#### Permitted roles
45-
|
46-
--- | ---
47-
Admin |
4845

49-
If the `user_org_creation` feature flag is enabled, any user with the `cloud_controller.write` scope will be able to create organizations.
46+
Role | Notes
47+
---- | -----
48+
Admin |
49+
All Roles | If the `user_org_creation` feature flag is enabled, any user with the `cloud_controller.write` scope can create organizations, but cannot set the `suspended` field to `true`

docs/v3/source/includes/resources/organizations/_update.md.erb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Name | Type | Description
3737
**metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the organization
3838

3939
#### Permitted roles
40-
|
41-
--- | ---
40+
41+
Role | Notes
42+
---- | -----
4243
Admin |
43-
Org Manager |
44+
Org Manager | Cannot change the `suspended` field

spec/request/organizations_spec.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,39 @@ module VCAP::CloudController
169169
expect(last_response.status).to eq(201)
170170
end
171171
end
172+
173+
context 'when a non-admin sets the suspended field' do
174+
let(:org) { Organization.make }
175+
let(:space) { Space.make(organization: org) }
176+
177+
before do
178+
VCAP::CloudController::FeatureFlag.make(name: 'user_org_creation', enabled: true)
179+
end
180+
181+
context 'with suspended: true' do
182+
let(:api_call) do
183+
->(user_headers) { post '/v3/organizations', { name: "org-#{SecureRandom.uuid}", suspended: true }.to_json, user_headers }
184+
end
185+
let(:expected_codes_and_responses) do
186+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
187+
h['admin'] = { code: 201 }
188+
h
189+
end
190+
191+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
192+
end
193+
194+
context 'with suspended: false (no-op)' do
195+
let(:api_call) do
196+
->(user_headers) { post '/v3/organizations', { name: "org-#{SecureRandom.uuid}", suspended: false }.to_json, user_headers }
197+
end
198+
let(:expected_codes_and_responses) do
199+
Hash.new({ code: 201 }.freeze)
200+
end
201+
202+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
203+
end
204+
end
172205
end
173206

174207
describe 'GET /v3/organizations' do
@@ -1307,6 +1340,59 @@ module VCAP::CloudController
13071340

13081341
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
13091342
end
1343+
1344+
context 'when a non-admin mutates the suspended field' do
1345+
let(:org) { Organization.make }
1346+
let(:space) { Space.make(organization: org) }
1347+
1348+
context 'on an active org with suspended: true' do
1349+
let(:api_call) do
1350+
->(user_headers) { patch "/v3/organizations/#{org.guid}", { suspended: true }.to_json, user_headers.merge('CONTENT_TYPE' => 'application/json') }
1351+
end
1352+
let(:expected_codes_and_responses) do
1353+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
1354+
h['admin'] = { code: 200 }
1355+
h['no_role'] = { code: 404 }
1356+
h
1357+
end
1358+
1359+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
1360+
end
1361+
1362+
context 'on a suspended org with suspended: false (un-suspend)' do
1363+
let(:api_call) do
1364+
->(user_headers) { patch "/v3/organizations/#{org.guid}", { suspended: false }.to_json, user_headers.merge('CONTENT_TYPE' => 'application/json') }
1365+
end
1366+
let(:expected_codes_and_responses) do
1367+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
1368+
h['admin'] = { code: 200 }
1369+
h['org_manager'] = { code: 403, errors: CF_ORG_SUSPENDED }
1370+
h['no_role'] = { code: 404 }
1371+
h
1372+
end
1373+
1374+
before do
1375+
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
1376+
end
1377+
1378+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
1379+
end
1380+
1381+
context 'on an active org with suspended: false (no-op)' do
1382+
let(:api_call) do
1383+
->(user_headers) { patch "/v3/organizations/#{org.guid}", { suspended: false }.to_json, user_headers.merge('CONTENT_TYPE' => 'application/json') }
1384+
end
1385+
let(:expected_codes_and_responses) do
1386+
h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze)
1387+
h['admin'] = { code: 200 }
1388+
h['org_manager'] = { code: 200 }
1389+
h['no_role'] = { code: 404 }
1390+
h
1391+
end
1392+
1393+
it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS
1394+
end
1395+
end
13101396
end
13111397

13121398
describe 'DELETE /v3/organizations/:guid' do

spec/request/v2/organizations_spec.rb

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,83 @@
122122
expect(decoded_response['description']).to eq('Current usage exceeds new quota values. This org currently contains apps running with an unlimited log rate limit.')
123123
end
124124
end
125+
126+
context 'when a OrgManager mutates the status field' do
127+
let(:org_manager) { VCAP::CloudController::User.make }
128+
let(:org_manager_header) { headers_for(org_manager) }
129+
let(:admin_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }
130+
131+
before do
132+
org.add_user(org_manager)
133+
org.add_manager(org_manager)
134+
end
135+
136+
it 'returns 403 NotAuthorized when attempting to suspend an active org' do
137+
put "/v2/organizations/#{org.guid}", { status: 'suspended' }.to_json, org_manager_header
138+
139+
expect(last_response).to have_status_code(403)
140+
expect(decoded_response['error_code']).to eq('CF-NotAuthorized')
141+
expect(org.reload).not_to be_suspended
142+
end
143+
144+
it 'returns 403 NotAuthorized when attempting to un-suspend a suspended org' do
145+
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
146+
147+
put "/v2/organizations/#{org.guid}", { status: 'active' }.to_json, org_manager_header
148+
149+
expect(last_response).to have_status_code(403)
150+
expect(decoded_response['error_code']).to eq('CF-NotAuthorized')
151+
expect(org.reload).to be_suspended
152+
end
153+
154+
it 'still allows an admin to mutate status' do
155+
put "/v2/organizations/#{org.guid}", { status: 'suspended' }.to_json, admin_header
156+
157+
expect(last_response).to have_status_code(201)
158+
expect(org.reload).to be_suspended
159+
end
160+
161+
it 'allows a no-op status update (status matches current state) by an OrgManager' do
162+
put "/v2/organizations/#{org.guid}", { status: 'active' }.to_json, org_manager_header
163+
164+
expect(last_response).to have_status_code(201)
165+
expect(org.reload).not_to be_suspended
166+
end
167+
end
168+
end
169+
170+
describe 'POST /v2/organizations' do
171+
context 'when a non-admin sets status to suspended at create' do
172+
let(:user_header) { headers_for(user) }
173+
let(:admin_header) { headers_for(user, scopes: %w[cloud_controller.admin]) }
174+
175+
before do
176+
VCAP::CloudController::FeatureFlag.create(name: 'user_org_creation', enabled: true)
177+
end
178+
179+
it 'returns 403 NotAuthorized when a non-admin attempts to create a suspended org' do
180+
post '/v2/organizations', { name: 'suspended-at-birth', status: 'suspended' }.to_json, user_header
181+
182+
expect(last_response).to have_status_code(403)
183+
expect(decoded_response['error_code']).to eq('CF-NotAuthorized')
184+
expect(VCAP::CloudController::Organization.find(name: 'suspended-at-birth')).to be_nil
185+
end
186+
187+
it 'allows a non-admin to create an org with status: active (the default) as a no-op' do
188+
post '/v2/organizations', { name: 'noop-active-org', status: 'active' }.to_json, user_header
189+
190+
expect(last_response).to have_status_code(201)
191+
created = VCAP::CloudController::Organization.find(name: 'noop-active-org')
192+
expect(created).not_to be_nil
193+
expect(created).not_to be_suspended
194+
end
195+
196+
it 'still allows an admin to create a suspended org' do
197+
post '/v2/organizations', { name: 'admin-suspended-org', status: 'suspended' }.to_json, admin_header
198+
199+
expect(last_response).to have_status_code(201)
200+
expect(VCAP::CloudController::Organization.find(name: 'admin-suspended-org')).to be_suspended
201+
end
202+
end
125203
end
126204
end

spec/unit/access/organization_access_spec.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,18 @@ module VCAP::CloudController
114114
end
115115

116116
it_behaves_like('an access control', :create, flag_enabled_create_table)
117+
118+
context 'status param is set to suspended' do
119+
let(:op_params) { { 'status' => VCAP::CloudController::Organization::SUSPENDED } }
120+
121+
it_behaves_like('an access control', :create, write_table)
122+
end
123+
124+
context 'status param is set to active (no-op)' do
125+
let(:op_params) { { 'status' => VCAP::CloudController::Organization::ACTIVE } }
126+
127+
it_behaves_like('an access control', :create, flag_enabled_create_table)
128+
end
117129
end
118130

119131
context 'when the flag is disabled' do
@@ -141,6 +153,18 @@ module VCAP::CloudController
141153

142154
it_behaves_like('an access control', :read_for_update, write_table)
143155
end
156+
157+
context 'status param is set to suspended' do
158+
let(:op_params) { { 'status' => VCAP::CloudController::Organization::SUSPENDED } }
159+
160+
it_behaves_like('an access control', :read_for_update, write_table)
161+
end
162+
163+
context 'status param is set to active (no-op on an active org)' do
164+
let(:op_params) { { 'status' => VCAP::CloudController::Organization::ACTIVE } }
165+
166+
it_behaves_like('an access control', :read_for_update, update_table)
167+
end
144168
end
145169
end
146170

0 commit comments

Comments
 (0)