Skip to content

Commit

Permalink
Service instance permissions (#2946)
Browse files Browse the repository at this point in the history
* Add /v3/service_instance/:guid/permissions endpoint
  • Loading branch information
will-gant authored Sep 7, 2022
1 parent 019784c commit 64e4a56
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 19 deletions.
15 changes: 9 additions & 6 deletions app/controllers/v3/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,19 @@ def enforce_write_scope?
!READ_SCOPE_HTTP_METHODS.include?(request.method)
end

def check_read_permissions!
read_scope = SecurityContext.scopes.include?('cloud_controller.read')
admin_read_only_scope = SecurityContext.scopes.include?('cloud_controller.admin_read_only')
global_auditor_scope = SecurityContext.scopes.include?('cloud_controller.global_auditor')
def read_scope
roles.cloud_controller_reader?
end

raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') if !roles.admin? && !read_scope && !admin_read_only_scope && !global_auditor_scope
def write_scope
roles.cloud_controller_writer?
end

def check_read_permissions!
raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') if !roles.admin? && !roles.admin_read_only? && !roles.global_auditor? && !read_scope
end

def check_write_permissions!
write_scope = SecurityContext.scopes.include?('cloud_controller.write')
raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') if !roles.admin? && !write_scope
end

Expand Down
14 changes: 14 additions & 0 deletions app/controllers/v3/service_instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,16 @@ def parameters
end
end

def show_permissions
service_instance = ServiceInstance.first(guid: hashed_params[:guid])
service_instance_not_found! unless service_instance

render status: :ok, json: {
manage: is_space_active?(service_instance.space) ? can_write_to_active_space?(service_instance.space) : admin?,
read: can_read_service_instance?(service_instance),
}
end

private

DECORATORS = [
Expand Down Expand Up @@ -424,4 +434,8 @@ def invalid_service_plan_relation!
def operation_in_progress!
unprocessable!('There is an operation in progress for the service instance.')
end

def read_scope
%w(show_permissions).include?(action_name) && roles.cloud_controller_service_permissions_reader? ? true : super
end
end
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
get '/service_instances/:guid/relationships/shared_spaces/usage_summary', to: 'service_instances_v3#shared_spaces_usage_summary'
get '/service_instances/:guid/credentials', to: 'service_instances_v3#credentials'
get '/service_instances/:guid/parameters', to: 'service_instances_v3#parameters'
get '/service_instances/:guid/permissions', to: 'service_instances_v3#show_permissions'
post '/service_instances', to: 'service_instances_v3#create'
post '/service_instances/:guid/relationships/shared_spaces', to: 'service_instances_v3#share_service_instance'
patch '/service_instances/:guid', to: 'service_instances_v3#update'
Expand Down
7 changes: 7 additions & 0 deletions docs/v3/source/includes/api_resources/_service_instances.erb
Original file line number Diff line number Diff line change
Expand Up @@ -293,3 +293,10 @@
}
}
<% end %>

