diff --git a/app/access/private_domain_access.rb b/app/access/private_domain_access.rb index 5d12ee04a40..accfdc01a81 100644 --- a/app/access/private_domain_access.rb +++ b/app/access/private_domain_access.rb @@ -66,7 +66,7 @@ def read_for_update?(private_domain, _params=nil) def update?(private_domain, _params=nil) return true if admin_user? - return false if private_domain.in_suspended_org? + return false if private_domain.in_suspended_or_deleting_org? private_domain.owning_organization.managers.include?(context.user) end diff --git a/app/access/process_model_access.rb b/app/access/process_model_access.rb index 48c8aeec575..18e2b62a3b8 100644 --- a/app/access/process_model_access.rb +++ b/app/access/process_model_access.rb @@ -54,7 +54,7 @@ def index_with_token?(_) def create?(app, _params=nil) return true if admin_user? - return false if app.in_suspended_org? + return false if app.in_suspended_or_deleting_org? || app.space&.suspended_or_deleting? app.space&.has_developer?(context.user) end diff --git a/app/access/route_access.rb b/app/access/route_access.rb index 79c5bebf36d..715a4130212 100644 --- a/app/access/route_access.rb +++ b/app/access/route_access.rb @@ -76,7 +76,7 @@ def index_with_token?(_) def can_write_to_route(route, is_create=false) return true if context.queryer.can_write_globally? - return false if route.in_suspended_org? + return false if route.in_suspended_or_deleting_org? || route.space.suspended_or_deleting? return false if route.wildcard_host? && route.domain.shared? FeatureFlag.raise_unless_enabled!(:route_creation) if is_create diff --git a/app/access/route_mapping_access.rb b/app/access/route_mapping_access.rb index 85fec06a577..9c792eb5c36 100644 --- a/app/access/route_mapping_access.rb +++ b/app/access/route_mapping_access.rb @@ -54,9 +54,11 @@ def index_with_token?(_) def create?(route_mapping, _params=nil) return true if admin_user? - return false if route_mapping.route.in_suspended_org? - route_mapping.route.space.has_developer?(context.user) + space = route_mapping.route.space + return false if space.in_suspended_or_deleting_org? || space.suspended_or_deleting? + + space.has_developer?(context.user) end def read_for_update?(route_mapping, _params=nil) diff --git a/app/access/service_instance_access.rb b/app/access/service_instance_access.rb index 889b7e8e757..16a658b80ce 100644 --- a/app/access/service_instance_access.rb +++ b/app/access/service_instance_access.rb @@ -56,14 +56,14 @@ def create?(service_instance, _params=nil) return true if admin_user? FeatureFlag.raise_unless_enabled!(:service_instance_creation) - return false if service_instance.in_suspended_org? + return false if service_instance.in_suspended_or_deleting_org? || service_instance.space&.suspended_or_deleting? service_instance.space&.has_developer?(context.user) && allowed?(service_instance) end def read_for_update?(service_instance, _params=nil) return true if admin_user? - return false if service_instance.in_suspended_org? + return false if service_instance.in_suspended_or_deleting_org? || service_instance.space&.suspended_or_deleting? service_instance.space&.has_developer?(context.user) end @@ -74,7 +74,7 @@ def update?(service_instance, params=nil) def delete?(service_instance) return true if admin_user? - return false if service_instance.in_suspended_org? + return false if service_instance.in_suspended_or_deleting_org? || service_instance.space&.suspended_or_deleting? service_instance.space&.has_developer?(context.user) end diff --git a/app/access/service_key_access.rb b/app/access/service_key_access.rb index e2c565bd755..b8975516268 100644 --- a/app/access/service_key_access.rb +++ b/app/access/service_key_access.rb @@ -52,9 +52,11 @@ def index_with_token?(_) def create?(service_key, _params=nil) return true if admin_user? - return false if service_key.in_suspended_org? - service_key.service_instance.space.has_developer?(context.user) + space = service_key.service_instance.space + return false if space.in_suspended_or_deleting_org? || space.suspended_or_deleting? + + space.has_developer?(context.user) end def delete?(service_key) diff --git a/app/access/space_access.rb b/app/access/space_access.rb index b9f9d542505..52ab2d17d12 100644 --- a/app/access/space_access.rb +++ b/app/access/space_access.rb @@ -44,7 +44,7 @@ def read_related_object_for_update?(space, params=nil) def create?(space, _params=nil) return true if context.queryer.can_write_globally? - return false if space.in_suspended_org? + return false if space.in_suspended_or_deleting_org? || space.suspended_or_deleting? context.queryer.can_write_to_active_org?(space.organization_id) end @@ -57,7 +57,7 @@ def can_remove_related_object?(space, params) def read_for_update?(space, _params=nil) return true if context.queryer.can_write_globally? - return false if space.in_suspended_org? + return false if space.in_suspended_or_deleting_org? || space.suspended_or_deleting? context.queryer.can_write_to_active_org?(space.organization_id) || context.queryer.can_update_active_space?(space.id, space.organization_id) end diff --git a/app/access/space_quota_definition_access.rb b/app/access/space_quota_definition_access.rb index 1624d27cc48..3cbf474dd46 100644 --- a/app/access/space_quota_definition_access.rb +++ b/app/access/space_quota_definition_access.rb @@ -48,7 +48,7 @@ def index_with_token?(_) def create?(space_quota_definition, _params=nil) return true if admin_user? - return false if space_quota_definition.organization.suspended? + return false if space_quota_definition.organization.suspended_or_deleting? space_quota_definition.organization.managers.include?(context.user) end diff --git a/app/actions/organization_update.rb b/app/actions/organization_update.rb index 55d96c7fedb..ca2237e4b04 100644 --- a/app/actions/organization_update.rb +++ b/app/actions/organization_update.rb @@ -15,6 +15,7 @@ def update(org, message) AnnotationsUpdate.update(org, message.annotations, OrganizationAnnotationModel) if message.requested?(:suspended) + error!("Organization '#{org.name}' is being deleted and cannot be reactivated.") if org.deleting? && message.suspended == false org.status = message.suspended ? Organization::SUSPENDED : Organization::ACTIVE end diff --git a/app/actions/space_create.rb b/app/actions/space_create.rb index 87239c3bdfe..948acc9a114 100644 --- a/app/actions/space_create.rb +++ b/app/actions/space_create.rb @@ -10,7 +10,11 @@ def initialize(user_audit_info:) def create(org, message) space = nil Space.db.transaction do - space = VCAP::CloudController::Space.create(name: message.name, organization: org) + space = VCAP::CloudController::Space.create( + name: message.name, + organization: org, + status: message.suspended ? Space::SUSPENDED : Space::ACTIVE + ) MetadataUpdate.update(space, message) Repositories::SpaceEventRepository.new.record_space_create(space, user_audit_info, message.audit_hash) end diff --git a/app/actions/space_update.rb b/app/actions/space_update.rb index 42641877ead..f84cbd52f04 100644 --- a/app/actions/space_update.rb +++ b/app/actions/space_update.rb @@ -11,6 +11,10 @@ def update(space, message) space.db.transaction do space.lock! space.name = message.name if message.requested?(:name) + if message.requested?(:suspended) + error!("Space '#{space.name}' is being deleted and cannot be reactivated.") if space.deleting? && message.suspended == false + space.status = message.suspended ? Space::SUSPENDED : Space::ACTIVE + end MetadataUpdate.update(space, message) space.save diff --git a/app/controllers/runtime/route_mappings_controller.rb b/app/controllers/runtime/route_mappings_controller.rb index 0aee4db0edb..513e4486c95 100644 --- a/app/controllers/runtime/route_mappings_controller.rb +++ b/app/controllers/runtime/route_mappings_controller.rb @@ -34,7 +34,7 @@ def create raise CloudController::Errors::ApiError.new_from_details('RouteNotFound', request_attrs['route_guid']) unless route raise CloudController::Errors::ApiError.new_from_details('AppNotFound', request_attrs['app_guid']) unless process raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') unless permissions.can_write_to_active_space?(process.space.id) - raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.is_space_active?(process.space.id) + raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.writable_space_state(process.space.id) == :active route_mapping = V2::RouteMappingCreate.new(UserAuditInfo.from_context(SecurityContext), route, process, request_attrs, logger).add @@ -61,7 +61,7 @@ def delete(guid) raise CloudController::Errors::ApiError.new_from_details('RouteMappingNotFound', guid) unless route_mapping raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') unless permissions.can_write_to_active_space?(route_mapping.space.id) - raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.is_space_active?(route_mapping.space.id) + raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.writable_space_state(route_mapping.space.id) == :active RouteMappingDelete.new(UserAuditInfo.from_context(SecurityContext)).delete(route_mapping) diff --git a/app/controllers/services/service_bindings_controller.rb b/app/controllers/services/service_bindings_controller.rb index e0d29fc9c95..63b09cf2050 100644 --- a/app/controllers/services/service_bindings_controller.rb +++ b/app/controllers/services/service_bindings_controller.rb @@ -64,7 +64,7 @@ def create raise CloudController::Errors::ApiError.new_from_details('AppNotFound', @request_attrs['app_guid']) unless app raise CloudController::Errors::ApiError.new_from_details('ServiceInstanceNotFound', @request_attrs['service_instance_guid']) unless service_instance raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') unless permissions.can_write_to_active_space?(app.space.id) - raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.is_space_active?(app.space.id) + raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.writable_space_state(app.space.id) == :active creator = ServiceBindingCreate.new(UserAuditInfo.from_context(SecurityContext)) service_binding = creator.create(app, service_instance, message, volume_services_enabled?, accepts_incomplete) @@ -95,7 +95,7 @@ def delete(guid) raise CloudController::Errors::ApiError.new_from_details('ServiceBindingNotFound', guid) unless service_binding raise CloudController::Errors::ApiError.new_from_details('NotAuthorized') unless permissions.can_write_to_active_space?(service_binding.space.id) - raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.is_space_active?(service_binding.space.id) + raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') unless permissions.writable_space_state(service_binding.space.id) == :active accepts_incomplete = convert_flag_to_bool(params['accepts_incomplete']) diff --git a/app/controllers/v3/app_features_controller.rb b/app/controllers/v3/app_features_controller.rb index 761c626b109..bb38d9cde00 100644 --- a/app/controllers/v3/app_features_controller.rb +++ b/app/controllers/v3/app_features_controller.rb @@ -47,7 +47,7 @@ def update else unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) end - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) message = VCAP::CloudController::AppFeatureUpdateMessage.new(hashed_params['body']) unprocessable!(message.errors.full_messages) unless message.valid? diff --git a/app/controllers/v3/application_controller.rb b/app/controllers/v3/application_controller.rb index 30a8f2ca69d..8718e72fa57 100644 --- a/app/controllers/v3/application_controller.rb +++ b/app/controllers/v3/application_controller.rb @@ -26,6 +26,14 @@ def suspended! raise CloudController::Errors::ApiError.new_from_details('OrgSuspended') end + def space_suspended! + raise CloudController::Errors::ApiError.new_from_details('SpaceSuspended') + end + + def resource_being_deleted!(resource_name) + raise CloudController::Errors::ApiError.new_from_details('ResourceBeingDeleted', resource_name) + end + def resource_not_found_with_message!(message) raise CloudController::Errors::ApiError.new_from_details('ResourceNotFound', message) end @@ -150,6 +158,33 @@ def permission_queryer @permission_queryer ||= VCAP::CloudController::Permissions.new(VCAP::CloudController::SecurityContext.current_user) end + def require_writable_org!(org) + require_writable_org_id!(org.id) + end + + def require_writable_org_id!(org_id) + case permission_queryer.writable_org_state(org_id) + when :active + nil + when :deleting + resource_being_deleted!('organization') + when :suspended + suspended! + end + end + + def require_writable_space!(space) + org_state = permission_queryer.writable_org_state(space.organization_id) + return resource_being_deleted!('organization') if org_state == :deleting + + space_state = permission_queryer.writable_space_state(space.id) + return resource_being_deleted!('space') if space_state == :deleting + + return suspended! if org_state == :suspended + + space_suspended! if space_state == :suspended + end + def add_warning_headers(warnings) return if warnings.nil? raise ArgumentError.new('warnings should be an array') unless warnings.is_a?(Array) diff --git a/app/controllers/v3/apps_controller.rb b/app/controllers/v3/apps_controller.rb index 3ed4e2a8a9d..69e19dbcb93 100644 --- a/app/controllers/v3/apps_controller.rb +++ b/app/controllers/v3/apps_controller.rb @@ -92,7 +92,7 @@ def create space = Space.where(guid: message.space_guid).first unprocessable_space! unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) FeatureFlag.raise_unless_enabled!(:diego_docker) if message.lifecycle_type == VCAP::CloudController::PackageModel::DOCKER_TYPE lifecycle = AppLifecycleProvider.provide_for_create(message) FeatureFlag.raise_unless_enabled!(:diego_cnb) if lifecycle.type == VCAP::CloudController::Lifecycles::CNB @@ -123,7 +123,7 @@ def update app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) lifecycle = AppLifecycleProvider.provide_for_update(message, app) unprocessable!(lifecycle.errors.full_messages) unless lifecycle.valid? @@ -152,7 +152,7 @@ def destroy app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) delete_action = AppDelete.new(user_audit_info) deletion_job = VCAP::CloudController::Jobs::DeleteActionJob.new(AppModel, app.guid, delete_action) @@ -169,7 +169,7 @@ def start app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unprocessable_lacking_droplet! unless app.droplet unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) FeatureFlag.raise_unless_enabled!(:diego_docker) if app.lifecycle_type == DockerLifecycleDataModel::LIFECYCLE_TYPE FeatureFlag.raise_unless_enabled!(:diego_cnb) if app.lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE @@ -191,7 +191,7 @@ def stop app, space = AppFetcher.new.fetch(hashed_params[:guid]) app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) AppStop.stop(app:, user_audit_info:) TelemetryLogger.v3_emit( @@ -212,7 +212,7 @@ def restart app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unprocessable_lacking_droplet! unless app.droplet unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) FeatureFlag.raise_unless_enabled!(:diego_docker) if app.lifecycle_type == DockerLifecycleDataModel::LIFECYCLE_TYPE FeatureFlag.raise_unless_enabled!(:diego_cnb) if app.lifecycle_type == CNBLifecycleDataModel::LIFECYCLE_TYPE @@ -237,7 +237,7 @@ def clear_buildpack_cache app, space = AppFetcher.new.fetch(hashed_params[:guid]) app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_delete_buildpack_cache?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) delete_job = Jobs::V3::BuildpackCacheDelete.new(app.guid) job = Jobs::Enqueuer.new(queue: Jobs::Queues.generic).enqueue_pollable(delete_job) @@ -299,7 +299,7 @@ def update_environment_variables app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) message = UpdateEnvironmentVariablesMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -317,7 +317,7 @@ def assign_current_droplet app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) deployment_in_progress! if app.deploying? AppAssignDroplet.new(user_audit_info).assign(app, droplet) diff --git a/app/controllers/v3/builds_controller.rb b/app/controllers/v3/builds_controller.rb index c2072481d5b..5eaa20eec6f 100644 --- a/app/controllers/v3/builds_controller.rb +++ b/app/controllers/v3/builds_controller.rb @@ -40,8 +40,8 @@ def create package = PackageModel.where(guid: message.package_guid). eager(:app, :space, space: :organization, app: %i[buildpack_lifecycle_data cnb_lifecycle_data]).first - unprocessable_package! unless package && - permission_queryer.can_manage_apps_in_active_space?(package.space.id) && permission_queryer.is_space_active?(package.space.id) + unprocessable_package! unless package && permission_queryer.can_manage_apps_in_active_space?(package.space.id) + require_writable_space!(package.space) if package FeatureFlag.raise_unless_enabled!(:diego_docker) if package.type == PackageModel::DOCKER_TYPE @@ -98,7 +98,7 @@ def update unauthorized! unless permission_queryer.can_update_build_state? else unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) end build = BuildUpdate.new.update(build, create_valid_update_message) diff --git a/app/controllers/v3/deployments_controller.rb b/app/controllers/v3/deployments_controller.rb index a5b93281bb4..5802c0f2730 100644 --- a/app/controllers/v3/deployments_controller.rb +++ b/app/controllers/v3/deployments_controller.rb @@ -41,11 +41,11 @@ def create message = DeploymentCreateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? - unable_to_use = 'Unable to use app. Ensure that the app exists and you have access to it and the organization is not suspended.' + unable_to_use = 'Unable to use app. Ensure that the app exists and you have access to it.' app = AppModel.find(guid: message.app_guid) - unprocessable!(unable_to_use) unless app && permission_queryer.can_manage_apps_in_active_space?(app.space.id) && - permission_queryer.is_space_active?(app.space.id) + unprocessable!(unable_to_use) unless app && permission_queryer.can_manage_apps_in_active_space?(app.space.id) + require_writable_space!(app.space) unprocessable!('Cannot create deployment from a revision for an app without revisions enabled') if message.revision_guid && !app.revisions_enabled begin @@ -80,7 +80,7 @@ def update resource_not_found!(:deployment) unless deployment && permission_queryer.can_read_from_space?(deployment.app.space.id, deployment.app.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(deployment.app.space.id) - suspended! unless permission_queryer.is_space_active?(deployment.app.space.id) + require_writable_space!(deployment.app.space) message = VCAP::CloudController::DeploymentUpdateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -93,8 +93,8 @@ def update def cancel deployment = DeploymentModel.find(guid: hashed_params[:guid]) - resource_not_found!(:deployment) unless deployment && permission_queryer.can_manage_apps_in_active_space?(deployment.app.space.id) && - permission_queryer.is_space_active?(deployment.app.space.id) + resource_not_found!(:deployment) unless deployment && permission_queryer.can_manage_apps_in_active_space?(deployment.app.space.id) + require_writable_space!(deployment.app.space) begin DeploymentCancel.cancel(deployment:, user_audit_info:) @@ -109,8 +109,8 @@ def cancel def continue deployment = DeploymentModel.find(guid: hashed_params[:guid]) - resource_not_found!(:deployment) unless deployment && permission_queryer.can_manage_apps_in_active_space?(deployment.app.space.id) && - permission_queryer.is_space_active?(deployment.app.space.id) + resource_not_found!(:deployment) unless deployment && permission_queryer.can_manage_apps_in_active_space?(deployment.app.space.id) + require_writable_space!(deployment.app.space) begin DeploymentContinue.continue(deployment:, user_audit_info:) diff --git a/app/controllers/v3/domains_controller.rb b/app/controllers/v3/domains_controller.rb index 834556672fd..5715e952948 100644 --- a/app/controllers/v3/domains_controller.rb +++ b/app/controllers/v3/domains_controller.rb @@ -85,7 +85,7 @@ def update domain_not_found! unless domain unauthorized! unless can_write_to_active_org?(domain.owning_organization_id) - suspended! unless org_active?(domain.owning_organization_id) + require_writable_org_id!(domain.owning_organization_id) domain = DomainUpdate.new.update(domain:, message:) @@ -100,7 +100,7 @@ def destroy domain_not_found! unless domain unauthorized! unless can_write_to_active_org?(domain.owning_organization_id) - suspended! unless org_active?(domain.owning_organization_id) + require_writable_org_id!(domain.owning_organization_id) unprocessable!('This domain is shared with other organizations. Unshare before deleting.') unless domain.shared_organizations_dataset.empty? @@ -119,7 +119,7 @@ def update_shared_orgs domain_not_found! unless domain unauthorized! unless can_write_to_active_org?(domain.owning_organization_id) - suspended! unless org_active?(domain.owning_organization_id) + require_writable_org_id!(domain.owning_organization_id) shared_orgs = verify_shared_organizations_guids!(message, domain.owning_organization_guid) @@ -176,7 +176,9 @@ def check_unshare_domain_permissions!(owning_org_id, shared_org_id) return if shared_org_writable && org_active?(shared_org_id) unauthorized! unless owning_org_writable || shared_org_writable - suspended! + + require_writable_org_id!(owning_org_id) if owning_org_writable + require_writable_org_id!(shared_org_id) end def check_create_private_domain_permissions!(message) @@ -184,7 +186,7 @@ def check_create_private_domain_permissions!(message) unprocessable_org!(message.organization_guid) unless org unauthorized! unless can_write_to_active_org?(org.id) - suspended! unless org_active?(org.id) + require_writable_org!(org) FeatureFlag.raise_unless_enabled!(:private_domain_creation) unless permission_queryer.can_write_globally? end @@ -221,7 +223,7 @@ def can_write_to_active_org?(org_id) end def org_active?(org_id) - permission_queryer.is_org_active?(org_id) + permission_queryer.writable_org_state(org_id) == :active end def domain_not_found! diff --git a/app/controllers/v3/droplets_controller.rb b/app/controllers/v3/droplets_controller.rb index 1d98ec0adf0..ae80767ce4e 100644 --- a/app/controllers/v3/droplets_controller.rb +++ b/app/controllers/v3/droplets_controller.rb @@ -66,7 +66,7 @@ def update unauthorized! unless permission_queryer.can_update_build_state? else unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) end message = VCAP::CloudController::DropletUpdateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -81,7 +81,7 @@ def destroy droplet_not_found! unless droplet && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) in_use!(droplet) if droplet.current? delete_action = DropletDelete.new(user_audit_info) @@ -103,7 +103,7 @@ def create_copy app_not_found! unless destination_app && permission_queryer.can_read_from_space?(destination_app.space.id, destination_app.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(destination_app.space.id) - suspended! unless permission_queryer.is_space_active?(destination_app.space.id) + require_writable_space!(destination_app.space) DropletCopy.new(source_droplet).copy(destination_app, user_audit_info) end @@ -116,7 +116,7 @@ def create_fresh unprocessable_app!(message.relationships_message.app_guid) unless app && permission_queryer.can_read_from_space?(app.space.id, app.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(app.space.id) - suspended! unless permission_queryer.is_space_active?(app.space.id) + require_writable_space!(app.space) DropletCreate.new.create(app, message, user_audit_info) end @@ -130,7 +130,7 @@ def upload droplet_not_found! unless droplet && permission_queryer.can_read_from_space?(droplet.space.id, droplet.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(droplet.space.id) - suspended! unless permission_queryer.is_space_active?(droplet.space.id) + require_writable_space!(droplet.space) unprocessable!('Droplet may be uploaded only once. Create a new droplet to upload bits.') unless droplet.state == DropletModel::AWAITING_UPLOAD_STATE diff --git a/app/controllers/v3/organizations_controller.rb b/app/controllers/v3/organizations_controller.rb index 529f024957a..f28e62374ef 100644 --- a/app/controllers/v3/organizations_controller.rb +++ b/app/controllers/v3/organizations_controller.rb @@ -200,7 +200,7 @@ def fetch_editable_org(guid) org = fetch_org(guid) org_not_found! unless org && permission_queryer.can_read_from_org?(org.id) unauthorized! unless permission_queryer.can_write_to_active_org?(org.id) - suspended! unless permission_queryer.is_org_active?(org.id) + require_writable_org!(org) org end diff --git a/app/controllers/v3/packages_controller.rb b/app/controllers/v3/packages_controller.rb index feb509b99ad..00504190e17 100644 --- a/app/controllers/v3/packages_controller.rb +++ b/app/controllers/v3/packages_controller.rb @@ -59,7 +59,7 @@ def upload package = PackageModel.where(guid: hashed_params[:guid]).first package_not_found! unless package && permission_queryer.can_read_from_space?(package.space.id, package.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(package.space.id) - suspended! unless permission_queryer.is_space_active?(package.space.id) + require_writable_space!(package.space) unprocessable!('Package type must be bits.') unless package.type == 'bits' bits_already_uploaded! if package.state != PackageModel::CREATED_STATE @@ -126,7 +126,7 @@ def update package_not_found! unless package && permission_queryer.can_read_from_space?(space.id, space.organization_id) unprocessable_non_docker_package_update! if package.type != PackageModel::DOCKER_TYPE && (message.username || message.password) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) package = PackageUpdate.new.update(package, message) @@ -138,7 +138,7 @@ def destroy package_not_found! unless package && permission_queryer.can_read_from_space?(package.space.id, package.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(package.space.id) - suspended! unless permission_queryer.is_space_active?(package.space.id) + require_writable_space!(package.space) delete_action = PackageDelete.new(user_audit_info) deletion_job = VCAP::CloudController::Jobs::DeleteActionJob.new(PackageModel, package.guid, delete_action) @@ -157,7 +157,7 @@ def create_fresh unprocessable_app! unless app && permission_queryer.can_read_from_space?(app.space.id, app.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(app.space.id) - suspended! unless permission_queryer.is_space_active?(app.space.id) + require_writable_space!(app.space) if message.type != PackageModel::DOCKER_TYPE && app.docker? unprocessable_non_docker_package! @@ -174,13 +174,13 @@ def create_copy unprocessable_app! unless destination_app && permission_queryer.can_read_from_space?(destination_app.space.id, destination_app.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(destination_app.space.id) - suspended! unless permission_queryer.is_space_active?(destination_app.space.id) + require_writable_space!(destination_app.space) source_package = PackageModel.where(guid: hashed_params[:source_guid]).first unprocessable_source_package! unless source_package && permission_queryer.can_read_from_space?(source_package.space.id, source_package.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(source_package.space.id) - suspended! unless permission_queryer.is_space_active?(source_package.space.id) + require_writable_space!(source_package.space) PackageCopy.new.copy( destination_app_guid: app_guid, diff --git a/app/controllers/v3/processes_controller.rb b/app/controllers/v3/processes_controller.rb index 876e48a6277..6d653370fd4 100644 --- a/app/controllers/v3/processes_controller.rb +++ b/app/controllers/v3/processes_controller.rb @@ -144,7 +144,7 @@ def find_process_and_space def ensure_can_write unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(@space.id) - suspended! unless permission_queryer.is_space_active?(@space.id) + require_writable_space!(@space) end def process_not_found! diff --git a/app/controllers/v3/revisions_controller.rb b/app/controllers/v3/revisions_controller.rb index 90fd3b66c1b..40dcc9736e9 100644 --- a/app/controllers/v3/revisions_controller.rb +++ b/app/controllers/v3/revisions_controller.rb @@ -37,7 +37,7 @@ def fetch_revision(guid, needs_write_permissions: false, needs_secrets_read_perm space = app.space resource_not_found!(:revision) unless permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! if needs_write_permissions && !permission_queryer.can_write_to_active_space?(space.id) - suspended! if needs_write_permissions && !permission_queryer.is_space_active?(space.id) + require_writable_space!(space) if needs_write_permissions unauthorized! if needs_secrets_read_permission && !permission_queryer.can_read_secrets_in_space?(space.id, space.organization_id) revision diff --git a/app/controllers/v3/roles_controller.rb b/app/controllers/v3/roles_controller.rb index a07f6cf2ea5..eb92d3676ed 100644 --- a/app/controllers/v3/roles_controller.rb +++ b/app/controllers/v3/roles_controller.rb @@ -71,10 +71,10 @@ def destroy if role.for_space? unauthorized! unless permission_queryer.can_update_active_space?(role.space_id, role.space.organization_id) - suspended! unless permission_queryer.is_space_active?(role.space_id) + require_writable_space!(role.space) else unauthorized! unless permission_queryer.can_write_to_active_org?(role.organization_id) - suspended! unless permission_queryer.is_org_active?(role.organization_id) + require_writable_org!(role.organization) if role.type == VCAP::CloudController::RoleTypes::ORGANIZATION_USER org = Organization.find(id: role.organization_id) @@ -99,7 +99,7 @@ def create_space_role(message) unprocessable_space! unless space unauthorized! unless permission_queryer.can_update_active_space?(space.id, space.organization_id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) user_guid = message.user_guid || lookup_user_guid_in_uaa(message.username, message.user_origin, creating_space_role: true) user = fetch_readable_user(user_guid) @@ -116,7 +116,7 @@ def create_org_role(message) org = Organization.find(guid: message.organization_guid) unprocessable_organization! unless org unauthorized! unless permission_queryer.can_write_to_active_org?(org.id) - suspended! unless permission_queryer.is_org_active?(org.id) + require_writable_org!(org) user_guid = if message.username && message.user_origin && message.user_origin != 'uaa' && org_managers_can_create_users? create_or_get_uaa_user(message) diff --git a/app/controllers/v3/routes_controller.rb b/app/controllers/v3/routes_controller.rb index 7c7e48ce212..ffa539bbfa0 100644 --- a/app/controllers/v3/routes_controller.rb +++ b/app/controllers/v3/routes_controller.rb @@ -82,7 +82,7 @@ def create unprocessable_space! unless space unprocessable_domain! unless domain unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) unprocessable_wildcard! if domain.shared? && message.wildcard? && !permission_queryer.can_write_globally? route = RouteCreate.new(user_audit_info).create(message:, space:, domain:) @@ -103,7 +103,7 @@ def update unprocessable!(message.errors.full_messages) unless message.valid? unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) VCAP::CloudController::RouteUpdate.new.update(route:, message:) @@ -117,7 +117,7 @@ def destroy unprocessable!(message.errors.full_messages) unless message.valid? unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) delete_action = RouteDeleteAction.new(user_audit_info) deletion_job = VCAP::CloudController::Jobs::DeleteActionJob.new(Route, route.guid, delete_action) @@ -130,7 +130,7 @@ def share_routes FeatureFlag.raise_unless_enabled!(:route_sharing) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) message = VCAP::CloudController::ToManyRelationshipMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -151,7 +151,7 @@ def share_routes def unshare_route FeatureFlag.raise_unless_enabled!(:route_sharing) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) space_guid = hashed_params[:space_guid] @@ -182,7 +182,7 @@ def transfer_owner unprocessable!(message.errors.full_messages) unless message.valid? unauthorized! unless permission_queryer.can_write_to_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) target_space = Space.first(guid: message.space_guid) target_space_error = check_if_space_is_accessible(target_space) @@ -213,7 +213,7 @@ def insert_destinations unprocessable!(message.errors.full_messages) unless message.valid? unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) UpdateRouteDestinations.add(message.destinations_array, route, apps_hash(message), user_audit_info) @@ -227,7 +227,7 @@ def replace_destinations unprocessable!(message.errors.full_messages) unless message.valid? unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) UpdateRouteDestinations.replace(message.destinations_array, route, apps_hash(message), user_audit_info) @@ -243,7 +243,7 @@ def update_destination route = Route.find(guid: hashed_params[:guid]) route_not_found! unless route && permission_queryer.can_read_route?(route.space_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) destination = RouteMappingModel.find(guid: hashed_params[:destination_guid]) unprocessable_destination! unless destination @@ -280,7 +280,7 @@ def destroy_destination route_not_found! unless permission_queryer.can_read_route?(route.space_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(route.space_id) - suspended! unless permission_queryer.is_space_active?(route.space_id) + require_writable_space!(route.space) destination = RouteMappingModel.find(guid: hashed_params[:destination_guid]) unprocessable_destination! unless destination @@ -382,7 +382,7 @@ def can_read_space?(space) end def can_write_space?(space) - permission_queryer.can_write_to_active_space?(space.id) && permission_queryer.is_space_active?(space.id) + permission_queryer.can_write_to_active_space?(space.id) && permission_queryer.writable_space_state(space.id) == :active end def check_spaces_exist_and_are_writeable!(route, request_guids, found_spaces) @@ -425,8 +425,13 @@ def check_if_space_is_accessible(space) return 'Ensure the space exists and that you have access to it.' elsif !permission_queryer.can_manage_apps_in_active_space?(space.id) return "You don't have write permission for the target space." - elsif !permission_queryer.is_space_active?(space.id) - return 'The target organization is suspended.' + else + case permission_queryer.writable_space_state(space.id) + when :deleting + return 'The target space is being deleted.' + when :suspended + return 'The target organization or space is suspended.' + end end nil diff --git a/app/controllers/v3/security_groups_controller.rb b/app/controllers/v3/security_groups_controller.rb index 8ffce61e269..918833e360f 100644 --- a/app/controllers/v3/security_groups_controller.rb +++ b/app/controllers/v3/security_groups_controller.rb @@ -122,7 +122,7 @@ def delete_running_spaces space = Space.find(guid: hashed_params[:space_guid]) unprocessable_space! unless space unauthorized! unless permission_queryer.can_update_active_space?(space.id, space.organization_id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) unprocessable_space! unless security_group.spaces.include?(space) SecurityGroupUnapply.unapply_running(security_group, space) @@ -139,7 +139,7 @@ def delete_staging_spaces space = Space.find(guid: hashed_params[:space_guid]) unprocessable_space! unless space unauthorized! unless permission_queryer.can_update_active_space?(space.id, space.organization_id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) unprocessable_space! unless security_group.staging_spaces.include?(space) SecurityGroupUnapply.unapply_staging(security_group, space) @@ -169,21 +169,13 @@ def unprocessable_space! private def check_unwritable_spaces(space_guids) - unauthorized_space = false - suspended_space = false space_guids.each do |space_guid| space = Space.find(guid: space_guid) - if space - if !permission_queryer.can_update_active_space?(space.id, space.organization_id) - unauthorized_space = true - break - elsif !suspended_space && !permission_queryer.is_space_active?(space.id) - suspended_space = true - end - end + next unless space + + unauthorized! unless permission_queryer.can_update_active_space?(space.id, space.organization_id) + require_writable_space!(space) end - unauthorized! if unauthorized_space - suspended! if suspended_space end def presenter_args diff --git a/app/controllers/v3/service_brokers_controller.rb b/app/controllers/v3/service_brokers_controller.rb index 5a439997d6a..71b2c44f04b 100644 --- a/app/controllers/v3/service_brokers_controller.rb +++ b/app/controllers/v3/service_brokers_controller.rb @@ -59,7 +59,7 @@ def create space = Space.where(guid: message.space_guid).first unprocessable_space! unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) else unauthorized! unless permission_queryer.can_write_globally? end @@ -83,7 +83,7 @@ def update space = service_broker.space broker_not_found! unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) else broker_not_found! unless permission_queryer.can_read_globally? unauthorized! unless permission_queryer.can_write_globally? @@ -112,7 +112,7 @@ def destroy else broker_not_found! unless permission_queryer.can_read_from_space?(service_broker.space.id, service_broker.space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(service_broker.space.id) - suspended! unless permission_queryer.is_space_active?(service_broker.space.id) + require_writable_space!(service_broker.space) end broker_has_instances!(service_broker.name) if service_broker.has_service_instances? diff --git a/app/controllers/v3/service_credential_bindings_controller.rb b/app/controllers/v3/service_credential_bindings_controller.rb index 8549251f7c0..da722450092 100644 --- a/app/controllers/v3/service_credential_bindings_controller.rb +++ b/app/controllers/v3/service_credential_bindings_controller.rb @@ -60,12 +60,12 @@ def create when 'app' app = get_app!(message.app_guid) unauthorized! unless can_bind_in_active_space?(app.space) - suspended! unless is_space_active?(app.space) + require_writable_space!(app.space) create_app_binding(message, service_instance, app) when 'key' unauthorized! unless can_write_to_active_space?(service_instance.space) - suspended! unless is_space_active?(service_instance.space) + require_writable_space!(service_instance.space) create_key_binding(message, service_instance) end rescue V3::ServiceCredentialBindingAppCreate::UnprocessableCreate, @@ -76,7 +76,7 @@ def create def update not_found! if service_credential_binding.blank? unauthorized! unless can_write_to_active_space?(binding_space) - suspended! unless is_space_active?(binding_space) + require_writable_space!(binding_space) unprocessable!('The service binding is being deleted') if delete_in_progress?(service_credential_binding) @@ -107,7 +107,7 @@ def destroy else unauthorized! unless can_bind_in_active_space?(binding_space) end - suspended! unless is_space_active?(binding_space) + require_writable_space!(binding_space) type = service_credential_binding.is_a?(ServiceKey) ? :key : :credential @@ -363,10 +363,6 @@ def can_read_from_space?(space) permission_queryer.can_read_from_space?(space.id, space.organization_id) end - def is_space_active?(space) - permission_queryer.is_space_active?(space.id) - end - def binding_space service_credential_binding.space end diff --git a/app/controllers/v3/service_instances_controller.rb b/app/controllers/v3/service_instances_controller.rb index 84a24b94858..e5663120484 100644 --- a/app/controllers/v3/service_instances_controller.rb +++ b/app/controllers/v3/service_instances_controller.rb @@ -83,7 +83,7 @@ def create space = Space.first(guid: message.space_guid) unprocessable_space! unless space && can_read_from_space?(space) unauthorized! unless can_write_to_active_space?(space) - suspended! unless is_space_active?(space) + require_writable_space!(space) case message.type when 'user-provided' @@ -133,7 +133,7 @@ def share_service_instance service_instance = ServiceInstance.first(guid: hashed_params[:guid]) resource_not_found!(:service_instance) unless service_instance && can_read_service_instance?(service_instance) unauthorized! unless can_write_to_active_space?(service_instance.space) - suspended! unless is_space_active?(service_instance.space) + require_writable_space!(service_instance.space) message = VCAP::CloudController::ToManyRelationshipMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -156,7 +156,7 @@ def unshare_service_instance resource_not_found!(:service_instance) unless service_instance && can_read_service_instance?(service_instance) unauthorized! unless can_write_to_active_space?(service_instance.space) - suspended! unless is_space_active?(service_instance.space) + require_writable_space!(service_instance.space) space_guid = hashed_params[:space_guid] target_space = Space.first(guid: space_guid) @@ -386,7 +386,7 @@ def fetch_writable_service_instance(guid) service_instance_not_found! unless service_instance && can_read_service_instance?(service_instance) unauthorized! unless can_write_to_active_space?(service_instance.space) - suspended! unless is_space_active?(service_instance.space) + require_writable_space!(service_instance.space) service_instance end @@ -443,7 +443,7 @@ def can_write_to_active_space?(space) end def is_space_active?(space) - permission_queryer.is_space_active?(space.id) + permission_queryer.writable_space_state(space.id) == :active end def admin? diff --git a/app/controllers/v3/service_route_bindings_controller.rb b/app/controllers/v3/service_route_bindings_controller.rb index 8247099ba19..7772be60635 100644 --- a/app/controllers/v3/service_route_bindings_controller.rb +++ b/app/controllers/v3/service_route_bindings_controller.rb @@ -67,7 +67,7 @@ def create def update route_binding_not_found! unless @route_binding.present? && can_read_from_space?(@route_binding.route.space) unauthorized! unless can_write_to_active_space?(@route_binding.route.space) - suspended! unless is_space_active?(@route_binding.route.space) + require_writable_space!(@route_binding.route.space) unprocessable!('The service route binding is being deleted') if delete_in_progress?(@route_binding) @@ -90,7 +90,7 @@ def update def destroy route_binding_not_found! unless @route_binding && can_read_from_space?(@route_binding.route.space) unauthorized! unless can_bind_in_active_space?(@route_binding.route.space) - suspended! unless is_space_active?(@route_binding.route.space) + require_writable_space!(@route_binding.route.space) action = V3::ServiceRouteBindingDelete.new(user_audit_info) binding_operation_in_progress! if action.blocking_operation_in_progress?(@route_binding) @@ -113,7 +113,7 @@ def parameters not_found_with_message!(@route_binding) unless @route_binding.create_succeeded? unauthorized! unless can_write_to_active_space?(@route_binding.route.space) - suspended! unless is_space_active?(@route_binding.route.space) + require_writable_space!(@route_binding.route.space) fetcher = ServiceBindingRead.new parameters = fetcher.fetch_parameters(@route_binding) @@ -201,7 +201,7 @@ def fetch_service_instance(guid) service_instance_not_found!(guid) unless service_instance && can_read_from_space?(service_instance.space) unauthorized! unless can_bind_in_active_space?(service_instance.space) - suspended! unless is_space_active?(service_instance.space) + require_writable_space!(service_instance.space) service_instance end @@ -234,10 +234,6 @@ def can_write_to_active_space?(space) permission_queryer.can_write_to_active_space?(space.id) end - def is_space_active?(space) - permission_queryer.is_space_active?(space.id) - end - def route_services_enabled? VCAP::CloudController::Config.config.get(:route_services_enabled) end diff --git a/app/controllers/v3/sidecars_controller.rb b/app/controllers/v3/sidecars_controller.rb index 8eb0c14df52..7e82ade6c12 100644 --- a/app/controllers/v3/sidecars_controller.rb +++ b/app/controllers/v3/sidecars_controller.rb @@ -53,7 +53,7 @@ def create app, space = AppFetcher.new.fetch(hashed_params[:guid]) resource_not_found!(:app) unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) message = SidecarCreateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -84,7 +84,7 @@ def update space = sidecar.app.space resource_not_found!(:sidecar) unless permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) message = SidecarUpdateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -102,7 +102,7 @@ def destroy space = sidecar.app.space resource_not_found!(:sidecar) unless permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) SidecarDelete.delete(sidecar) head :no_content diff --git a/app/controllers/v3/space_features_controller.rb b/app/controllers/v3/space_features_controller.rb index 7ba699f4aba..09c5fd1d048 100644 --- a/app/controllers/v3/space_features_controller.rb +++ b/app/controllers/v3/space_features_controller.rb @@ -29,7 +29,7 @@ def update resource_not_found!(:space) unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) resource_not_found!(:feature) unless hashed_params[:name] == SPACE_FEATURE unauthorized! unless permission_queryer.can_update_active_space?(space.id, space.organization_id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) space.update(allow_ssh: message.enabled) diff --git a/app/controllers/v3/space_manifests_controller.rb b/app/controllers/v3/space_manifests_controller.rb index 9e8f8cd91ae..8ef864021ba 100644 --- a/app/controllers/v3/space_manifests_controller.rb +++ b/app/controllers/v3/space_manifests_controller.rb @@ -63,7 +63,7 @@ def diff_manifest def can_write_space(space) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) end def errors_for_message(message, index) diff --git a/app/controllers/v3/space_quotas_controller.rb b/app/controllers/v3/space_quotas_controller.rb index fcd48c5d2be..e0690ec6813 100644 --- a/app/controllers/v3/space_quotas_controller.rb +++ b/app/controllers/v3/space_quotas_controller.rb @@ -44,7 +44,7 @@ def create unprocessable_organization!(message.organization_guid) unless org unauthorized! unless permission_queryer.can_write_to_active_org?(org.id) - suspended! unless permission_queryer.is_org_active?(org.id) + require_writable_org!(org) space_quota = SpaceQuotasCreate.new(user_audit_info).create(message, organization: org) @@ -64,7 +64,7 @@ def update unauthorized! unless permission_queryer.can_write_globally? || (space_quota && permission_queryer.can_write_to_active_org?(space_quota.organization_id)) - suspended! unless space_quota && permission_queryer.is_org_active?(space_quota.organization_id) + require_writable_org!(space_quota.organization) if space_quota message = VCAP::CloudController::OrganizationQuotasUpdateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -87,7 +87,7 @@ def apply_to_spaces unauthorized! unless permission_queryer.can_write_globally? || (space_quota && permission_queryer.can_write_to_active_org?(space_quota.organization_id)) - suspended! unless space_quota && permission_queryer.is_org_active?(space_quota.organization_id) + require_writable_org!(space_quota.organization) if space_quota message = SpaceQuotaApplyMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? @@ -112,7 +112,7 @@ def remove_from_space unauthorized! unless permission_queryer.can_write_globally? || (space_quota && permission_queryer.can_write_to_active_org?(space_quota.organization_id)) - suspended! unless space_quota && permission_queryer.is_org_active?(space_quota.organization_id) + require_writable_org!(space_quota.organization) if space_quota space_guid = hashed_params[:space_guid] space = Space.first(guid: space_guid) @@ -134,7 +134,7 @@ def destroy unauthorized! unless permission_queryer.can_write_globally? || (space_quota && permission_queryer.can_write_to_active_org?(space_quota.organization_id)) - suspended! unless space_quota && permission_queryer.is_org_active?(space_quota.organization_id) + require_writable_org!(space_quota.organization) if space_quota unprocessable!('This quota is applied to one or more spaces. Remove this quota from all spaces before deleting.') unless space_quota.spaces_dataset.empty? diff --git a/app/controllers/v3/spaces_controller.rb b/app/controllers/v3/spaces_controller.rb index deb534ef2cf..8e814fc06b1 100644 --- a/app/controllers/v3/spaces_controller.rb +++ b/app/controllers/v3/spaces_controller.rb @@ -57,7 +57,8 @@ def create org = fetch_organization(message.organization_guid) unprocessable!(missing_org) unless org && permission_queryer.can_read_from_org?(org.id) unauthorized! unless permission_queryer.can_write_to_active_org?(org.id) - suspended! unless permission_queryer.is_org_active?(org.id) + require_writable_org!(org) + unauthorized! if suspended_by_unauthorized?(message, org) space = SpaceCreate.new(user_audit_info:).create(org, message) @@ -70,11 +71,13 @@ def update space = fetch_space(hashed_params[:guid]) space_not_found! unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_update_active_space?(space.id, space.organization_id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) message = VCAP::CloudController::SpaceUpdateMessage.new(hashed_params[:body]) unprocessable!(message.errors.full_messages) unless message.valid? + unauthorized! if suspended_by_unauthorized?(message, space.organization) + space = SpaceUpdate.new(user_audit_info).update(space, message) render status: :ok, json: Presenters::V3::SpacePresenter.new(space) @@ -86,7 +89,7 @@ def destroy space = fetch_space(hashed_params[:guid]) space_not_found! unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_org?(space.organization_id) - suspended! unless permission_queryer.is_org_active?(space.organization_id) + require_writable_org!(space.organization) service_event_repository = VCAP::CloudController::Repositories::ServiceEventRepository.new(user_audit_info) delete_action = SpaceDelete.new(user_audit_info, service_event_repository) @@ -142,7 +145,7 @@ def delete_unmapped_routes space_not_found! unless space && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) deletion_job = VCAP::CloudController::Jobs::V3::SpaceDeleteUnmappedRoutesJob.new(space) pollable_job = Jobs::Enqueuer.new(queue: Jobs::Queues.generic).enqueue_pollable(deletion_job) @@ -222,6 +225,12 @@ def show_usage_summary private + def suspended_by_unauthorized?(message, org) + return false unless message.requested?(:suspended) + + !permission_queryer.can_write_globally? && !permission_queryer.is_org_manager_for_org?(org.id) + end + def fetch_organization(guid) Organization.where(guid:).first end diff --git a/app/controllers/v3/tasks_controller.rb b/app/controllers/v3/tasks_controller.rb index a03bdbd8a51..c2d80c0499b 100644 --- a/app/controllers/v3/tasks_controller.rb +++ b/app/controllers/v3/tasks_controller.rb @@ -59,7 +59,7 @@ def create app_not_found! unless app && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) droplet_not_found! if message.requested?(:droplet_guid) && droplet.nil? task = TaskCreate.new(configuration).create(app, message, user_audit_info, droplet:) @@ -80,7 +80,7 @@ def cancel task_not_found! unless task && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_manage_apps_in_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) TaskCancel.new(configuration).cancel(task:, user_audit_info:) render status: :accepted, json: Presenters::V3::TaskPresenter.new(task.reload) @@ -95,7 +95,7 @@ def update task, space = TaskFetcher.new.fetch(task_guid: hashed_params[:task_guid]) task_not_found! unless task && permission_queryer.can_read_from_space?(space.id, space.organization_id) unauthorized! unless permission_queryer.can_write_to_active_space?(space.id) - suspended! unless permission_queryer.is_space_active?(space.id) + require_writable_space!(space) task = TaskUpdate.new.update(task, message) render status: :ok, json: Presenters::V3::TaskPresenter.new(task) diff --git a/app/messages/space_create_message.rb b/app/messages/space_create_message.rb index e7a2042a24c..267ca9373dd 100644 --- a/app/messages/space_create_message.rb +++ b/app/messages/space_create_message.rb @@ -2,7 +2,7 @@ module VCAP::CloudController class SpaceCreateMessage < MetadataBaseMessage - register_allowed_keys %i[name relationships] + register_allowed_keys %i[name relationships suspended] validates_with NoAdditionalKeysValidator, RelationshipValidator @@ -14,6 +14,10 @@ class SpaceCreateMessage < MetadataBaseMessage format: { with: ->(_) { Space::SPACE_NAME_REGEX }, message: 'must not contain escaped characters' }, allow_nil: true + validates :suspended, + boolean: true, + allow_nil: true + delegate :organization_guid, to: :relationships_message def relationships_message diff --git a/app/messages/space_update_message.rb b/app/messages/space_update_message.rb index a882869bd70..634030e4fac 100644 --- a/app/messages/space_update_message.rb +++ b/app/messages/space_update_message.rb @@ -2,7 +2,7 @@ module VCAP::CloudController class SpaceUpdateMessage < MetadataBaseMessage - register_allowed_keys [:name] + register_allowed_keys %i[name suspended] validates_with NoAdditionalKeysValidator @@ -11,5 +11,9 @@ class SpaceUpdateMessage < MetadataBaseMessage length: { minimum: 1, maximum: 255 }, format: { with: ->(_) { Space::SPACE_NAME_REGEX }, message: 'must not contain escaped characters' }, allow_nil: true + + validates :suspended, + boolean: true, + allow_nil: true end end diff --git a/app/models/helpers/org_space_status.rb b/app/models/helpers/org_space_status.rb new file mode 100644 index 00000000000..94514bde58a --- /dev/null +++ b/app/models/helpers/org_space_status.rb @@ -0,0 +1,25 @@ +module VCAP::CloudController + module OrgSpaceStatus + ACTIVE = 'active'.freeze + SUSPENDED = 'suspended'.freeze + DELETING = 'deleting'.freeze + + VALID_STATUSES = [ACTIVE, SUSPENDED, DELETING].freeze + + def active? + status == ACTIVE + end + + def suspended? + status == SUSPENDED + end + + def deleting? + status == DELETING + end + + def suspended_or_deleting? + suspended? || deleting? + end + end +end diff --git a/app/models/runtime/domain.rb b/app/models/runtime/domain.rb index 4ca18ef9b6f..36bb434233b 100644 --- a/app/models/runtime/domain.rb +++ b/app/models/runtime/domain.rb @@ -149,8 +149,8 @@ def owned_by?(org) owning_organization_id == org.id end - def in_suspended_org? - return owning_organization.suspended? if owning_organization + def in_suspended_or_deleting_org? + return owning_organization.suspended_or_deleting? if owning_organization false end diff --git a/app/models/runtime/organization.rb b/app/models/runtime/organization.rb index c5701b8c633..88111564116 100644 --- a/app/models/runtime/organization.rb +++ b/app/models/runtime/organization.rb @@ -1,11 +1,12 @@ require 'models/helpers/process_types' +require 'models/helpers/org_space_status' module VCAP::CloudController class Organization < Sequel::Model + include OrgSpaceStatus + ORG_NAME_REGEX = /\A[[:alnum:][:punct:][:print:]]+\Z/ - ACTIVE = 'active'.freeze - SUSPENDED = 'suspended'.freeze - ORG_STATUS_VALUES = [ACTIVE, SUSPENDED].freeze + ORG_STATUS_VALUES = VALID_STATUSES one_to_many :spaces @@ -254,14 +255,6 @@ def meets_max_task_limit? app_task_limit <= running_and_pending_tasks_count end - def active? - status == ACTIVE - end - - def suspended? - status == SUSPENDED - end - def billing_enabled? billing_enabled end diff --git a/app/models/runtime/private_domain.rb b/app/models/runtime/private_domain.rb index b1c0ffd47f0..75d1504de55 100644 --- a/app/models/runtime/private_domain.rb +++ b/app/models/runtime/private_domain.rb @@ -48,10 +48,6 @@ def validate validate_total_private_domains end - def in_suspended_org? - owning_organization.suspended? - end - def addable_to_organization!(org) return if owned_by?(org) diff --git a/app/models/runtime/process_model.rb b/app/models/runtime/process_model.rb index b4cc61628ab..98924718eee 100644 --- a/app/models/runtime/process_model.rb +++ b/app/models/runtime/process_model.rb @@ -345,7 +345,9 @@ def needs_package_in_current_state? started? end - delegate :in_suspended_org?, to: :space + def in_suspended_or_deleting_org? + space&.in_suspended_or_deleting_org? || false + end def being_started? column_changed?(:state) && started? diff --git a/app/models/runtime/route.rb b/app/models/runtime/route.rb index bdefff78c41..7acd3f8808c 100644 --- a/app/models/runtime/route.rb +++ b/app/models/runtime/route.rb @@ -202,7 +202,7 @@ def available_in_space?(other_space) other_space == space || shared_spaces.include?(other_space) end - delegate :in_suspended_org?, to: :space + delegate :in_suspended_or_deleting_org?, to: :space def tcp? domain.shared? && domain.tcp? && port.present? && port > 0 diff --git a/app/models/runtime/space.rb b/app/models/runtime/space.rb index 507260fb856..577f2da14ab 100644 --- a/app/models/runtime/space.rb +++ b/app/models/runtime/space.rb @@ -1,8 +1,11 @@ require 'models/helpers/process_types' +require 'models/helpers/org_space_status' require 'cloud_controller/errors/invalid_relation' module VCAP::CloudController class Space < Sequel::Model + include OrgSpaceStatus + class InvalidDeveloperRelation < CloudController::Errors::InvalidRelation; end class InvalidAuditorRelation < CloudController::Errors::InvalidRelation; end class InvalidSupporterRelation < CloudController::Errors::InvalidRelation; end @@ -13,6 +16,7 @@ class UnauthorizedAccessToPrivateDomain < RuntimeError; end class DBNameUniqueRaceError < Sequel::ValidationFailed; end SPACE_NAME_REGEX = /\A[[:alnum:][:punct:][:print:]]+\Z/ + SPACE_STATUS_VALUES = VALID_STATUSES SELECT_NEWEST_PROCESS = lambda { |_, processes| newest_processes = {} processes.group_by(&:app_guid).each_value do |processes_for_app| @@ -230,6 +234,7 @@ def validate validates_presence :name validates_presence :organization validates_format SPACE_NAME_REGEX, :name + validates_includes SPACE_STATUS_VALUES, :status, allow_missing: true errors.add(:space_quota_definition, :invalid_organization) if space_quota_definition && space_quota_definition.organization_id != organization.id @@ -324,8 +329,8 @@ def meets_max_task_limit? app_task_limit <= running_and_pending_tasks_count end - def in_suspended_org? - organization.suspended? + def in_suspended_or_deleting_org? + organization.suspended_or_deleting? end def members diff --git a/app/models/services/service_binding.rb b/app/models/services/service_binding.rb index 06fe5f8a689..21eb482fef4 100644 --- a/app/models/services/service_binding.rb +++ b/app/models/services/service_binding.rb @@ -75,7 +75,7 @@ def to_hash(_opts={}) { guid: } end - delegate :in_suspended_org?, to: :space + delegate :in_suspended_or_deleting_org?, to: :space delegate :space, to: :app diff --git a/app/models/services/service_instance.rb b/app/models/services/service_instance.rb index a2241c7d2e2..a1bc938f735 100644 --- a/app/models/services/service_instance.rb +++ b/app/models/services/service_instance.rb @@ -175,8 +175,8 @@ def credentials_with_serialization alias_method 'credentials_without_serialization', 'credentials' alias_method 'credentials', 'credentials_with_serialization' - def in_suspended_org? - space&.in_suspended_org? + def in_suspended_or_deleting_org? + space&.in_suspended_or_deleting_org? end def after_create diff --git a/app/models/services/service_key.rb b/app/models/services/service_key.rb index ae3e02f699a..9ac89ce4fa0 100644 --- a/app/models/services/service_key.rb +++ b/app/models/services/service_key.rb @@ -33,7 +33,7 @@ def credhub_reference credentials.present? ? credentials['credhub-ref'] : nil end - delegate :in_suspended_org?, to: :space + delegate :in_suspended_or_deleting_org?, to: :space delegate :space, to: :service_instance diff --git a/app/presenters/v3/space_presenter.rb b/app/presenters/v3/space_presenter.rb index 1b68461b6e7..8bb64b9c67b 100644 --- a/app/presenters/v3/space_presenter.rb +++ b/app/presenters/v3/space_presenter.rb @@ -18,6 +18,7 @@ def to_hash created_at: space.created_at, updated_at: space.updated_at, name: space.name, + suspended: space.suspended?, relationships: { organization: { data: { diff --git a/db/migrations/20260522120000_add_status_to_spaces.rb b/db/migrations/20260522120000_add_status_to_spaces.rb new file mode 100644 index 00000000000..e76c38c9690 --- /dev/null +++ b/db/migrations/20260522120000_add_status_to_spaces.rb @@ -0,0 +1,13 @@ +Sequel.migration do + up do + alter_table :spaces do + add_column :status, String, null: false, default: 'active', size: 255 unless @db.schema(:spaces).map(&:first).include?(:status) + end + end + + down do + alter_table :spaces do + drop_column :status if @db.schema(:spaces).map(&:first).include?(:status) + end + end +end diff --git a/docs/v3/source/includes/api_resources/_spaces.erb b/docs/v3/source/includes/api_resources/_spaces.erb index e9a24461bf2..e6548a05185 100644 --- a/docs/v3/source/includes/api_resources/_spaces.erb +++ b/docs/v3/source/includes/api_resources/_spaces.erb @@ -4,6 +4,7 @@ "created_at": "2017-02-01T01:33:58Z", "updated_at": "2017-02-01T01:33:58Z", "name": "my-space", + "suspended": false, "relationships": { "organization": { "data": { @@ -72,6 +73,7 @@ "created_at": "2017-02-01T01:33:58Z", "updated_at": "2017-02-01T01:33:58Z", "name": "space1", + "suspended": false, "relationships": { "organization": { "data": { @@ -107,6 +109,7 @@ "created_at": "2017-02-02T00:14:30Z", "updated_at": "2017-02-02T00:14:30Z", "name": "space2", + "suspended": false, "relationships": { "organization": { "data": { diff --git a/docs/v3/source/includes/resources/spaces/_create.md.erb b/docs/v3/source/includes/resources/spaces/_create.md.erb index 8640e76fc2c..b9c346be882 100644 --- a/docs/v3/source/includes/resources/spaces/_create.md.erb +++ b/docs/v3/source/includes/resources/spaces/_create.md.erb @@ -47,6 +47,7 @@ Name | Type | Description Name | Type | Description ---- | ---- | ----------- +**suspended** | _boolean_ | Whether a space is suspended or not **metadata.labels** | [_label object_](#labels) | Labels applied to the space **metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the space diff --git a/docs/v3/source/includes/resources/spaces/_object.md.erb b/docs/v3/source/includes/resources/spaces/_object.md.erb index 5ed252acfe9..9583daff0ef 100644 --- a/docs/v3/source/includes/resources/spaces/_object.md.erb +++ b/docs/v3/source/includes/resources/spaces/_object.md.erb @@ -13,6 +13,7 @@ Name | Type | Description **created_at** | _[timestamp](#timestamps)_ | The time with zone when the object was created **updated_at** | _[timestamp](#timestamps)_ | The time with zone when the object was last updated **name** | _string_ | Name of the space +**suspended** | _boolean_ | Whether a space is suspended or not; non-admins and non-org-managers will be blocked from creating, updating, or deleting resources in a suspended space **relationships.organization** | [_to-one relationship_](#to-one-relationships) | The organization the space is contained in **relationships.quota** | [_to-one relationship_](#to-one-relationships) | The space quota applied to the space **metadata.labels** | [_label object_](#labels) | Labels applied to the space diff --git a/docs/v3/source/includes/resources/spaces/_update.md.erb b/docs/v3/source/includes/resources/spaces/_update.md.erb index b53131395e4..c171b06689c 100644 --- a/docs/v3/source/includes/resources/spaces/_update.md.erb +++ b/docs/v3/source/includes/resources/spaces/_update.md.erb @@ -32,12 +32,14 @@ Content-Type: application/json Name | Type | Description ---- | ---- | ----------- **name** | _string_ | New space name | +**suspended** | _boolean_ | Whether a space is suspended or not **metadata.labels** | [_label object_](#labels) | Labels applied to the space **metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the space #### Permitted roles - | ---- | --- + +Role | Notes +---- | ----- Admin | Org Manager | -Space Manager | +Space Manager | Cannot change the `suspended` field diff --git a/errors/v2.yml b/errors/v2.yml index 1f0d12418cb..710809685fd 100644 --- a/errors/v2.yml +++ b/errors/v2.yml @@ -118,6 +118,16 @@ http_code: 429 message: "Rate Limit of V2 API Exceeded. Please consider using the V3 API" +10019: + name: SpaceSuspended + http_code: 403 + message: "The space is suspended" + +10020: + name: ResourceBeingDeleted + http_code: 422 + message: "The %s is being deleted" + 20001: name: UserInvalid http_code: 400 diff --git a/lib/cloud_controller/permissions.rb b/lib/cloud_controller/permissions.rb index eb4ec13116e..441d20c6bc4 100644 --- a/lib/cloud_controller/permissions.rb +++ b/lib/cloud_controller/permissions.rb @@ -101,6 +101,10 @@ def is_org_manager? membership.authorized_orgs_subquery(VCAP::CloudController::Membership::ORG_MANAGER).any? end + def is_org_manager_for_org?(org_id) + membership.role_applies?(VCAP::CloudController::Membership::ORG_MANAGER, nil, org_id) + end + def readable_org_guids readable_org_guids_query.select_map(:guid) end @@ -149,21 +153,50 @@ def can_write_to_active_org?(org_id) membership.role_applies?(ROLES_FOR_ORG_WRITING, nil, org_id) end - def is_org_active?(org_id) - return true if can_write_globally? # admins can modify suspended orgs + def org_state(org_id) + status = VCAP::CloudController::Organization.where(id: org_id).get(:status) + return :active if status.nil? - !VCAP::CloudController::Organization. - where(id: org_id, status: VCAP::CloudController::Organization::ACTIVE). - empty? + status.to_sym end - def is_space_active?(space_id) - return true if can_write_globally? # admins can modify suspended orgs + def writable_org_state(org_id) + return :active if can_write_globally? # admins can modify suspended/deleting orgs + + org_state(org_id) + end + + def space_state(space_id) + space = VCAP::CloudController::Space.where(id: space_id).select(:status, :organization_id).first + return :active if space.nil? + + org = org_state(space.organization_id) + return :deleting if org == :deleting + return :deleting if space.status == VCAP::CloudController::Space::DELETING + return :suspended if org == :suspended + return :suspended if space.status == VCAP::CloudController::Space::SUSPENDED + + :active + end + + def writable_space_state(space_id) + return :active if can_write_globally? # admins can modify suspended/deleting spaces + + space = VCAP::CloudController::Space.where(id: space_id).select(:status, :organization_id).first + return :active if space.nil? + + # Org state is never bypassed by org managers — a suspended/deleting org locks out everyone except admins + org = org_state(space.organization_id) + return :deleting if org == :deleting + return :suspended if org == :suspended + + # Org managers can manage spaces in their active orgs even if the space itself is suspended/deleting + return :active if membership.role_applies?(VCAP::CloudController::Membership::ORG_MANAGER, nil, space.organization_id) + + return :deleting if space.status == VCAP::CloudController::Space::DELETING + return :suspended if space.status == VCAP::CloudController::Space::SUSPENDED - !VCAP::CloudController::Organization. - join(:spaces, organization_id: :id). - where(spaces__id: space_id, organizations__status: VCAP::CloudController::Organization::ACTIVE). - empty? + :active end def readable_space_guids diff --git a/spec/migrations/20260522120000_add_status_to_spaces_spec.rb b/spec/migrations/20260522120000_add_status_to_spaces_spec.rb new file mode 100644 index 00000000000..445180e8751 --- /dev/null +++ b/spec/migrations/20260522120000_add_status_to_spaces_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' +require 'migrations/helpers/migration_shared_context' + +RSpec.describe 'migration to add status column to spaces table', isolation: :truncation, type: :migration do + include_context 'migration' do + let(:migration_filename) { '20260522120000_add_status_to_spaces.rb' } + end + + describe 'spaces table' do + it 'adds the status column with the expected properties and is idempotent' do + quota_id = db[:quota_definitions].insert( + guid: 'quota-guid', + name: 'test-quota', + non_basic_services_allowed: true, + total_services: 10, + total_routes: 10, + memory_limit: 1024 + ) + org_id = db[:organizations].insert(guid: 'org-guid', name: 'an-org', quota_definition_id: quota_id) + db[:spaces].insert(guid: 'existing-space-guid', name: 'existing-space', organization_id: org_id) + + expect(db[:spaces].columns).not_to include(:status) + + Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) + + expect(db[:spaces].columns).to include(:status) + + expect(db[:spaces].first(guid: 'existing-space-guid')[:status]).to eq('active') + + db[:spaces].insert(guid: 'new-space-guid', name: 'new-space', organization_id: org_id) + expect(db[:spaces].first(guid: 'new-space-guid')[:status]).to eq('active') + + expect do + db[:spaces].insert(guid: 'null-status-guid', name: 'null-space', organization_id: org_id, status: nil) + end.to raise_error(Sequel::NotNullConstraintViolation) + + expect do + Sequel::Migrator.run(db, migrations_path, target: current_migration_index, allow_missing_migration_files: true) + end.not_to raise_error + + Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) + expect(db[:spaces].columns).not_to include(:status) + + expect do + Sequel::Migrator.run(db, migrations_path, target: current_migration_index - 1, allow_missing_migration_files: true) + end.not_to raise_error + end + end +end diff --git a/spec/request/apps_spec.rb b/spec/request/apps_spec.rb index 7e90a7d013c..fd05658d2af 100644 --- a/spec/request/apps_spec.rb +++ b/spec/request/apps_spec.rb @@ -126,6 +126,48 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context 'when space is suspended' do + let(:expected_codes_and_responses) do + h = super() + h['space_developer'] = { code: 403, errors: CF_SPACE_SUSPENDED } + h + end + + before do + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when organization is being deleted' do + let(:expected_codes_and_responses) do + h = super() + h['space_developer'] = { code: 422, errors: CF_ORGANIZATION_BEING_DELETED } + h + end + + before do + org.update(status: VCAP::CloudController::Organization::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when space is being deleted' do + let(:expected_codes_and_responses) do + h = super() + h['space_developer'] = { code: 422, errors: CF_SPACE_BEING_DELETED } + h + end + + before do + space.update(status: VCAP::CloudController::Space::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end context 'when the user can create an app' do @@ -746,6 +788,7 @@ 'created_at' => iso8601, 'updated_at' => iso8601, 'name' => space.name, + 'suspended' => false, 'relationships' => { 'organization' => { 'data' => { @@ -1545,6 +1588,7 @@ 'created_at' => iso8601, 'updated_at' => iso8601, 'name' => space.name, + 'suspended' => false, 'relationships' => { 'organization' => { 'data' => { diff --git a/spec/request/builds_spec.rb b/spec/request/builds_spec.rb index 89b3fa2bc70..c1c27e13ff0 100644 --- a/spec/request/builds_spec.rb +++ b/spec/request/builds_spec.rb @@ -158,7 +158,7 @@ context 'when organization is suspended' do let(:expected_codes_and_responses) do h = super() - %w[space_developer space_supporter].each { |r| h[r] = { code: 422 } } + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_ORG_SUSPENDED } } h end diff --git a/spec/request/deployments_spec.rb b/spec/request/deployments_spec.rb index 88db57c603f..e1514a8f2ce 100644 --- a/spec/request/deployments_spec.rb +++ b/spec/request/deployments_spec.rb @@ -97,7 +97,7 @@ context 'when organization is suspended' do let(:expected_codes_and_responses) do h = super() - %w[space_developer space_supporter].each { |r| h[r] = { code: 422 } } + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_ORG_SUSPENDED } } h end @@ -107,6 +107,48 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context 'when space is suspended' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_SPACE_SUSPENDED } } + h + end + + before do + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when organization is being deleted' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 422, errors: CF_ORGANIZATION_BEING_DELETED } } + h + end + + before do + org.update(status: VCAP::CloudController::Organization::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when space is being deleted' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 422, errors: CF_SPACE_BEING_DELETED } } + h + end + + before do + space.update(status: VCAP::CloudController::Space::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end context 'when a droplet is supplied with the request' do @@ -1579,6 +1621,48 @@ it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context 'when space is suspended' do + let(:expected_codes_and_responses) do + h = super() + h['space_developer'] = { code: 403, errors: CF_SPACE_SUSPENDED } + h + end + + before do + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when organization is being deleted' do + let(:expected_codes_and_responses) do + h = super() + h['space_developer'] = { code: 422, errors: CF_ORGANIZATION_BEING_DELETED } + h + end + + before do + org.update(status: VCAP::CloudController::Organization::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when space is being deleted' do + let(:expected_codes_and_responses) do + h = super() + h['space_developer'] = { code: 422, errors: CF_SPACE_BEING_DELETED } + h + end + + before do + space.update(status: VCAP::CloudController::Space::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end end @@ -2189,7 +2273,7 @@ def json_for_options(deployment) context 'when organization is suspended' do let(:expected_codes_and_responses) do h = super() - %w[space_developer space_supporter].each { |r| h[r] = { code: 404 } } + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_ORG_SUSPENDED } } h end @@ -2199,6 +2283,48 @@ def json_for_options(deployment) it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context 'when space is suspended' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_SPACE_SUSPENDED } } + h + end + + before do + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when organization is being deleted' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 422, errors: CF_ORGANIZATION_BEING_DELETED } } + h + end + + before do + org.update(status: VCAP::CloudController::Organization::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when space is being deleted' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 422, errors: CF_SPACE_BEING_DELETED } } + h + end + + before do + space.update(status: VCAP::CloudController::Space::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end context 'when the deployment is running and has a previous droplet' do @@ -2313,7 +2439,7 @@ def json_for_options(deployment) context 'when organization is suspended' do let(:expected_codes_and_responses) do h = super() - %w[space_developer space_supporter].each { |r| h[r] = { code: 404 } } + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_ORG_SUSPENDED } } h end @@ -2323,6 +2449,48 @@ def json_for_options(deployment) it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end + + context 'when space is suspended' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 403, errors: CF_SPACE_SUSPENDED } } + h + end + + before do + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when organization is being deleted' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 422, errors: CF_ORGANIZATION_BEING_DELETED } } + h + end + + before do + org.update(status: VCAP::CloudController::Organization::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'when space is being deleted' do + let(:expected_codes_and_responses) do + h = super() + %w[space_developer space_supporter].each { |r| h[r] = { code: 422, errors: CF_SPACE_BEING_DELETED } } + h + end + + before do + space.update(status: VCAP::CloudController::Space::DELETING) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end end end end diff --git a/spec/request/organizations_spec.rb b/spec/request/organizations_spec.rb index 22b2f35624e..816f0ff3310 100644 --- a/spec/request/organizations_spec.rb +++ b/spec/request/organizations_spec.rb @@ -1393,6 +1393,22 @@ module VCAP::CloudController it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS end end + + context 'when the organization is being deleted' do + before do + set_current_user(user, { admin: true }) + allow_user_read_access_for(user, orgs: [organization1]) + organization1.update(status: VCAP::CloudController::Organization::DELETING) + end + + it 'rejects an admin attempt to set suspended:false' do + patch "/v3/organizations/#{organization1.guid}", { suspended: false }.to_json, + admin_headers_for(user).merge('CONTENT_TYPE' => 'application/json') + + expect(last_response.status).to eq(422) + expect(parsed_response['errors'][0]['detail']).to include("Organization '#{organization1.name}' is being deleted and cannot be reactivated.") + end + end end describe 'DELETE /v3/organizations/:guid' do diff --git a/spec/request/roles_spec.rb b/spec/request/roles_spec.rb index 421765a8052..c0ae297a447 100644 --- a/spec/request/roles_spec.rb +++ b/spec/request/roles_spec.rb @@ -1437,6 +1437,7 @@ def make_space_role_for_current_user(type) created_at: iso8601, updated_at: iso8601, name: space.name, + suspended: false, relationships: { organization: { data: { guid: org.guid } @@ -1513,6 +1514,7 @@ def make_space_role_for_current_user(type) created_at: iso8601, updated_at: iso8601, name: another_space.name, + suspended: false, relationships: { organization: { data: { guid: another_space.organization.guid } @@ -1777,6 +1779,7 @@ def make_space_role_for_current_user(type) created_at: iso8601, updated_at: iso8601, name: space.name, + suspended: false, relationships: { organization: { data: { guid: org.guid } diff --git a/spec/request/routes_spec.rb b/spec/request/routes_spec.rb index a8ad0dc1e2d..c1d3e0da9b9 100644 --- a/spec/request/routes_spec.rb +++ b/spec/request/routes_spec.rb @@ -3119,7 +3119,7 @@ h[r] = { code: 422, errors: [{ - detail: "Unable to unshare route '#{route.uri}' from space '#{space_to_unshare.guid}'. The target organization is suspended.", + detail: "Unable to unshare route '#{route.uri}' from space '#{space_to_unshare.guid}'. The target organization or space is suspended.", title: 'CF-UnprocessableEntity', code: 10_008 }] @@ -3347,7 +3347,7 @@ h[r] = { code: 422, errors: [{ - detail: "Unable to transfer owner of route '#{route.uri}' to space '#{suspended_space.guid}'. The target organization is suspended.", + detail: "Unable to transfer owner of route '#{route.uri}' to space '#{suspended_space.guid}'. The target organization or space is suspended.", title: 'CF-UnprocessableEntity', code: 10_008 }] diff --git a/spec/request/spaces_spec.rb b/spec/request/spaces_spec.rb index 589028b6cb1..433f365456c 100644 --- a/spec/request/spaces_spec.rb +++ b/spec/request/spaces_spec.rb @@ -51,6 +51,7 @@ 'created_at' => iso8601, 'updated_at' => iso8601, 'name' => 'space1', + 'suspended' => false, 'relationships' => { 'organization' => { 'data' => { 'guid' => created_space.organization_guid } @@ -123,6 +124,7 @@ { 'guid' => space1.guid, 'name' => 'Catan', + 'suspended' => false, 'created_at' => iso8601, 'updated_at' => iso8601, 'relationships' => { @@ -199,6 +201,7 @@ { 'guid' => space1.guid, 'name' => 'Catan', + 'suspended' => false, 'created_at' => iso8601, 'updated_at' => iso8601, 'relationships' => { @@ -305,6 +308,7 @@ { 'guid' => space1.guid, 'name' => 'Catan', + 'suspended' => false, 'created_at' => iso8601, 'updated_at' => iso8601, 'relationships' => { @@ -324,6 +328,7 @@ { 'guid' => space2.guid, 'name' => 'Ticket to Ride', + 'suspended' => false, 'created_at' => iso8601, 'updated_at' => iso8601, 'relationships' => { @@ -838,6 +843,7 @@ { guid: space.guid, name: 'codenames', + suspended: false, created_at: iso8601, updated_at: iso8601, relationships: { @@ -938,6 +944,7 @@ { 'guid' => space1.guid, 'name' => space1.name, + 'suspended' => false, 'created_at' => iso8601, 'updated_at' => iso8601, 'relationships' => { @@ -972,6 +979,7 @@ { 'guid' => space1.guid, 'name' => space1.name, + 'suspended' => false, 'created_at' => iso8601, 'updated_at' => iso8601, 'relationships' => { @@ -993,6 +1001,82 @@ ) end end + + context 'when the space is being deleted' do + before do + space1.update(status: VCAP::CloudController::Space::DELETING) + end + + it 'rejects an admin attempt to set suspended:false' do + patch "/v3/spaces/#{space1.guid}", { suspended: false }.to_json, admin_header + + expect(last_response.status).to eq(422) + expect(parsed_response['errors'][0]['detail']).to include("Space '#{space1.name}' is being deleted and cannot be reactivated.") + end + end + + context 'updating the suspended attribute' do + it 'allows an admin to suspend a space' do + patch "/v3/spaces/#{space1.guid}", { suspended: true }.to_json, admin_header + + expect(last_response.status).to eq(200) + expect(parsed_response['suspended']).to be(true) + expect(space1.reload.status).to eq(VCAP::CloudController::Space::SUSPENDED) + end + + it 'allows an admin to unsuspend a suspended space' do + space1.update(status: VCAP::CloudController::Space::SUSPENDED) + patch "/v3/spaces/#{space1.guid}", { suspended: false }.to_json, admin_header + + expect(last_response.status).to eq(200) + expect(parsed_response['suspended']).to be(false) + expect(space1.reload.status).to eq(VCAP::CloudController::Space::ACTIVE) + end + end + + context 'when a non-authorized role mutates the suspended field' do + let(:space) { VCAP::CloudController::Space.make } + let(:org) { space.organization } + + context 'on an active space with suspended: true' do + let(:api_call) do + ->(user_headers) { patch "/v3/spaces/#{space.guid}", { suspended: true }.to_json, user_headers.merge('CONTENT_TYPE' => 'application/json') } + end + let(:expected_codes_and_responses) do + h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze) + h['admin'] = { code: 200 } + h['org_manager'] = { code: 200 } + h['org_billing_manager'] = { code: 404 } + h['org_auditor'] = { code: 404 } + h['no_role'] = { code: 404 } + h + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + + context 'on a suspended space with suspended: false (un-suspend)' do + let(:api_call) do + ->(user_headers) { patch "/v3/spaces/#{space.guid}", { suspended: false }.to_json, user_headers.merge('CONTENT_TYPE' => 'application/json') } + end + let(:expected_codes_and_responses) do + h = Hash.new({ code: 403, errors: CF_NOT_AUTHORIZED }.freeze) + h['admin'] = { code: 200 } + h['org_manager'] = { code: 200 } + h['space_manager'] = { code: 403, errors: CF_SPACE_SUSPENDED } + h['org_billing_manager'] = { code: 404 } + h['org_auditor'] = { code: 404 } + h['no_role'] = { code: 404 } + h + end + + before do + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like 'permissions for single object endpoint', ALL_PERMISSIONS + end + end end describe 'DELETE /v3/spaces/:guid' do diff --git a/spec/request_spec_shared_examples.rb b/spec/request_spec_shared_examples.rb index ef9606182ba..eb321ce7dd6 100644 --- a/spec/request_spec_shared_examples.rb +++ b/spec/request_spec_shared_examples.rb @@ -29,6 +29,24 @@ code: 10_017 } ].freeze +CF_SPACE_SUSPENDED = [ + { detail: 'The space is suspended', + title: 'CF-SpaceSuspended', + code: 10_019 } +].freeze + +CF_ORGANIZATION_BEING_DELETED = [ + { detail: 'The organization is being deleted', + title: 'CF-ResourceBeingDeleted', + code: 10_020 } +].freeze + +CF_SPACE_BEING_DELETED = [ + { detail: 'The space is being deleted', + title: 'CF-ResourceBeingDeleted', + code: 10_020 } +].freeze + RSpec.shared_examples 'paginated response' do |endpoint| it 'returns pagination information' do expect_filtered_resources(endpoint, 'per_page=1', resources[0, 1]) diff --git a/spec/unit/access/private_domain_access_spec.rb b/spec/unit/access/private_domain_access_spec.rb index d63133eed38..137348cb5f9 100644 --- a/spec/unit/access/private_domain_access_spec.rb +++ b/spec/unit/access/private_domain_access_spec.rb @@ -27,7 +27,13 @@ module VCAP::CloudController it_behaves_like 'full access' context 'when the organization is suspended' do - before { allow(object).to receive(:in_suspended_org?).and_return(true) } + before { allow(object.owning_organization).to receive(:suspended?).and_return(true) } + + it_behaves_like 'read only access' + end + + context 'when the organization is deleting' do + before { allow(object.owning_organization).to receive(:deleting?).and_return(true) } it_behaves_like 'read only access' end diff --git a/spec/unit/access/process_model_access_spec.rb b/spec/unit/access/process_model_access_spec.rb index 8d12d39c48f..c9bb72d99b4 100644 --- a/spec/unit/access/process_model_access_spec.rb +++ b/spec/unit/access/process_model_access_spec.rb @@ -93,10 +93,18 @@ module VCAP::CloudController end end - context 'when the organization is suspended' do - before { object.space.organization.status = 'suspended' } + %i[suspended deleting].each do |state| + context "when the organization is #{state}" do + before { object.space.organization.status = state.to_s } - it_behaves_like 'read only access' + it_behaves_like 'read only access' + end + + context "when the space is #{state}" do + before { object.space.status = state.to_s } + + it_behaves_like 'read only access' + end end context 'when the app_scaling feature flag is disabled' do @@ -246,10 +254,6 @@ module VCAP::CloudController # only using global_auditor as an example of a non-admin user include_context 'global auditor setup' - before do - allow(object).to receive(:in_suspended_org?).and_return(false) - end - it 'does NOT allow global_auditor to create' do expect(subject).not_to be_create(object) end diff --git a/spec/unit/access/route_access_spec.rb b/spec/unit/access/route_access_spec.rb index 830bd62278b..871393dbec4 100644 --- a/spec/unit/access/route_access_spec.rb +++ b/spec/unit/access/route_access_spec.rb @@ -104,6 +104,43 @@ module VCAP::CloudController it_behaves_like('an access control', :update, restricted_write_table) end + describe 'in a deleting org' do + before do + org.update(status: VCAP::CloudController::Organization::DELETING) + end + + it_behaves_like('an access control', :create, restricted_write_table) + it_behaves_like('an access control', :delete, restricted_write_table) + it_behaves_like('an access control', :read_for_update, restricted_write_table) + it_behaves_like('an access control', :update, restricted_write_table) + end + + describe 'in a suspended space' do + before do + flag.enabled = true + flag.save + space.update(status: VCAP::CloudController::Space::SUSPENDED) + end + + it_behaves_like('an access control', :create, restricted_write_table) + it_behaves_like('an access control', :delete, restricted_write_table) + it_behaves_like('an access control', :read_for_update, restricted_write_table) + it_behaves_like('an access control', :update, restricted_write_table) + end + + describe 'in a deleting space' do + before do + flag.enabled = true + flag.save + space.update(status: VCAP::CloudController::Space::DELETING) + end + + it_behaves_like('an access control', :create, restricted_write_table) + it_behaves_like('an access control', :delete, restricted_write_table) + it_behaves_like('an access control', :read_for_update, restricted_write_table) + it_behaves_like('an access control', :update, restricted_write_table) + end + describe 'in an unsuspended org' do describe 'when route creation is enabled' do before do diff --git a/spec/unit/access/route_mapping_access_spec.rb b/spec/unit/access/route_mapping_access_spec.rb index b0abf6eeeed..e3bbf2e7831 100644 --- a/spec/unit/access/route_mapping_access_spec.rb +++ b/spec/unit/access/route_mapping_access_spec.rb @@ -31,13 +31,24 @@ module VCAP::CloudController it_behaves_like 'full access' - context 'when the organization is suspended' do - before do - org.status = 'suspended' - org.save + %i[suspended deleting].each do |state| + context "when the organization is #{state}" do + before do + org.status = state.to_s + org.save + end + + it_behaves_like 'read only access' end - it_behaves_like 'read only access' + context "when the space is #{state}" do + before do + space.status = state.to_s + space.save + end + + it_behaves_like 'read only access' + end end end end diff --git a/spec/unit/access/service_binding_access_spec.rb b/spec/unit/access/service_binding_access_spec.rb index 9ddebe8ce2c..9544ce4d66f 100644 --- a/spec/unit/access/service_binding_access_spec.rb +++ b/spec/unit/access/service_binding_access_spec.rb @@ -131,7 +131,7 @@ module VCAP::CloudController it { is_expected.not_to allow_op_on_object :read_for_update, object } context 'when the organization is suspended' do - before { allow(object).to receive(:in_suspended_org?).and_return(true) } + before { allow(object.space.organization).to receive(:suspended?).and_return(true) } it { is_expected.to allow_op_on_object :read, object } it { is_expected.not_to allow_op_on_object :read_for_update, object } diff --git a/spec/unit/access/service_instance_access_spec.rb b/spec/unit/access/service_instance_access_spec.rb index 541e281ae2e..24a158cfa26 100644 --- a/spec/unit/access/service_instance_access_spec.rb +++ b/spec/unit/access/service_instance_access_spec.rb @@ -71,11 +71,21 @@ module VCAP::CloudController let(:object) { service_instance } end - context 'when the organization is suspended' do - before { allow(service_instance).to receive(:in_suspended_org?).and_return(true) } + %i[suspended deleting].each do |state| + context "when the organization is #{state}" do + before { allow(service_instance.space.organization).to receive(:"#{state}?").and_return(true) } - it_behaves_like 'read only access' do - let(:object) { service_instance } + it_behaves_like 'read only access' do + let(:object) { service_instance } + end + end + + context "when the space is #{state}" do + before { allow(service_instance.space).to receive(:"#{state}?").and_return(true) } + + it_behaves_like 'read only access' do + let(:object) { service_instance } + end end end diff --git a/spec/unit/access/service_key_access_spec.rb b/spec/unit/access/service_key_access_spec.rb index f1bff40913d..bf97a7d84a5 100644 --- a/spec/unit/access/service_key_access_spec.rb +++ b/spec/unit/access/service_key_access_spec.rb @@ -97,10 +97,18 @@ module VCAP::CloudController it { is_expected.to allow_op_on_object :delete, object } it { is_expected.to allow_op_on_object(:read_env, object) } - context 'when the organization is suspended' do - before { allow(object).to receive(:in_suspended_org?).and_return(true) } + %i[suspended deleting].each do |state| + context "when the organization is #{state}" do + before { allow(object.service_instance.space.organization).to receive(:"#{state}?").and_return(true) } - it_behaves_like 'read only access' + it_behaves_like 'read only access' + end + + context "when the space is #{state}" do + before { allow(object.service_instance.space).to receive(:"#{state}?").and_return(true) } + + it_behaves_like 'read only access' + end end end diff --git a/spec/unit/access/space_access_spec.rb b/spec/unit/access/space_access_spec.rb index 11cb99d650d..90c4f09c018 100644 --- a/spec/unit/access/space_access_spec.rb +++ b/spec/unit/access/space_access_spec.rb @@ -12,6 +12,25 @@ module VCAP::CloudController let(:object) { VCAP::CloudController::Space.make(organization: org) } let(:space) { object } + admin_only_write_table = { + unauthenticated: false, + reader_and_writer: false, + reader: false, + writer: false, + + admin: true, + admin_read_only: false, + global_auditor: false, + + space_developer: false, + space_manager: false, + space_auditor: false, + org_user: false, + org_manager: false, + org_auditor: false, + org_billing_manager: false + } + describe 'when the parent organization is suspended' do before do org.update(status: VCAP::CloudController::Organization::SUSPENDED) @@ -55,24 +74,7 @@ module VCAP::CloudController org_billing_manager: false } - write_table = { - unauthenticated: false, - reader_and_writer: false, - reader: false, - writer: false, - - admin: true, - admin_read_only: false, - global_auditor: false, - - space_developer: false, - space_manager: false, - space_auditor: false, - org_user: false, - org_manager: false, - org_auditor: false, - org_billing_manager: false - } + write_table = admin_only_write_table it_behaves_like('an access control', :create, write_table) it_behaves_like('an access control', :delete, write_table) @@ -170,6 +172,34 @@ module VCAP::CloudController end end + describe 'when the parent organization is deleting' do + before do + org.update(status: 'deleting') + end + + write_table = admin_only_write_table + + it_behaves_like('an access control', :create, write_table) + it_behaves_like('an access control', :delete, write_table) + it_behaves_like('an access control', :read_for_update, write_table) + it_behaves_like('an access control', :update, write_table) + end + + %i[suspended deleting].each do |space_state| + describe "when the space is #{space_state}" do + before do + object.update(status: space_state.to_s) + end + + write_table = admin_only_write_table + + it_behaves_like('an access control', :create, write_table) + it_behaves_like('an access control', :delete, write_table) + it_behaves_like('an access control', :read_for_update, write_table) + it_behaves_like('an access control', :update, write_table) + end + end + describe 'when the parent organization is not suspended' do index_table = { unauthenticated: false, diff --git a/spec/unit/actions/organization_update_spec.rb b/spec/unit/actions/organization_update_spec.rb index bb46f4213ab..feb5940ddc5 100644 --- a/spec/unit/actions/organization_update_spec.rb +++ b/spec/unit/actions/organization_update_spec.rb @@ -101,6 +101,13 @@ module VCAP::CloudController expect(updated_org).to be_suspended end + it 'reactivates a suspended organization' do + org.update(status: Organization::SUSPENDED) + message = VCAP::CloudController::OrganizationUpdateMessage.new({ suspended: false }) + updated_org = org_update.update(org, message) + expect(updated_org.reload.status).to eq(Organization::ACTIVE) + end + context 'when model validation fails' do it 'errors' do errors = Sequel::Model::Errors.new @@ -140,6 +147,17 @@ module VCAP::CloudController expect(updated_org.reload.labels).to be_empty end end + + context 'when reactivating a deleting organization' do + it 'rejects the update' do + org.update(status: Organization::DELETING) + message = VCAP::CloudController::OrganizationUpdateMessage.new({ suspended: false }) + + expect do + org_update.update(org, message) + end.to raise_error(OrganizationUpdate::Error, /is being deleted and cannot be reactivated/) + end + end end end end diff --git a/spec/unit/actions/space_create_spec.rb b/spec/unit/actions/space_create_spec.rb index c1547d30001..63b46823aa4 100644 --- a/spec/unit/actions/space_create_spec.rb +++ b/spec/unit/actions/space_create_spec.rb @@ -52,6 +52,18 @@ module VCAP::CloudController end end + it 'creates an active space by default' do + message = VCAP::CloudController::SpaceCreateMessage.new(name: 'my-space', relationships: relationships) + space = SpaceCreate.new(user_audit_info:).create(org, message) + expect(space.suspended?).to be false + end + + it 'creates a suspended space when requested' do + message = VCAP::CloudController::SpaceCreateMessage.new(name: 'my-space', relationships: relationships, suspended: true) + space = SpaceCreate.new(user_audit_info:).create(org, message) + expect(space.suspended?).to be true + end + context 'when a model validation fails' do it 'raises an error' do errors = Sequel::Model::Errors.new diff --git a/spec/unit/actions/space_update_spec.rb b/spec/unit/actions/space_update_spec.rb index 8cc993bec49..c9d37ffe2da 100644 --- a/spec/unit/actions/space_update_spec.rb +++ b/spec/unit/actions/space_update_spec.rb @@ -87,6 +87,30 @@ module VCAP::CloudController expect(updated_space.reload.name).to eq 'old-space-name' end end + + context 'when suspended is requested' do + it 'suspends an active space' do + message = VCAP::CloudController::SpaceUpdateMessage.new({ suspended: true }) + updated = SpaceUpdate.new(user_audit_info).update(space, message) + expect(updated.reload.status).to eq(Space::SUSPENDED) + end + + it 'reactivates a suspended space' do + space.update(status: Space::SUSPENDED) + message = VCAP::CloudController::SpaceUpdateMessage.new({ suspended: false }) + updated = SpaceUpdate.new(user_audit_info).update(space, message) + expect(updated.reload.status).to eq(Space::ACTIVE) + end + + it 'rejects reactivating a deleting space' do + space.update(status: Space::DELETING) + message = VCAP::CloudController::SpaceUpdateMessage.new({ suspended: false }) + + expect do + SpaceUpdate.new(user_audit_info).update(space, message) + end.to raise_error(SpaceUpdate::Error, /is being deleted and cannot be reactivated/) + end + end end end end diff --git a/spec/unit/controllers/v3/deployments_controller_spec.rb b/spec/unit/controllers/v3/deployments_controller_spec.rb index 6179407742d..52e6e96d28d 100644 --- a/spec/unit/controllers/v3/deployments_controller_spec.rb +++ b/spec/unit/controllers/v3/deployments_controller_spec.rb @@ -238,7 +238,7 @@ it 'returns 422 with an error message' do post :create, params: request_body, as: :json expect(response).to have_http_status :unprocessable_content - expect(response.body).to include('Unable to use app. Ensure that the app exists and you have access to it and the organization is not suspended.') + expect(response.body).to include('Unable to use app. Ensure that the app exists and you have access to it.') end end @@ -415,7 +415,7 @@ it 'returns 422 with an error message' do post :create, params: request_body, as: :json expect(response).to have_http_status :unprocessable_content - expect(response.body).to include('Unable to use app. Ensure that the app exists and you have access to it and the organization is not suspended.') + expect(response.body).to include('Unable to use app. Ensure that the app exists and you have access to it.') end end diff --git a/spec/unit/lib/cloud_controller/permissions_spec.rb b/spec/unit/lib/cloud_controller/permissions_spec.rb index a2efd751a58..db2043c9e69 100644 --- a/spec/unit/lib/cloud_controller/permissions_spec.rb +++ b/spec/unit/lib/cloud_controller/permissions_spec.rb @@ -307,47 +307,141 @@ module VCAP::CloudController end end - describe '#is_org_active?' do - it 'returns true' do - expect(permissions.is_org_active?(org.id)).to be true + describe '#org_state' do + it 'returns :active for an active org' do + expect(permissions.org_state(org.id)).to eq(:active) end - context 'org is suspended' do - before do - org.update(status: Organization::SUSPENDED) - end + it 'returns :suspended when the org is suspended' do + org.update(status: Organization::SUSPENDED) + expect(permissions.org_state(org.id)).to eq(:suspended) + end - it 'returns false' do - set_current_user(user) - expect(permissions.is_org_active?(org.id)).to be false - end + it 'returns :deleting when the org is deleting' do + org.update(status: Organization::DELETING) + expect(permissions.org_state(org.id)).to eq(:deleting) + end - it 'returns true for an admin' do - set_current_user(user, { admin: true }) - expect(permissions.is_org_active?(org.id)).to be true - end + it 'reports the actual stored status for admins' do + org.update(status: Organization::DELETING) + set_current_user(user, { admin: true }) + expect(permissions.org_state(org.id)).to eq(:deleting) + end + + it 'reports the actual stored status for org managers' do + org.update(status: Organization::DELETING) + org.add_manager(user) + expect(permissions.org_state(org.id)).to eq(:deleting) + end + + it 'returns :active when the org cannot be found' do + expect(permissions.org_state(-1)).to eq(:active) end end - describe '#is_space_active?' do - it 'returns true' do - expect(permissions.is_space_active?(space.id)).to be true + describe '#writable_org_state' do + it 'returns :active for an active org' do + expect(permissions.writable_org_state(org.id)).to eq(:active) end - context 'org is suspended' do - before do - space.organization.update(status: Organization::SUSPENDED) - end + it 'returns the stored status for users without bypass' do + org.update(status: Organization::SUSPENDED) + expect(permissions.writable_org_state(org.id)).to eq(:suspended) + end - it 'returns false' do - set_current_user(user) - expect(permissions.is_space_active?(space.id)).to be false - end + it 'returns :active for admins regardless of stored status' do + org.update(status: Organization::DELETING) + set_current_user(user, { admin: true }) + expect(permissions.writable_org_state(org.id)).to eq(:active) + end - it 'returns true for an admin' do - set_current_user(user, { admin: true }) - expect(permissions.is_space_active?(space.id)).to be true - end + it 'returns the stored status for org managers (only admins bypass org-level state)' do + org.update(status: Organization::SUSPENDED) + org.add_manager(user) + expect(permissions.writable_org_state(org.id)).to eq(:suspended) + end + end + + describe '#space_state' do + it 'returns :active when both org and space are active' do + expect(permissions.space_state(space.id)).to eq(:active) + end + + it 'returns :suspended when the org is suspended' do + space.organization.update(status: Organization::SUSPENDED) + expect(permissions.space_state(space.id)).to eq(:suspended) + end + + it 'returns :suspended when the space is suspended' do + space.update(status: Space::SUSPENDED) + expect(permissions.space_state(space.id)).to eq(:suspended) + end + + it 'returns :deleting when the space is deleting' do + space.update(status: Space::DELETING) + expect(permissions.space_state(space.id)).to eq(:deleting) + end + + it 'returns :deleting when the org is deleting' do + space.organization.update(status: Organization::DELETING) + expect(permissions.space_state(space.id)).to eq(:deleting) + end + + it 'prefers :deleting over :suspended (parent wins, more terminal wins)' do + space.organization.update(status: Organization::DELETING) + space.update(status: Space::SUSPENDED) + expect(permissions.space_state(space.id)).to eq(:deleting) + end + + it 'reports the actual stored status for admins' do + space.update(status: Space::DELETING) + set_current_user(user, { admin: true }) + expect(permissions.space_state(space.id)).to eq(:deleting) + end + + it 'reports the actual stored status for org managers' do + space.update(status: Space::DELETING) + space.organization.add_manager(user) + expect(permissions.space_state(space.id)).to eq(:deleting) + end + + it 'returns :active when the space cannot be found' do + expect(permissions.space_state(-1)).to eq(:active) + end + end + + describe '#writable_space_state' do + it 'returns :active when both org and space are active' do + expect(permissions.writable_space_state(space.id)).to eq(:active) + end + + it 'returns the stored state for users without bypass' do + space.update(status: Space::SUSPENDED) + expect(permissions.writable_space_state(space.id)).to eq(:suspended) + end + + it 'returns :active for admins regardless of stored status' do + space.update(status: Space::DELETING) + set_current_user(user, { admin: true }) + expect(permissions.writable_space_state(space.id)).to eq(:active) + end + + it 'returns :active for org managers when only the space is suspended/deleting' do + space.update(status: Space::DELETING) + space.organization.add_manager(user) + expect(permissions.writable_space_state(space.id)).to eq(:active) + end + + it 'returns the org status for org managers when the org is suspended (only admins bypass org-level state)' do + space.organization.update(status: Organization::SUSPENDED) + space.organization.add_manager(user) + expect(permissions.writable_space_state(space.id)).to eq(:suspended) + end + + it 'returns the org status for org managers when the org is being deleted' do + space.organization.update(status: Organization::DELETING) + space.organization.add_manager(user) + expect(permissions.writable_space_state(space.id)).to eq(:deleting) end end diff --git a/spec/unit/messages/space_update_message_spec.rb b/spec/unit/messages/space_update_message_spec.rb index d4d9e0909b8..9ec2358b3d8 100644 --- a/spec/unit/messages/space_update_message_spec.rb +++ b/spec/unit/messages/space_update_message_spec.rb @@ -88,6 +88,23 @@ module VCAP::CloudController expect(message).to be_valid end end + + describe 'suspended' do + it 'validates that it is a boolean' do + body = { suspended: 1 } + message = SpaceUpdateMessage.new(body) + + expect(message).not_to be_valid + expect(message.errors.full_messages).to include('Suspended must be a boolean') + end + + it 'is not required' do + body = {} + message = SpaceUpdateMessage.new(body) + + expect(message).to be_valid + end + end end end end diff --git a/spec/unit/messages/spaces/space_create_message_spec.rb b/spec/unit/messages/spaces/space_create_message_spec.rb index 591c482be2d..d239963f5df 100644 --- a/spec/unit/messages/spaces/space_create_message_spec.rb +++ b/spec/unit/messages/spaces/space_create_message_spec.rb @@ -93,6 +93,21 @@ module VCAP::CloudController end end + describe 'suspended' do + it 'validates that it is a boolean' do + body[:suspended] = 1 + message = SpaceCreateMessage.new(body) + + expect(message).not_to be_valid + expect(message.errors.full_messages).to include('Suspended must be a boolean') + end + + it 'is not required' do + message = SpaceCreateMessage.new(body) + expect(message).to be_valid + end + end + context 'relationships' do let(:params) { { relationships: relationships, name: 'name' } } diff --git a/spec/unit/models/helpers/org_space_status_spec.rb b/spec/unit/models/helpers/org_space_status_spec.rb new file mode 100644 index 00000000000..b5859114434 --- /dev/null +++ b/spec/unit/models/helpers/org_space_status_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' +require 'models/helpers/org_space_status' + +module VCAP::CloudController + RSpec.describe OrgSpaceStatus do + let(:host_class) do + Class.new do + include OrgSpaceStatus + + attr_accessor :status + end + end + let(:resource) { host_class.new } + + describe 'constants' do + it 'exposes status string constants' do + expect(OrgSpaceStatus::ACTIVE).to eq('active') + expect(OrgSpaceStatus::SUSPENDED).to eq('suspended') + expect(OrgSpaceStatus::DELETING).to eq('deleting') + end + + it 'lists VALID_STATUSES' do + expect(OrgSpaceStatus::VALID_STATUSES).to eq(%w[active suspended deleting]) + end + + it 'freezes the constants' do + expect(OrgSpaceStatus::ACTIVE).to be_frozen + expect(OrgSpaceStatus::SUSPENDED).to be_frozen + expect(OrgSpaceStatus::DELETING).to be_frozen + expect(OrgSpaceStatus::VALID_STATUSES).to be_frozen + end + end + + describe '#active?' do + it 'is true only when status is active' do + resource.status = 'active' + expect(resource.active?).to be true + + resource.status = 'suspended' + expect(resource.active?).to be false + + resource.status = 'deleting' + expect(resource.active?).to be false + + resource.status = nil + expect(resource.active?).to be false + end + end + + describe '#suspended?' do + it 'is true only when status is suspended' do + resource.status = 'suspended' + expect(resource.suspended?).to be true + + resource.status = 'active' + expect(resource.suspended?).to be false + + resource.status = 'deleting' + expect(resource.suspended?).to be false + end + end + + describe '#deleting?' do + it 'is true only when status is deleting' do + resource.status = 'deleting' + expect(resource.deleting?).to be true + + resource.status = 'active' + expect(resource.deleting?).to be false + + resource.status = 'suspended' + expect(resource.deleting?).to be false + end + end + end +end diff --git a/spec/unit/models/runtime/organization_spec.rb b/spec/unit/models/runtime/organization_spec.rb index 52945ecadb1..d562339d570 100644 --- a/spec/unit/models/runtime/organization_spec.rb +++ b/spec/unit/models/runtime/organization_spec.rb @@ -252,8 +252,8 @@ module VCAP::CloudController end describe 'status' do - it "allows 'active' and 'suspended'" do - %w[active suspended].each do |status| + it "allows 'active', 'suspended', and 'deleting'" do + %w[active suspended deleting].each do |status| org.status = status expect do org.save @@ -439,12 +439,21 @@ module VCAP::CloudController subject(:org) { Organization.make(status: 'active') } it('is active') { expect(org).to be_active } it('is not suspended') { expect(org).not_to be_suspended } + it('is not deleting') { expect(org).not_to be_deleting } end describe 'when status == suspended' do subject(:org) { Organization.make(status: 'suspended') } it('is not active') { expect(org).not_to be_active } it('is suspended') { expect(org).to be_suspended } + it('is not deleting') { expect(org).not_to be_deleting } + end + + describe 'when status == deleting' do + subject(:org) { Organization.make(status: 'deleting') } + it('is not active') { expect(org).not_to be_active } + it('is not suspended') { expect(org).not_to be_suspended } + it('is deleting') { expect(org).to be_deleting } end end diff --git a/spec/unit/models/runtime/private_domain_spec.rb b/spec/unit/models/runtime/private_domain_spec.rb index cc6e69921a7..ec2c0798f2a 100644 --- a/spec/unit/models/runtime/private_domain_spec.rb +++ b/spec/unit/models/runtime/private_domain_spec.rb @@ -165,23 +165,23 @@ module VCAP::CloudController end end - describe '#in_suspended_org?' do + describe '#in_suspended_or_deleting_org?' do let(:org) { Organization.make } let(:private_domain) { PrivateDomain.new(owning_organization: org) } context 'when in a suspended organization' do - before { allow(org).to receive(:suspended?).and_return(true) } + before { allow(org).to receive(:suspended_or_deleting?).and_return(true) } it 'is true' do - expect(private_domain).to be_in_suspended_org + expect(private_domain).to be_in_suspended_or_deleting_org end end context 'when in an un-suspended organization' do - before { allow(org).to receive(:suspended?).and_return(false) } + before { allow(org).to receive(:suspended_or_deleting?).and_return(false) } it 'is false' do - expect(private_domain).not_to be_in_suspended_org + expect(private_domain).not_to be_in_suspended_or_deleting_org end end end diff --git a/spec/unit/models/runtime/process_model_spec.rb b/spec/unit/models/runtime/process_model_spec.rb index 90ad854969b..6bc7d723a18 100644 --- a/spec/unit/models/runtime/process_model_spec.rb +++ b/spec/unit/models/runtime/process_model_spec.rb @@ -587,14 +587,14 @@ def act_as_cf_admin } end - describe '#in_suspended_org?' do + describe '#in_suspended_or_deleting_org?' do subject(:process) { ProcessModel.make } context 'when in a space in a suspended organization' do before { process.organization.update(status: 'suspended') } it 'is true' do - expect(process).to be_in_suspended_org + expect(process).to be_in_suspended_or_deleting_org end end @@ -602,7 +602,7 @@ def act_as_cf_admin before { process.organization.update(status: 'active') } it 'is false' do - expect(process).not_to be_in_suspended_org + expect(process).not_to be_in_suspended_or_deleting_org end end end diff --git a/spec/unit/models/runtime/route_spec.rb b/spec/unit/models/runtime/route_spec.rb index c6554939adc..aa009fec742 100644 --- a/spec/unit/models/runtime/route_spec.rb +++ b/spec/unit/models/runtime/route_spec.rb @@ -1665,24 +1665,24 @@ module VCAP::CloudController end end - describe '#in_suspended_org?' do + describe '#in_suspended_or_deleting_org?' do let(:space) { Space.make } subject(:route) { Route.new(space:) } context 'when in a suspended organization' do - before { allow(space).to receive(:in_suspended_org?).and_return(true) } + before { allow(space).to receive(:in_suspended_or_deleting_org?).and_return(true) } it 'is true' do - expect(route).to be_in_suspended_org + expect(route).to be_in_suspended_or_deleting_org end end context 'when in an unsuspended organization' do - before { allow(space).to receive(:in_suspended_org?).and_return(false) } + before { allow(space).to receive(:in_suspended_or_deleting_org?).and_return(false) } it 'is false' do - expect(route).not_to be_in_suspended_org + expect(route).not_to be_in_suspended_or_deleting_org end end end diff --git a/spec/unit/models/runtime/space_spec.rb b/spec/unit/models/runtime/space_spec.rb index c22df7313aa..8917ff8860b 100644 --- a/spec/unit/models/runtime/space_spec.rb +++ b/spec/unit/models/runtime/space_spec.rb @@ -567,28 +567,70 @@ module VCAP::CloudController } end - describe '#in_suspended_org?' do + describe '#in_suspended_or_deleting_org?' do let(:org) { Organization.make } subject(:space) { Space.new(organization: org) } context 'when in a suspended organization' do - before { allow(org).to receive(:suspended?).and_return(true) } + before { allow(org).to receive(:suspended_or_deleting?).and_return(true) } it 'is true' do - expect(space).to be_in_suspended_org + expect(space).to be_in_suspended_or_deleting_org end end context 'when in an unsuspended organization' do - before { allow(org).to receive(:suspended?).and_return(false) } + before { allow(org).to receive(:suspended_or_deleting?).and_return(false) } it 'is false' do - expect(space).not_to be_in_suspended_org + expect(space).not_to be_in_suspended_or_deleting_org end end end + describe 'status' do + let(:space) { Space.make } + + it "allows 'active', 'suspended', and 'deleting'" do + %w[active suspended deleting].each do |status| + space.status = status + expect { space.save }.not_to raise_error + expect(space.status).to eq(status) + end + end + + it 'does not allow arbitrary status values' do + space.status = 'unknown' + expect { space.save }.to raise_error(Sequel::ValidationFailed) + end + + it 'defaults to active' do + expect(Space.make.status).to eq('active') + end + + describe 'when status == active' do + subject(:space) { Space.make(status: 'active') } + it('is active') { expect(space).to be_active } + it('is not suspended') { expect(space).not_to be_suspended } + it('is not deleting') { expect(space).not_to be_deleting } + end + + describe 'when status == suspended' do + subject(:space) { Space.make(status: 'suspended') } + it('is not active') { expect(space).not_to be_active } + it('is suspended') { expect(space).to be_suspended } + it('is not deleting') { expect(space).not_to be_deleting } + end + + describe 'when status == deleting' do + subject(:space) { Space.make(status: 'deleting') } + it('is not active') { expect(space).not_to be_active } + it('is not suspended') { expect(space).not_to be_suspended } + it('is deleting') { expect(space).to be_deleting } + end + end + describe '#destroy' do subject(:space) { Space.make } diff --git a/spec/unit/models/services/service_binding_spec.rb b/spec/unit/models/services/service_binding_spec.rb index fde838b2224..ddf2fdf1e3b 100644 --- a/spec/unit/models/services/service_binding_spec.rb +++ b/spec/unit/models/services/service_binding_spec.rb @@ -201,24 +201,24 @@ def new_model end end - describe '#in_suspended_org?' do + describe '#in_suspended_or_deleting_org?' do let(:app_model) { VCAP::CloudController::AppModel.make } subject(:service_binding) { VCAP::CloudController::ServiceBinding.new(app: app_model) } context 'when in a suspended organization' do - before { allow(app_model.space).to receive(:in_suspended_org?).and_return(true) } + before { allow(app_model.space).to receive(:in_suspended_or_deleting_org?).and_return(true) } it 'is true' do - expect(service_binding).to be_in_suspended_org + expect(service_binding).to be_in_suspended_or_deleting_org end end context 'when in an unsuspended organization' do - before { allow(app_model.space).to receive(:in_suspended_org?).and_return(false) } + before { allow(app_model.space).to receive(:in_suspended_or_deleting_org?).and_return(false) } it 'is false' do - expect(service_binding).not_to be_in_suspended_org + expect(service_binding).not_to be_in_suspended_or_deleting_org end end end diff --git a/spec/unit/models/services/service_instance_spec.rb b/spec/unit/models/services/service_instance_spec.rb index 8d2242e5203..245daf29aad 100644 --- a/spec/unit/models/services/service_instance_spec.rb +++ b/spec/unit/models/services/service_instance_spec.rb @@ -336,24 +336,24 @@ module VCAP::CloudController end end - describe '#in_suspended_org?' do + describe '#in_suspended_or_deleting_org?' do let(:space) { VCAP::CloudController::Space.make } subject(:service_instance) { VCAP::CloudController::ServiceInstance.new(space:) } context 'when in a suspended organization' do - before { allow(space).to receive(:in_suspended_org?).and_return(true) } + before { allow(space).to receive(:in_suspended_or_deleting_org?).and_return(true) } it 'is true' do - expect(service_instance).to be_in_suspended_org + expect(service_instance).to be_in_suspended_or_deleting_org end end context 'when in an unsuspended organization' do - before { allow(space).to receive(:in_suspended_org?).and_return(false) } + before { allow(space).to receive(:in_suspended_or_deleting_org?).and_return(false) } it 'is false' do - expect(service_instance).not_to be_in_suspended_org + expect(service_instance).not_to be_in_suspended_or_deleting_org end end @@ -361,7 +361,7 @@ module VCAP::CloudController let(:space) { nil } it 'is false' do - expect(service_instance).not_to be_in_suspended_org + expect(service_instance).not_to be_in_suspended_or_deleting_org end end end diff --git a/spec/unit/presenters/v3/space_presenter_spec.rb b/spec/unit/presenters/v3/space_presenter_spec.rb index 59324cb7f01..c9ff023128d 100644 --- a/spec/unit/presenters/v3/space_presenter_spec.rb +++ b/spec/unit/presenters/v3/space_presenter_spec.rb @@ -46,6 +46,7 @@ module VCAP::CloudController::Presenters::V3 expect(result[:created_at]).to eq(space.created_at) expect(result[:updated_at]).to eq(space.updated_at) expect(result[:name]).to eq(space.name) + expect(result[:suspended]).to be(false) expect(result[:links][:self][:href]).to match(%r{/v3/spaces/#{space.guid}$}) expect(result[:links][:self][:href]).to eq("#{link_prefix}/v3/spaces/#{space.guid}") expect(result[:links][:features][:href]).to match(%r{/v3/spaces/#{space.guid}/features$}) @@ -59,6 +60,22 @@ module VCAP::CloudController::Presenters::V3 expect(result[:metadata][:annotations]).to eq('altitude' => '14,411', 'grass' => 'yes') end + context 'when the space is suspended' do + before { space.update(status: VCAP::CloudController::Space::SUSPENDED) } + + it 'presents suspended as true' do + expect(result[:suspended]).to be(true) + end + end + + context 'when the space is deleting' do + before { space.update(status: VCAP::CloudController::Space::DELETING) } + + it 'presents suspended as false' do + expect(result[:suspended]).to be(false) + end + end + context 'when the space has a space quota applied to it' do let!(:space_quota) { VCAP::CloudController::SpaceQuotaDefinition.make(organization: space.organization) }