diff --git a/app/controllers/v3/apps_controller.rb b/app/controllers/v3/apps_controller.rb index 015a65e7bca..efc9b28f4c1 100644 --- a/app/controllers/v3/apps_controller.rb +++ b/app/controllers/v3/apps_controller.rb @@ -41,7 +41,8 @@ def index dataset = if permission_queryer.can_read_globally? AppListFetcher.fetch_all(message, eager_loaded_associations: Presenters::V3::AppPresenter.associated_resources) else - AppListFetcher.fetch(message, permission_queryer.readable_space_guids, eager_loaded_associations: Presenters::V3::AppPresenter.associated_resources) + AppListFetcher.fetch(message, permission_queryer.readable_application_supporter_space_guids, +eager_loaded_associations: Presenters::V3::AppPresenter.associated_resources) end decorators = [] @@ -68,7 +69,7 @@ def show app, space, org = AppFetcher.new.fetch(hashed_params[:guid]) - app_not_found! unless app && permission_queryer.can_read_from_space?(space.guid, org.guid) + app_not_found! unless app && permission_queryer.untrusted_can_read_from_space?(space.guid, org.guid) decorators = [] decorators << IncludeSpaceDecorator if IncludeSpaceDecorator.match?(message.include) @@ -227,7 +228,7 @@ def builds invalid_param!(message.errors.full_messages) unless message.valid? app, space, org = AppFetcher.new.fetch(hashed_params[:guid]) - app_not_found! unless app && permission_queryer.can_read_from_space?(space.guid, org.guid) + app_not_found! unless app && permission_queryer.untrusted_can_read_from_space?(space.guid, org.guid) dataset = AppBuildsListFetcher.fetch_all(app.guid, message) render status: :ok, json: Presenters::V3::PaginatedListPresenter.new( diff --git a/app/controllers/v3/builds_controller.rb b/app/controllers/v3/builds_controller.rb index 53bdc940916..95ca6543f81 100644 --- a/app/controllers/v3/builds_controller.rb +++ b/app/controllers/v3/builds_controller.rb @@ -14,7 +14,7 @@ def index dataset = if permission_queryer.can_read_globally? BuildListFetcher.fetch_all(message, eager_loaded_associations: Presenters::V3::BuildPresenter.associated_resources) else - BuildListFetcher.fetch_for_spaces(message, space_guids: permission_queryer.readable_space_guids, + BuildListFetcher.fetch_for_spaces(message, space_guids: permission_queryer.readable_space_application_supporter_guids, eager_loaded_associations: Presenters::V3::BuildPresenter.associated_resources) end diff --git a/lib/cloud_controller/membership.rb b/lib/cloud_controller/membership.rb index 4daa52d24ef..e82e1962fb4 100644 --- a/lib/cloud_controller/membership.rb +++ b/lib/cloud_controller/membership.rb @@ -90,6 +90,10 @@ def member_guids(roles: []) @space_auditor ||= @user.audited_spaces_dataset. association_join(:organization).map(&:guid) + when SPACE_APPLICATION_SUPPORTER + @space_application_supporter ||= + @user.application_supported_spaces_dataset. + association_join(:organization).map(&:guid) when ORG_USER @org_user ||= @user.organizations_dataset.map(&:guid) diff --git a/lib/cloud_controller/permissions.rb b/lib/cloud_controller/permissions.rb index c399a1e05ce..08dc9e46ff1 100644 --- a/lib/cloud_controller/permissions.rb +++ b/lib/cloud_controller/permissions.rb @@ -135,6 +135,10 @@ def can_read_from_space?(space_guid, org_guid) can_read_globally? || membership.has_any_roles?(ROLES_FOR_SPACE_READING, space_guid, org_guid) end + def untrusted_can_read_from_space?(space_guid, org_guid) + can_read_globally? || membership.has_any_roles?(ROLES_FOR_SPACE_APPLICATION_SUPPORTER_READING, space_guid, org_guid) + end + def can_read_secrets_in_space?(space_guid, org_guid) can_read_secrets_globally? || membership.has_any_roles?(ROLES_FOR_SPACE_SECRETS_READING, space_guid, org_guid) diff --git a/lib/cloud_controller/permissions/queryer.rb b/lib/cloud_controller/permissions/queryer.rb index a704c81c2ae..7fe591a9c9b 100644 --- a/lib/cloud_controller/permissions/queryer.rb +++ b/lib/cloud_controller/permissions/queryer.rb @@ -150,6 +150,10 @@ def can_read_from_space?(space_guid, org_guid) end end + def untrusted_can_read_from_space?(space_guid, org_guid) + db_permissions.untrusted_can_read_from_space?(space_guid, org_guid) + end + def can_read_secrets_in_space?(space_guid, org_guid) science 'can_read_secrets_in_space' do |e| e.context(space_guid: space_guid, org_guid: org_guid) diff --git a/spec/request/apps_spec.rb b/spec/request/apps_spec.rb index 1e708b6bbed..d7f87743564 100644 --- a/spec/request/apps_spec.rb +++ b/spec/request/apps_spec.rb @@ -471,11 +471,17 @@ app_model1_response_object, ] } + h['space_application_supporter'] = { + code: 200, + response_objects: [ + app_model1_response_object, + ] } + h['no_role'] = { code: 200, response_objects: [] } h end - it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS + it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS + ['space_application_supporter'] end describe 'query list parameters' do @@ -1310,7 +1316,7 @@ h end - it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + ['space_application_supporter'] end context 'when the user has permission to view the app' do @@ -1653,113 +1659,139 @@ ) } let(:body) do - { lifecycle: { type: 'buildpack', data: { buildpacks: ['http://github.com/myorg/awesome-buildpack'], - stack: 'cflinuxfs3' } } } + { + lifecycle: { + type: 'buildpack', + data: { + buildpacks: ['http://github.com/myorg/awesome-buildpack'], + stack: 'cflinuxfs3' + } + } + } end - let(:staging_message) { VCAP::CloudController::BuildCreateMessage.new(body) } - let(:per_page) { 2 } - let(:order_by) { '-created_at' } + describe 'permissions' do + let(:api_call) do + lambda { |headers| get "/v3/apps/#{app_model.guid}/builds", nil, headers } + end + # TODO: be a more specific on the response content + let(:build_response_object) { anything } + let(:expected_codes_and_responses) do + h = Hash.new(code: 200, response_objects: build_response_object) + h['org_auditor'] = { code: 404 } + h['org_billing_manager'] = { code: 404 } + h['no_role'] = { code: 404 } + h + end - before do - space.organization.add_user(user) - space.add_developer(user) - VCAP::CloudController::BuildpackLifecycle.new(package, staging_message).create_lifecycle_data_model(build) - VCAP::CloudController::BuildpackLifecycle.new(package, staging_message).create_lifecycle_data_model(second_build) - build.update(state: droplet.state, error_description: droplet.error_description) - second_build.update(state: second_droplet.state, error_description: second_droplet.error_description) + it_behaves_like 'permissions for list endpoint', ALL_PERMISSIONS + ['space_application_supporter'] end - it 'lists the builds for app' do - get "v3/apps/#{app_model.guid}/builds?order_by=#{order_by}&per_page=#{per_page}", nil, user_header + describe 'as a developer' do + let(:staging_message) { VCAP::CloudController::BuildCreateMessage.new(body) } + let(:per_page) { 2 } + let(:order_by) { '-created_at' } - parsed_response = MultiJson.load(last_response.body) + before do + space.organization.add_user(user) + space.add_developer(user) + VCAP::CloudController::BuildpackLifecycle.new(package, staging_message).create_lifecycle_data_model(build) + VCAP::CloudController::BuildpackLifecycle.new(package, staging_message).create_lifecycle_data_model(second_build) + build.update(state: droplet.state, error_description: droplet.error_description) + second_build.update(state: second_droplet.state, error_description: second_droplet.error_description) + end - expect(last_response.status).to eq(200) - expect(parsed_response['resources']).to include(hash_including('guid' => build.guid)) - expect(parsed_response['resources']).to include(hash_including('guid' => second_build.guid)) - expect(parsed_response).to be_a_response_like({ - 'pagination' => { - 'total_results' => 2, - 'total_pages' => 1, - 'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/builds?order_by=#{order_by}&page=1&per_page=2" }, - 'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/builds?order_by=#{order_by}&page=1&per_page=2" }, - 'next' => nil, - 'previous' => nil, - }, - 'resources' => [ - { - 'guid' => build.guid, - 'created_at' => iso8601, - 'updated_at' => iso8601, - 'state' => 'STAGED', - 'error' => nil, - 'lifecycle' => { - 'type' => 'buildpack', - 'data' => { - 'buildpacks' => ['http://github.com/myorg/awesome-buildpack'], - 'stack' => 'cflinuxfs3', - }, - }, - 'package' => { 'guid' => package.guid, }, - 'droplet' => { - 'guid' => droplet.guid - }, - 'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } }, - 'metadata' => { 'labels' => {}, 'annotations' => {} }, - 'links' => { - 'self' => { 'href' => "#{link_prefix}/v3/builds/#{build.guid}", }, - 'app' => { 'href' => "#{link_prefix}/v3/apps/#{package.app.guid}", }, - 'droplet' => { 'href' => "#{link_prefix}/v3/droplets/#{droplet.guid}", } - }, - 'created_by' => { 'guid' => user.guid, 'name' => 'bob the builder', 'email' => 'bob@loblaw.com', } - }, - { - 'guid' => second_build.guid, - 'created_at' => iso8601, - 'updated_at' => iso8601, - 'state' => 'STAGED', - 'error' => nil, - 'lifecycle' => { - 'type' => 'buildpack', - 'data' => { - 'buildpacks' => ['http://github.com/myorg/awesome-buildpack'], - 'stack' => 'cflinuxfs3', - }, - }, - 'package' => { 'guid' => package.guid, }, - 'droplet' => { - 'guid' => second_droplet.guid, - }, - 'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } }, - 'metadata' => { 'labels' => {}, 'annotations' => {} }, - 'links' => { - 'self' => { 'href' => "#{link_prefix}/v3/builds/#{second_build.guid}", }, - 'app' => { 'href' => "#{link_prefix}/v3/apps/#{package.app.guid}", }, - 'droplet' => { 'href' => "#{link_prefix}/v3/droplets/#{second_droplet.guid}", } - }, - 'created_by' => { 'guid' => user.guid, 'name' => 'bob the builder', 'email' => 'bob@loblaw.com', } - }, - ] - }) - end + it 'lists the builds for app' do + get "v3/apps/#{app_model.guid}/builds?order_by=#{order_by}&per_page=#{per_page}", nil, user_header - it_behaves_like 'list_endpoint_with_common_filters' do - let(:resource_klass) { VCAP::CloudController::BuildModel } - let(:additional_resource_params) { { app: app_model } } - let(:api_call) do - lambda { |headers, filters| get "/v3/apps/#{app_model.guid}/builds?#{filters}", nil, headers } + parsed_response = MultiJson.load(last_response.body) + + expect(last_response.status).to eq(200) + expect(parsed_response['resources']).to include(hash_including('guid' => build.guid)) + expect(parsed_response['resources']).to include(hash_including('guid' => second_build.guid)) + expect(parsed_response).to be_a_response_like({ + 'pagination' => { + 'total_results' => 2, + 'total_pages' => 1, + 'first' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/builds?order_by=#{order_by}&page=1&per_page=2" }, + 'last' => { 'href' => "#{link_prefix}/v3/apps/#{app_model.guid}/builds?order_by=#{order_by}&page=1&per_page=2" }, + 'next' => nil, + 'previous' => nil, + }, + 'resources' => [ + { + 'guid' => build.guid, + 'created_at' => iso8601, + 'updated_at' => iso8601, + 'state' => 'STAGED', + 'error' => nil, + 'lifecycle' => { + 'type' => 'buildpack', + 'data' => { + 'buildpacks' => ['http://github.com/myorg/awesome-buildpack'], + 'stack' => 'cflinuxfs3', + }, + }, + 'package' => { 'guid' => package.guid, }, + 'droplet' => { + 'guid' => droplet.guid + }, + 'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } }, + 'metadata' => { 'labels' => {}, 'annotations' => {} }, + 'links' => { + 'self' => { 'href' => "#{link_prefix}/v3/builds/#{build.guid}", }, + 'app' => { 'href' => "#{link_prefix}/v3/apps/#{package.app.guid}", }, + 'droplet' => { 'href' => "#{link_prefix}/v3/droplets/#{droplet.guid}", } + }, + 'created_by' => { 'guid' => user.guid, 'name' => 'bob the builder', 'email' => 'bob@loblaw.com', } + }, + { + 'guid' => second_build.guid, + 'created_at' => iso8601, + 'updated_at' => iso8601, + 'state' => 'STAGED', + 'error' => nil, + 'lifecycle' => { + 'type' => 'buildpack', + 'data' => { + 'buildpacks' => ['http://github.com/myorg/awesome-buildpack'], + 'stack' => 'cflinuxfs3', + }, + }, + 'package' => { 'guid' => package.guid, }, + 'droplet' => { + 'guid' => second_droplet.guid, + }, + 'relationships' => { 'app' => { 'data' => { 'guid' => app_model.guid } } }, + 'metadata' => { 'labels' => {}, 'annotations' => {} }, + 'links' => { + 'self' => { 'href' => "#{link_prefix}/v3/builds/#{second_build.guid}", }, + 'app' => { 'href' => "#{link_prefix}/v3/apps/#{package.app.guid}", }, + 'droplet' => { 'href' => "#{link_prefix}/v3/droplets/#{second_droplet.guid}", } + }, + 'created_by' => { 'guid' => user.guid, 'name' => 'bob the builder', 'email' => 'bob@loblaw.com', } + }, + ] + }) end - let(:headers) { admin_header } - end - it 'filters on label_selector' do - VCAP::CloudController::BuildLabelModel.make(key_name: 'fruit', value: 'strawberry', build: build) + it_behaves_like 'list_endpoint_with_common_filters' do + let(:resource_klass) { VCAP::CloudController::BuildModel } + let(:additional_resource_params) { { app: app_model } } + let(:api_call) do + lambda { |headers, filters| get "/v3/apps/#{app_model.guid}/builds?#{filters}", nil, headers } + end + let(:headers) { admin_header } + end - get "/v3/apps/#{app_model.guid}/builds?label_selector=fruit=strawberry", {}, user_header + it 'filters on label_selector' do + VCAP::CloudController::BuildLabelModel.make(key_name: 'fruit', value: 'strawberry', build: build) - expect(last_response.status).to eq(200) - expect(parsed_response['resources'].count).to eq(1) - expect(parsed_response['resources'][0]['guid']).to eq(build.guid) + get "/v3/apps/#{app_model.guid}/builds?label_selector=fruit=strawberry", {}, user_header + + expect(last_response.status).to eq(200) + expect(parsed_response['resources'].count).to eq(1) + expect(parsed_response['resources'][0]['guid']).to eq(build.guid) + end end end