<% content_for :service_instance_permissions do %>
{
"read": true,
"manage": false
}
<% end %>
1 change: 1 addition & 0 deletions docs/v3/source/includes/concepts/_authorization.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Scope | Description
`cloud_controller.read` | This scope provides read access to resources based on user roles
`cloud_controller.write` | This scope provides write access to resources based on user roles
`cloud_controller.update_build_state` | This scope allows its bearer to update the state of a build; currently only used when [updating builds](#update-a-build)
`cloud_controller_service_permissions.read` | This scope provides read only access for [service instance permissions](#get-permissions-for-a-service-instance)

#### Cloud Foundry user roles

Expand Down
2 changes: 1 addition & 1 deletion docs/v3/source/includes/resources/apps/_permissions.md.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### Get permissions
### Get permissions for an app

```
Example Request
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
### Get permissions for a service instance

```
Example Request
```

```shell
curl "https://api.example.org/v3/service_instances/[guid]/permissions" \
-X GET \
-H "Authorization: bearer [token]"
```

```
Example Response
```

```http
HTTP/1.1 200 OK
Content-Type: application/json

<%= yield_content :service_instance_permissions %>
```

Get the current user's permissions for the given service instance. If a user can get a service instance then they can 'read' it. Users who can update a service instance can 'manage' it.

This endpoint's primary purpose is to enable third-party service dashboards to determine the permissions of a given Cloud Foundry user that has authenticated with the dashboard via single sign-on (SSO). For more information, see the Cloud Foundry documentation on [Dashboard Single Sign-On](https://docs.cloudfoundry.org/services/dashboard-sso.html).

#### Definition
`GET /v3/service_instances/:guid/permissions`

#### Permitted roles
|
--- | ---
All Roles |
11 changes: 6 additions & 5 deletions docs/v3/source/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,17 @@ includes:
- resources/service_instances/header
- resources/service_instances/object
- resources/service_instances/create
- resources/service_instances/list
- resources/service_instances/get
- resources/service_instances/credentials
- resources/service_instances/parameters
- resources/service_instances/update
- resources/service_instances/list
- resources/service_instances/delete
- resources/service_instances/update
- resources/service_instances/credentials
- resources/service_instances/get_shared_spaces_usage_summary
- resources/service_instances/list_shared_spaces
- resources/service_instances/parameters
- resources/service_instances/permissions
- resources/service_instances/share_to_space
- resources/service_instances/unshare_from_space
- resources/service_instances/get_shared_spaces_usage_summary
- resources/service_credential_bindings/header
- resources/service_credential_bindings/object
- resources/service_credential_bindings/create
Expand Down
15 changes: 15 additions & 0 deletions lib/cloud_controller/roles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class Roles
CLOUD_CONTROLLER_ADMIN_READ_ONLY_SCOPE = 'cloud_controller.admin_read_only'.freeze
CLOUD_CONTROLLER_GLOBAL_AUDITOR = 'cloud_controller.global_auditor'.freeze
CLOUD_CONTROLLER_BUILD_STATE_UPDATER = 'cloud_controller.update_build_state'.freeze
CLOUD_CONTROLLER_READER_SCOPE = 'cloud_controller.read'.freeze
CLOUD_CONTROLLER_WRITER_SCOPE = 'cloud_controller.write'.freeze
CLOUD_CONTROLLER_SERVICE_PERMISSIONS_READER = 'cloud_controller_service_permissions.read'.freeze

ORG_ROLE_NAMES = [:user, :manager, :billing_manager, :auditor].freeze
SPACE_ROLE_NAMES = [:manager, :developer, :auditor].freeze
Expand All @@ -26,6 +29,18 @@ def global_auditor?
@scopes.include?(CLOUD_CONTROLLER_GLOBAL_AUDITOR)
end

def cloud_controller_reader?
@scopes.include?(CLOUD_CONTROLLER_READER_SCOPE)
end

def cloud_controller_writer?
@scopes.include?(CLOUD_CONTROLLER_WRITER_SCOPE)
end

def cloud_controller_service_permissions_reader?
@scopes.include?(CLOUD_CONTROLLER_SERVICE_PERMISSIONS_READER)
end

def admin=(flag)
@scopes.send(flag ? :add : :delete, CLOUD_CONTROLLER_ADMIN_SCOPE)
end
Expand Down
90 changes: 90 additions & 0 deletions spec/request/service_instances_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4095,6 +4095,96 @@ def create_bindings(instance, space:, count:)
end
end

describe 'GET /v3/service_instances/:guid/permissions' do
# For this endpoint we also want to test the 'cloud_controller_service_permissions.read' scope as well as unauthenticated calls.
ADDITIONAL_PERMISSIONS_TO_TEST = %w[service_permissions_reader unauthenticated].freeze

READ_AND_WRITE = { code: 200, response_object: { manage: true, read: true } }.freeze
READ_ONLY = { code: 200, response_object: { manage: false, read: true } }.freeze
NO_PERMISSIONS = { code: 200, response_object: { manage: false, read: false } }.freeze

let(:api_call) { lambda { |user_headers| get "/v3/service_instances/#{guid}/permissions", nil, user_headers } }

context 'when the service instance does not exist' do
let(:guid) { 'no-such-guid' }

let(:expected_codes_and_responses) do
h = Hash.new(code: 404)
h['unauthenticated'] = { code: 401 }
h
end

it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST
end

context 'when the user is a member of the org or space the service instance exists in' do
let(:instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) }
let(:guid) { instance.guid }

let(:expected_codes_and_responses) do
h = Hash.new(code: 404)
%w[admin space_developer].each { |r| h[r] = READ_AND_WRITE }
%w[admin_read_only global_auditor org_manager space_manager space_auditor space_supporter].each { |r| h[r] = READ_ONLY }
%w[org_billing_manager org_auditor no_role service_permissions_reader].each { |r| h[r] = NO_PERMISSIONS }
h['unauthenticated'] = { code: 401 }
h
end

it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST

context 'when organization is suspended' do
let(:expected_codes_and_responses) do
h = super()
h['space_developer'] = READ_ONLY
h
end

before do
org.update(status: VCAP::CloudController::Organization::SUSPENDED)
end

it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST
end

context 'request with only the cloud_controller_service_permissions.read scope' do
it_behaves_like 'permissions for single object endpoint', LOCAL_ROLES do
let(:after_request_check) do
lambda do
# Store the HTTP status and response from the original call.
expected_status_code = last_response.status
expected_json_response = parsed_response

# Repeat the same call for the same user, but now with only the 'cloud_controller_service_permissions.read' scope.
api_call.call(set_user_with_header_as_service_permissions_reader(user: user))

# Both the HTTP status and response should be the same.
expect(last_response).to have_status_code(expected_status_code)
expect(parsed_response).to match_json_response(expected_json_response)
end
end
end
end
end

context 'when the user is not a member of the org or space the service instance exists in' do
let(:instance) { VCAP::CloudController::ManagedServiceInstance.make }
let(:guid) { instance.guid }

let(:expected_codes_and_responses) do
h = Hash.new(code: 404)
h['admin'] = READ_AND_WRITE
%w[admin_read_only global_auditor].each { |r| h[r] = READ_ONLY }
%w[org_billing_manager org_auditor org_manager space_manager space_auditor space_developer space_supporter no_role service_permissions_reader].each do |r|
h[r] = NO_PERMISSIONS
end
h['unauthenticated'] = { code: 401 }
h
end

it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ADDITIONAL_PERMISSIONS_TO_TEST
end
end

def create_managed_json(instance, labels: {}, annotations: {}, last_operation: {}, tags: [])
{
guid: instance.guid,
Expand Down
12 changes: 11 additions & 1 deletion spec/support/user_header_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,20 @@ def set_user_with_header_as_writer(opts = {})
set_user_with_header(user, scopes.merge(opts))
end

# rubocop:disable all
def set_user_with_header_as_service_permissions_reader(opts = {})
# rubocop:enable all
user = opts.delete(:user) || VCAP::CloudController::User.make
scopes = { scopes: %w(cloud_controller_service_permissions.read) }
set_user_with_header(user, scopes.merge(opts))
end

# rubocop:disable all
def set_user_with_header_as_role(role:, org: nil, space: nil, user: nil, scopes: nil, user_name: nil, email: nil)
# rubocop:enable all
current_user = user || VCAP::CloudController::User.make

scope_roles = %w(admin admin_read_only global_auditor reader_and_writer reader writer)
scope_roles = %w(admin admin_read_only global_auditor reader_and_writer reader writer service_permissions_reader)
if org && !scope_roles.include?(role) && role.to_s != 'no_role'
org.add_user(current_user)
end
Expand Down Expand Up @@ -116,6 +124,8 @@ def set_user_with_header_as_role(role:, org: nil, space: nil, user: nil, scopes:
set_user_with_header_as_reader(user: current_user, user_name: user_name, email: email)
when 'writer'
set_user_with_header_as_writer(user: current_user, user_name: user_name, email: email)
when 'service_permissions_reader'
set_user_with_header_as_service_permissions_reader(user: current_user, user_name: user_name, email: email)
when 'no_role' # not a real role - added for testing
set_user_with_header(user, user_name: user_name, email: email)
else
Expand Down
12 changes: 11 additions & 1 deletion spec/support/user_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,21 @@ def set_current_user_as_writer(opts = {})
set_current_user(user, scopes.merge(opts))
end

# rubocop:disable all
def set_current_user_as_service_permissions_reader(opts = {})
# rubocop:enable all
user = opts.delete(:user) || VCAP::CloudController::User.make
scopes = { scopes: %w(cloud_controller_service_permissions.read) }
set_current_user(user, scopes.merge(opts))
end

# rubocop:disable all
def set_current_user_as_role(role:, org: nil, space: nil, user: nil, scopes: nil)
# rubocop:enable all
current_user = user || VCAP::CloudController::User.make
current_user = set_current_user(current_user, scopes: scopes)

scope_roles = %w(admin admin_read_only global_auditor reader_and_writer reader writer)
scope_roles = %w(admin admin_read_only global_auditor reader_and_writer reader writer service_permissions_reader)
if org && !scope_roles.include?(role)
org.add_user(current_user)
end
Expand Down Expand Up @@ -108,6 +116,8 @@ def set_current_user_as_role(role:, org: nil, space: nil, user: nil, scopes: nil
set_current_user_as_reader(user: current_user)
when 'writer'
set_current_user_as_writer(user: current_user)
when 'service_permissions_reader'
set_current_user_as_service_permissions_reader(user: current_user)
when 'no_role'
nil
else
Expand Down
Loading

0 comments on commit 64e4a56

Please sign in to comment.