├── bundler.d └── .keep ├── lib ├── manageiq-api.rb ├── manageiq │ ├── api.rb │ └── api │ │ ├── version.rb │ │ └── engine.rb ├── tasks │ └── manageiq │ │ └── api_tasks.rake ├── api │ ├── http_headers.rb │ ├── api_config.rb │ ├── query_counts.rb │ ├── utils.rb │ ├── environment.rb │ └── links_builder.rb ├── services │ └── api │ │ ├── request_editor.rb │ │ ├── authentication_service.rb │ │ ├── error_serializer.rb │ │ ├── request_parser.rb │ │ ├── metric_rollups_service.rb │ │ ├── options_serializer.rb │ │ └── user_token_service.rb ├── tasks_private │ └── spec.rake └── api.rb ├── config ├── settings │ └── test.yml ├── settings.yml └── routes.rb ├── .rubocop_local.yml ├── app └── controllers │ └── api │ ├── tasks_controller.rb │ ├── zones_controller.rb │ ├── events_controller.rb │ ├── features_controller.rb │ ├── flavors_controller.rb │ ├── measures_controller.rb │ ├── regions_controller.rb │ ├── servers_controller.rb │ ├── currencies_controller.rb │ ├── firmwares_controller.rb │ ├── alert_actions_controller.rb │ ├── cloud_subnets_controller.rb │ ├── cloud_tenants_controller.rb │ ├── floating_ips_controller.rb │ ├── guest_devices_controller.rb │ ├── request_tasks_controller.rb │ ├── tenant_quotas_controller.rb │ ├── cloud_networks_controller.rb │ ├── load_balancers_controller.rb │ ├── network_routers_controller.rb │ ├── policy_actions_controller.rb │ ├── security_groups_controller.rb │ ├── availability_zones_controller.rb │ ├── middleware_domains_controller.rb │ ├── middleware_servers_controller.rb │ ├── provision_dialogs_controller.rb │ ├── middleware_messagings_controller.rb │ ├── middleware_datasources_controller.rb │ ├── middleware_deployments_controller.rb │ ├── alerts_controller.rb │ ├── chargebacks_controller.rb │ ├── data_stores_controller.rb │ ├── policy_profiles_controller.rb │ ├── ping_controller.rb │ ├── service_catalogs_controller.rb │ ├── configuration_script_payloads_controller.rb │ ├── subcollections │ ├── accounts.rb │ ├── rates.rb │ ├── software.rb │ ├── cloud_subnets.rb │ ├── cloud_tenants.rb │ ├── schedules.rb │ ├── resource_actions.rb │ ├── events.rb │ ├── alert_definitions.rb │ ├── cloud_networks.rb │ ├── load_balancers.rb │ ├── conditions.rb │ ├── configuration_script_payloads.rb │ ├── policy_actions.rb │ ├── request_tasks.rb │ ├── results.rb │ ├── orchestration_stacks.rb │ ├── authentications.rb │ ├── metric_rollups.rb │ ├── policy_profiles.rb │ ├── alert_actions.rb │ ├── service_dialogs.rb │ ├── quotas.rb │ ├── vms.rb │ ├── snapshots.rb │ ├── service_requests.rb │ ├── custom_attributes.rb │ └── features.rb │ ├── templates_controller.rb │ ├── resource_pools_controller.rb │ ├── pictures_controller.rb │ ├── clusters_controller.rb │ ├── rates_controller.rb │ ├── actions_controller.rb │ ├── cloud_volumes_controller.rb │ ├── results_controller.rb │ ├── notifications_controller.rb │ ├── automate_workspaces_controller.rb │ ├── categories_controller.rb │ ├── metric_rollups_controller.rb │ ├── blueprints_controller.rb │ ├── orchestration_templates_controller.rb │ ├── event_streams_controller.rb │ ├── conditions_controller.rb │ ├── alert_definition_profiles_controller.rb │ ├── container_deployments_controller.rb │ ├── auth_controller.rb │ ├── base_controller │ ├── action.rb │ ├── results.rb │ └── parameters.rb │ ├── service_templates_controller.rb │ ├── hosts_controller.rb │ ├── settings_controller.rb │ ├── alert_definitions_controller.rb │ ├── tenants_controller.rb │ ├── automate_controller.rb │ ├── automation_requests_controller.rb │ ├── physical_servers_controller.rb │ ├── automate_domains_controller.rb │ ├── tags_controller.rb │ ├── roles_controller.rb │ ├── provision_requests_controller.rb │ ├── generic_objects_controller.rb │ ├── groups_controller.rb │ ├── policies_controller.rb │ ├── service_requests_controller.rb │ ├── authentications_controller.rb │ ├── configuration_script_sources_controller.rb │ ├── service_dialogs_controller.rb │ ├── requests_controller.rb │ ├── service_orders_controller.rb │ ├── base_controller.rb │ └── reports_controller.rb ├── .rspec ├── .rubocop_cc.yml ├── .rspec_ci ├── spec ├── requests │ ├── ping_spec.rb │ ├── clusters_spec.rb │ ├── normalization_spec.rb │ ├── firmwares_spec.rb │ ├── container_deployments_spec.rb │ ├── regions_spec.rb │ ├── guest_devices_spec.rb │ ├── versioning_spec.rb │ ├── cloud_subnets_spec.rb │ ├── cloud_tenants_spec.rb │ ├── load_balancers_spec.rb │ ├── custom_attributes_spec.rb │ ├── floating_ips_spec.rb │ ├── network_routers_spec.rb │ ├── middleware_domains_spec.rb │ ├── middleware_messagings_spec.rb │ ├── middleware_datasources_spec.rb │ ├── automate_workspaces_spec.rb │ ├── middleware_servers_spec.rb │ ├── middleware_deployments_spec.rb │ ├── entrypoint_spec.rb │ ├── base_controller │ │ └── parser_spec.rb │ ├── automate_domains_spec.rb │ ├── events_spec.rb │ ├── headers_spec.rb │ ├── templates_spec.rb │ ├── policy_actions_spec.rb │ ├── cloud_networks_spec.rb │ └── hosts_spec.rb ├── lib │ ├── services │ │ └── api │ │ │ ├── options_serializer_spec.rb │ │ │ ├── metric_rollups_service_spec.rb │ │ │ └── request_parser_spec.rb │ ├── api_spec.rb │ └── api │ │ ├── collection_config_spec.rb │ │ └── api_config_spec.rb ├── spec_helper.rb └── support │ └── api │ └── request_helpers.rb ├── .rubocop.yml ├── .gitignore ├── Rakefile ├── bin ├── setup ├── update └── rails ├── .travis.yml ├── Gemfile ├── manageiq-api.gemspec ├── .codeclimate.yml ├── README.md └── CHANGELOG.md /bundler.d/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/manageiq-api.rb: -------------------------------------------------------------------------------- 1 | require 'manageiq/api' 2 | -------------------------------------------------------------------------------- /config/settings/test.yml: -------------------------------------------------------------------------------- 1 | :api: 2 | :metrics_default_limit: 10000 3 | -------------------------------------------------------------------------------- /.rubocop_local.yml: -------------------------------------------------------------------------------- 1 | GlobalVars: 2 | AllowedVariables: 3 | - $evm 4 | - $api_log 5 | -------------------------------------------------------------------------------- /lib/manageiq/api.rb: -------------------------------------------------------------------------------- 1 | require "manageiq/api/version" 2 | require "manageiq/api/engine" 3 | -------------------------------------------------------------------------------- /app/controllers/api/tasks_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class TasksController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/zones_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ZonesController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/events_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class EventsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/features_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class FeaturesController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/flavors_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class FlavorsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/measures_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MeasuresController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/regions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RegionsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/servers_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ServersController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/manageiq/api/version.rb: -------------------------------------------------------------------------------- 1 | module ManageIQ 2 | module Api 3 | VERSION = "3.0.0-pre".freeze 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/api/currencies_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class CurrenciesController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/firmwares_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class FirmwaresController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/alert_actions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AlertActionsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/cloud_subnets_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class CloudSubnetsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/cloud_tenants_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class CloudTenantsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/floating_ips_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class FloatingIpsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/guest_devices_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class GuestDevicesController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/request_tasks_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RequestTasksController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/tenant_quotas_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class TenantQuotasController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/cloud_networks_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class CloudNetworksController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/load_balancers_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class LoadBalancersController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/network_routers_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class NetworkRoutersController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/policy_actions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PolicyActionsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/security_groups_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class SecurityGroupsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/availability_zones_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AvailabilityZonesController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/middleware_domains_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MiddlewareDomainsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/middleware_servers_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MiddlewareServersController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/provision_dialogs_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ProvisionDialogsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/tasks/manageiq/api_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :manageiq_api do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /app/controllers/api/middleware_messagings_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MiddlewareMessagingsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/middleware_datasources_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MiddlewareDatasourcesController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/middleware_deployments_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MiddlewareDeploymentsController < BaseController 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require manageiq/spec/spec_helper 2 | --require spec_helper 3 | --color 4 | --order random 5 | --exclude-pattern "spec/manageiq/**/*_spec.rb" 6 | -------------------------------------------------------------------------------- /.rubocop_cc.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | # this is downloaded by .codeclimate.yml 3 | - .rubocop_base.yml 4 | - .rubocop_cc_base.yml 5 | - .rubocop_local.yml 6 | -------------------------------------------------------------------------------- /app/controllers/api/alerts_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AlertsController < BaseController 3 | include Subcollections::AlertActions 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/api/chargebacks_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ChargebacksController < BaseController 3 | include Subcollections::Rates 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/api/data_stores_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class DataStoresController < BaseController 3 | include Subcollections::Tags 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.rspec_ci: -------------------------------------------------------------------------------- 1 | --require manageiq/spec/spec_helper 2 | --require spec_helper 3 | --color 4 | --order random 5 | --exclude-pattern "spec/manageiq/**/*_spec.rb" 6 | --profile 25 7 | -------------------------------------------------------------------------------- /app/controllers/api/policy_profiles_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PolicyProfilesController < BaseController 3 | include Subcollections::Policies 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | :api: 2 | :token_ttl: 10.minutes 3 | :authentication_timeout: 30.seconds 4 | :metrics_default_limit: 1000 5 | :event_streams_default_limit: 1000 6 | -------------------------------------------------------------------------------- /app/controllers/api/ping_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PingController < ActionController::API 3 | def index 4 | render :plain => 'pong' 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/api/service_catalogs_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ServiceCatalogsController < BaseController 3 | include Subcollections::ServiceTemplates 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/api/configuration_script_payloads_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ConfigurationScriptPayloadsController < BaseController 3 | include Subcollections::Authentications 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/api/http_headers.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module HttpHeaders 3 | AUTH_TOKEN = "HTTP_X_AUTH_TOKEN".freeze 4 | MIQ_TOKEN = "HTTP_X_MIQ_TOKEN".freeze 5 | MIQ_GROUP = "HTTP_X_MIQ_GROUP".freeze 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/api/api_config.rb: -------------------------------------------------------------------------------- 1 | require "config" 2 | 3 | module Api 4 | ApiConfig = ::Config::Options.new.tap do |o| 5 | o.add_source!(ManageIQ::Api::Engine.root.join("config/api.yml").to_s) 6 | o.load! 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/requests/ping_spec.rb: -------------------------------------------------------------------------------- 1 | describe "PingController" do 2 | it "get" do 3 | get(api_ping_url) 4 | 5 | expect(response).to have_http_status(:success) 6 | expect(response.parsed_body).to eq("pong") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_base.yml 3 | # put all local rubocop config into .rubocop_local.yml as it will be loaded by .rubocop_cc.yml as well 4 | - .rubocop_local.yml 5 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/accounts.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Accounts 4 | def accounts_query_resource(object) 5 | object.accounts 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/rates.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Rates 4 | def rates_query_resource(object) 5 | object.chargeback_rate_details 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rubocop-* 2 | /.bundle/ 3 | /.yardoc 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | 12 | /spec/manageiq 13 | 14 | # ignore included plugins in bundler.d 15 | bundler.d/* 16 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/software.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Software 4 | def software_query_resource(object) 5 | object.guest_applications 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/templates_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class TemplatesController < BaseController 3 | include Subcollections::Policies 4 | include Subcollections::PolicyProfiles 5 | include Subcollections::Tags 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/api/resource_pools_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ResourcePoolsController < BaseController 3 | include Subcollections::Policies 4 | include Subcollections::PolicyProfiles 5 | include Subcollections::Tags 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/cloud_subnets.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module CloudSubnets 4 | def cloud_subnets_query_resource(object) 5 | object.cloud_subnets 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/cloud_tenants.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module CloudTenants 4 | def cloud_tenants_query_resource(object) 5 | object.cloud_tenants 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/schedules.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Schedules 4 | def schedules_query_resource(object) 5 | object ? object.list_schedules : {} 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/resource_actions.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module ResourceActions 4 | def resource_actions_query_resource(object) 5 | object.resource_actions 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/events.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Events 4 | def events_query_resource(object) 5 | return {} unless object.respond_to?(:events) 6 | object.events 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/alert_definitions.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module AlertDefinitions 4 | def alert_definitions_query_resource(object) 5 | object.respond_to?(:miq_alerts) ? object.miq_alerts : [] 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/cloud_networks.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module CloudNetworks 4 | def cloud_networks_query_resource(object) 5 | object.respond_to?(:cloud_networks) ? object.cloud_networks : [] 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/load_balancers.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module LoadBalancers 4 | def load_balancers_query_resource(object) 5 | object.respond_to?(:load_balancers) ? object.load_balancers : [] 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/conditions.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Conditions 4 | def conditions_query_resource(object) 5 | return {} unless object.respond_to?(:conditions) 6 | object.conditions 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/configuration_script_payloads.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module ConfigurationScriptPayloads 4 | def configuration_script_payloads_query_resource(object) 5 | object.configuration_script_payloads 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/pictures_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PicturesController < BaseController 3 | def create_resource(_type, _id, data) 4 | Picture.create_from_base64(data) 5 | rescue => err 6 | raise BadRequestError, "Failed to create Picture - #{err}" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/policy_actions.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module PolicyActions 4 | def policy_actions_query_resource(object) 5 | return {} unless object.respond_to?(:miq_actions) 6 | object.miq_actions 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/request_tasks.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module RequestTasks 4 | def request_tasks_query_resource(object) 5 | klass = collection_class(:request_tasks) 6 | object ? klass.where(:miq_request_id => object.id) : {} 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/requests/clusters_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'Clusters API' do 2 | context 'OPTIONS /api/clusters' do 3 | it 'returns clusters node_types' do 4 | expected_data = {"node_types" => EmsCluster.node_types.to_s} 5 | 6 | options(api_clusters_url) 7 | expect_options_results(:clusters, expected_data) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/clusters_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ClustersController < BaseController 3 | include Subcollections::Policies 4 | include Subcollections::PolicyProfiles 5 | include Subcollections::Tags 6 | 7 | def options 8 | render_options(:clusters, :node_types => EmsCluster.node_types) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/api/rates_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RatesController < BaseController 3 | def create_resource(_type, _id, data = {}) 4 | rate_detail = ChargebackRateDetail.create(data) 5 | raise BadRequestError, rate_detail.errors.full_messages.join(', ') unless rate_detail.valid? 6 | rate_detail 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/results.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Results 4 | def find_results(id) 5 | MiqReportResult.for_user(User.current_user).find(id) 6 | end 7 | 8 | def results_query_resource(object) 9 | object.miq_report_results.for_user(User.current_user) 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | 6 | APP_RAKEFILE = File.expand_path("../spec/manageiq/Rakefile", __FILE__) 7 | load 'rails/tasks/engine.rake' 8 | load 'rails/tasks/statistics.rake' 9 | rescue LoadError 10 | end 11 | 12 | require 'bundler/gem_tasks' 13 | 14 | FileList['lib/tasks_private/**/*.rake'].each { |r| load r } 15 | 16 | task :default => :spec 17 | -------------------------------------------------------------------------------- /spec/requests/normalization_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Normalization of objects API" do 2 | it "represents datetimes in ISO8601 format" do 3 | api_basic_authorize action_identifier(:hosts, :read, :resource_actions, :get) 4 | host = FactoryGirl.create(:host) 5 | 6 | get(api_host_url(nil, host)) 7 | 8 | expect(response.parsed_body).to include("created_on" => host.created_on.iso8601) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | gem_root = Pathname.new(__dir__).join("..") 5 | 6 | unless gem_root.join("spec/manageiq").exist? 7 | puts "== Cloning manageiq sample app ==" 8 | system "git clone https://github.com/ManageIQ/manageiq.git --depth 1 spec/manageiq" 9 | end 10 | 11 | require gem_root.join("spec/manageiq/lib/manageiq/environment").to_s 12 | ManageIQ::Environment.manageiq_plugin_setup 13 | -------------------------------------------------------------------------------- /lib/services/api/request_editor.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RequestEditor 3 | def self.edit(request, data) 4 | request_options = RequestParser.parse_options(data) 5 | user = RequestParser.parse_user(data) || User.current_user 6 | 7 | begin 8 | request.update_request(request_options, user) 9 | rescue => err 10 | raise BadRequestError, "Could not update the request - #{err}" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/orchestration_stacks.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module OrchestrationStacks 4 | def orchestration_stacks_query_resource(object) 5 | object.orchestration_stacks 6 | end 7 | 8 | # 9 | # Virtual attribute accessors 10 | # 11 | def fetch_orchestration_stacks_stdout(resource) 12 | resource.stdout(attribute_format("stdout")) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/api/actions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ActionsController < BaseController 3 | def create_resource(type, id, data = {}) 4 | data["options"] = data["options"].deep_symbolize_keys if data["options"] 5 | super(type, id, data) 6 | end 7 | 8 | def edit_resource(type, id = nil, data = {}) 9 | data["options"] = data["options"].deep_symbolize_keys if data["options"] 10 | super(type, id, data) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/services/api/authentication_service.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AuthenticationService 3 | def self.create_authentication_task(manager_resource, attrs) 4 | klass = ::Authentication.descendant_get(attrs['type']) 5 | # TODO: Temporary validation - remove 6 | raise 'type not currently supported' unless klass.respond_to?(:create_in_provider_queue) 7 | klass.create_in_provider_queue(manager_resource.id, attrs.deep_symbolize_keys) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lib/services/api/options_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Api::OptionsSerializer do 2 | it "returns some default values when the class is nil" do 3 | actual = described_class.new(nil).serialize 4 | 5 | expected = { 6 | :attributes => [], 7 | :virtual_attributes => [], 8 | :relationships => [], 9 | :subcollections => [], 10 | :data => {} 11 | } 12 | 13 | expect(actual).to eq(expected) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | gem_root = Pathname.new(__dir__).join("..") 5 | 6 | if gem_root.join("spec/manageiq").symlink? 7 | puts "== SKIPPING update of spec/manageiq because its symlinked ==" 8 | else 9 | puts "== Updating manageiq sample app ==" 10 | system("git pull", :chdir => gem_root.join("spec/manageiq")) 11 | end 12 | 13 | require gem_root.join("spec/manageiq/lib/manageiq/environment").to_s 14 | ManageIQ::Environment.manageiq_plugin_update 15 | -------------------------------------------------------------------------------- /lib/tasks_private/spec.rake: -------------------------------------------------------------------------------- 1 | namespace :spec do 2 | desc "Setup environment specs" 3 | task :setup => ["app:test:initialize", "app:test:verify_no_db_access_loading_rails_environment", "app:test:setup_db"] 4 | end 5 | 6 | desc "Run all specs" 7 | RSpec::Core::RakeTask.new(:spec => ["app:test:initialize", "app:evm:compile_sti_loader"]) do |t| 8 | spec_dir = File.expand_path("../../spec", __dir__) 9 | EvmTestHelper.init_rspec_task(t, ['--require', File.join(spec_dir, 'spec_helper')]) 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/cloud_volumes_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class CloudVolumesController < BaseController 3 | def delete_resource(type, id, _data = {}) 4 | cloud_volume = resource_search(id, type, collection_class(:cloud_volumes)) 5 | task_id = cloud_volume.delete_volume_queue(User.current_user) 6 | action_result(true, "Deleting Cloud Volume #{cloud_volume.name}", :task_id => task_id) 7 | rescue => err 8 | action_result(false, err.to_s) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/api/query_counts.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class QueryCounts 3 | attr_reader :count, :subcount, :subquery_count 4 | 5 | def initialize(count, subcount = nil, subquery_count = nil) 6 | @count = count 7 | @subcount = subcount 8 | @subquery_count = subquery_count 9 | end 10 | 11 | def counts 12 | { 13 | :count => count, 14 | :subcount => subcount, 15 | :subquery_count => subquery_count 16 | }.compact 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/lib/api_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Api do 2 | describe ".compressed_id?" do 3 | it "returns true for a compressed id" do 4 | expect(Api.compressed_id?("1r1")).to be(true) 5 | end 6 | 7 | it "returns false for an uncompressed id" do 8 | expect(Api.compressed_id?(1_000_000_000_001)).to be(false) 9 | expect(Api.compressed_id?("1000000000001")).to be(false) 10 | end 11 | 12 | it "returns false for nil" do 13 | expect(Api.compressed_id?(nil)).to be(false) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 6 | ENGINE_PATH = File.expand_path('../../lib/manageiq/api/engine', __FILE__) 7 | 8 | # Set up gems listed in the Gemfile. 9 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 10 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 11 | 12 | require 'rails/all' 13 | require 'rails/engine/commands' 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - '2.3.1' 4 | - '2.4.1' 5 | sudo: false 6 | cache: bundler 7 | env: 8 | global: 9 | - RUBY_GC_HEAP_GROWTH_MAX_SLOTS=300000 10 | - RUBY_GC_HEAP_INIT_SLOTS=600000 11 | - RUBY_GC_HEAP_GROWTH_FACTOR=1.25 12 | addons: 13 | postgresql: '9.4' 14 | install: bin/setup 15 | after_script: bundle exec codeclimate-test-reporter 16 | notifications: 17 | webhooks: 18 | urls: 19 | - https://webhooks.gitter.im/e/46abb9c5f1f009b6a699 20 | on_success: change 21 | on_failure: always 22 | on_start: never 23 | -------------------------------------------------------------------------------- /app/controllers/api/results_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ResultsController < BaseController 3 | before_action :set_additional_attributes, :only => [:index, :show] 4 | 5 | def results_search_conditions 6 | MiqReportResult.for_user(User.current_user).where_clause.ast 7 | end 8 | 9 | def find_results(id) 10 | MiqReportResult.for_user(User.current_user).find(id) 11 | end 12 | 13 | private 14 | 15 | def set_additional_attributes 16 | @additional_attributes = %w(result_set) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/services/api/error_serializer.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ErrorSerializer 3 | attr_reader :kind, :error 4 | 5 | def initialize(kind, error) 6 | @kind = kind 7 | @error = error 8 | end 9 | 10 | def serialize 11 | result = { 12 | :error => { 13 | :kind => kind, 14 | :message => error.message, 15 | :klass => error.class.name 16 | } 17 | } 18 | result[:error][:backtrace] = error.backtrace.join("\n") if Rails.env.test? 19 | result 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/controllers/api/notifications_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class NotificationsController < BaseController 3 | def notifications_search_conditions 4 | {:user_id => User.current_user.id} 5 | end 6 | 7 | def find_notifications(id) 8 | User.current_user.notification_recipients.find(id) 9 | end 10 | 11 | def mark_as_seen_resource(type, id = nil, _data = nil) 12 | api_action(type, id) do |klass| 13 | notification = resource_search(id, type, klass) 14 | action_result(notification.update_attribute(:seen, true) || false) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/authentications.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Authentications 4 | def authentications_query_resource(object) 5 | object.respond_to?(:authentications) ? object.authentications : [] 6 | end 7 | 8 | def authentications_create_resource(parent, _type, _id, data) 9 | task_id = AuthenticationService.create_authentication_task(parent.manager, data) 10 | action_result(true, 'Creating Authentication', :task_id => task_id) 11 | rescue => err 12 | action_result(false, err.to_s) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/controllers/api/automate_workspaces_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AutomateWorkspacesController < BaseController 3 | def show 4 | obj = AutomateWorkspace.find_by(:guid => @req.c_id) 5 | if obj.nil? 6 | raise NotFoundError, "Invalid Workspace #{@req.c_id} specified" 7 | end 8 | render_resource(:automate_workspaces, obj) 9 | end 10 | 11 | def edit_resource(_type, id, data = {}) 12 | obj = AutomateWorkspace.find_by(:guid => id) 13 | if obj.nil? 14 | raise NotFoundError, "Invalid Workspace #{id} specified" 15 | end 16 | obj.merge_output!(data) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/categories_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class CategoriesController < BaseController 3 | include Subcollections::Tags 4 | 5 | before_action :set_additional_attributes, :only => [:index, :show, :update] 6 | 7 | def edit_resource(type, id, data = {}) 8 | raise ForbiddenError if Category.find(id).read_only? 9 | super 10 | end 11 | 12 | def delete_resource(type, id, data = {}) 13 | raise ForbiddenError if Category.find(id).read_only? 14 | super 15 | end 16 | 17 | private 18 | 19 | def set_additional_attributes 20 | @additional_attributes = %w(name) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/metric_rollups.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module MetricRollups 4 | RESOURCE_TYPES = { 5 | 'vms' => 'VmOrTemplate' 6 | }.freeze 7 | 8 | def metric_rollups_query_resource(object) 9 | params[:offset] ||= 0 10 | params[:limit] ||= Settings.api.metrics_default_limit 11 | params[:resource_type] = RESOURCE_TYPES[@req.collection] || object.class.to_s 12 | params[:resource_ids] ||= [object.id] 13 | 14 | rollups_service = MetricRollupsService.new(params) 15 | rollups_service.query_metric_rollups 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/api/metric_rollups_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MetricRollupsController < BaseController 3 | def index 4 | params[:offset] ||= 0 5 | params[:limit] ||= Settings.api.metrics_default_limit 6 | 7 | rollups_service = MetricRollupsService.new(params) 8 | resources = rollups_service.query_metric_rollups 9 | res = collection_filterer(resources, :metric_rollups, MetricRollup).flatten 10 | counts = Api::QueryCounts.new(MetricRollup.count, res.count, resources.count) 11 | 12 | render_collection(:metric_rollups, res, :counts => counts, :expand_resources => @req.expand?(:resources)) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/api/blueprints_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class BlueprintsController < BaseController 3 | include Subcollections::Tags 4 | 5 | before_action :set_additional_attributes, :only => [:index, :show] 6 | 7 | def publish_resource(type, id, data) 8 | blueprint = resource_search(id, type, Blueprint) 9 | begin 10 | blueprint.publish(data['bundle_name']) 11 | rescue => err 12 | raise BadRequestError, "Failed to publish blueprint - #{err}" 13 | end 14 | blueprint 15 | end 16 | 17 | private 18 | 19 | def set_additional_attributes 20 | @additional_attributes = %w(content) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | # Load Gemfile with dependencies from manageiq 6 | manageiq_gemfile = File.expand_path("spec/manageiq/Gemfile", __dir__) 7 | if File.exist?(manageiq_gemfile) 8 | eval_gemfile(manageiq_gemfile) 9 | else 10 | puts "ERROR: The ManageIQ application must be present in spec/manageiq." 11 | puts " Clone it from GitHub or symlink it from local source." 12 | exit 1 13 | end 14 | 15 | # Load other additional Gemfiles 16 | # Developers can create a file ending in .rb under bundler.d/ to specify additional development dependencies 17 | Dir.glob(File.join(__dir__, 'bundler.d/*.rb')).each { |f| eval_gemfile(File.expand_path(f, __dir__)) } 18 | -------------------------------------------------------------------------------- /spec/lib/services/api/metric_rollups_service_spec.rb: -------------------------------------------------------------------------------- 1 | describe Api::MetricRollupsService do 2 | context ".new" do 3 | it 'validates that all of the parameters are present' do 4 | expect do 5 | described_class.new({}) 6 | end.to raise_error(Api::BadRequestError, a_string_including('Must specify')) 7 | end 8 | 9 | it 'validates the capture interval' do 10 | expect do 11 | described_class.new(:resource_type => 'Service', 12 | :start_date => Time.zone.today.to_s, 13 | :capture_interval => 'bad_interval') 14 | end.to raise_error(Api::BadRequestError, a_string_including('Capture interval must be one of ')) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/api/orchestration_templates_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class OrchestrationTemplatesController < BaseController 3 | def delete_resource(type, id, data = {}) 4 | klass = collection_class(type) 5 | resource = resource_search(id, type, klass) 6 | result = super 7 | resource.raw_destroy if resource.kind_of?(OrchestrationTemplateVnfd) 8 | result 9 | end 10 | 11 | def copy_resource(type, id, data = {}) 12 | resource = resource_search(id, type, collection_class(type)) 13 | resource.dup.tap do |new_resource| 14 | new_resource.assign_attributes(data) 15 | new_resource.save! 16 | end 17 | rescue => err 18 | raise BadRequestError, "Failed to copy orchestration template - #{err}" 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/api/event_streams_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class EventStreamsController < BaseController 3 | before_action :validate_filters, :ensure_pagination, :only => :index 4 | 5 | private 6 | 7 | def validate_filters 8 | messages = [] 9 | messages << "must specify target_type" unless filter_contains?("target_type") 10 | messages << "must specify a minimum value for timestamp" unless filter_contains?("timestamp>") 11 | raise BadRequestError, messages.join(", ").capitalize if messages.any? 12 | end 13 | 14 | def filter_contains?(filter) 15 | Array(params["filter"]).any? { |f| f.start_with?(filter) } 16 | end 17 | 18 | def ensure_pagination 19 | params["limit"] ||= Settings.api.event_streams_default_limit 20 | params["offset"] ||= 0 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /manageiq-api.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "manageiq/api/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "manageiq-api" 9 | s.version = ManageIQ::Api::VERSION 10 | s.authors = ["ManageIQ Developers"] 11 | s.homepage = "https://github.com/ManageIQ/manageiq-api" 12 | s.summary = "The ManageIQ Api" 13 | s.description = "The ManageIQ Api" 14 | s.licenses = ["Apache-2.0"] 15 | 16 | s.files = Dir["{app,lib}/**/*", "LICENSE.txt", "Rakefile", "README.md"] 17 | 18 | s.add_dependency "config" 19 | s.add_dependency "jbuilder", "~> 2.5.0" 20 | 21 | s.add_development_dependency "codeclimate-test-reporter", "~> 1.0.0" 22 | s.add_development_dependency "simplecov" 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['CI'] 2 | require 'simplecov' 3 | SimpleCov.start 4 | end 5 | 6 | require 'manageiq-api' 7 | 8 | Dir[ManageIQ::Api::Engine.root.join("spec/support/**/*.rb")].each { |f| require f } 9 | Dir[Rails.root.join("spec/shared/**/*.rb")].each { |f| require f } 10 | 11 | RSpec.configure do |config| 12 | config.include Spec::Support::Api::Helpers, :type => :request 13 | config.include Spec::Support::Api::RequestHelpers, :type => :request 14 | config.define_derived_metadata(:type => :request) do |metadata| 15 | metadata[:aggregate_failures] = true 16 | end 17 | 18 | #TODO: Remove the conditional once the repo is split and the test checkout of manageiq 19 | # is guaranteed to no longer 'also' call init_api_spec_env. 20 | config.before(:each, :type => :request) { init_api_spec_env unless User.count > 0 } 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/policy_profiles.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module PolicyProfiles 4 | def policy_profiles_query_resource(object) 5 | policy_profile_klass = collection_class(:policy_profiles) 6 | object ? object.get_policies.select { |p| p.kind_of?(policy_profile_klass) } : {} 7 | end 8 | 9 | def policy_profiles_assign_resource(object, _type, id = nil, data = nil) 10 | policy_assign_action(object, :policy_profiles, id, data) 11 | end 12 | 13 | def policy_profiles_unassign_resource(object, _type, id = nil, data = nil) 14 | policy_unassign_action(object, :policy_profiles, id, data) 15 | end 16 | 17 | def policy_profiles_resolve_resource(object, _type, id = nil, data = nil) 18 | policy_resolve_action(object, :policy_profiles, id, data) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/services/api/request_parser.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RequestParser 3 | def self.parse_user(data) 4 | user_name = Hash(data).fetch_path("requester", "user_name") 5 | return if user_name.blank? 6 | user = User.lookup_by_identity(user_name) 7 | raise BadRequestError, "Unknown requester user_name #{user_name} specified" unless user 8 | user 9 | end 10 | 11 | def self.parse_options(data) 12 | raise BadRequestError, "Request is missing options" if data["options"].blank? 13 | data["options"].symbolize_keys 14 | end 15 | 16 | def self.parse_auto_approve(data) 17 | case data["auto_approve"] 18 | when TrueClass, "true" then true 19 | when FalseClass, "false", nil then false 20 | else raise BadRequestError, "Invalid requester auto_approve value #{data["auto_approve"]} specified" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/controllers/api/conditions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ConditionsController < BaseController 3 | def create_resource(type, id, data = {}) 4 | assert_id_not_specified(data, type) 5 | begin 6 | data["expression"] = MiqExpression.new(data["expression"]) if data["expression"] 7 | super(type, id, data) 8 | rescue => err 9 | raise BadRequestError, "Failed to create a new condition - #{err}" 10 | end 11 | end 12 | 13 | def edit_resource(type, id = nil, data = {}) 14 | raise BadRequestError, "Must specify an id for editing a #{type} resource" unless id 15 | begin 16 | data["expression"] = MiqExpression.new(data["expression"]) if data["expression"] 17 | super(type, id, data) 18 | rescue => err 19 | raise BadRequestError, "Failed to update condition - #{err}" 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/controllers/api/alert_definition_profiles_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AlertDefinitionProfilesController < BaseController 3 | include Subcollections::AlertDefinitions 4 | 5 | REQUIRED_FIELDS = %w(description mode).freeze 6 | 7 | def create_resource(type, id, data = {}) 8 | assert_all_required_fields_exists(data, type, REQUIRED_FIELDS) 9 | begin 10 | super(type, id, data) 11 | rescue => err 12 | raise BadRequestError, "Failed to create a new alert definition profile - #{err}" 13 | end 14 | end 15 | 16 | def edit_resource(type, id = nil, data = {}) 17 | raise BadRequestError, "Must specify an id for editing a #{type} resource" unless id 18 | begin 19 | super(type, id, data) 20 | rescue => err 21 | raise BadRequestError, "Failed to update alert definition profile - #{err}" 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/alert_actions.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module AlertActions 4 | def alert_actions_query_resource(object) 5 | object.miq_alert_status_actions 6 | end 7 | 8 | def alert_actions_create_resource(object, type, _id, data) 9 | attributes = data.dup 10 | attributes['miq_alert_status_id'] = object.id 11 | attributes['user_id'] = User.current_user.id 12 | if data.key?('assignee') 13 | attributes['assignee_id'] = parse_id(attributes.delete('assignee'), :users) 14 | end 15 | alert_action = collection_class(type).create(attributes) 16 | if alert_action.invalid? 17 | raise BadRequestError, 18 | "Failed to add a new alert action resource - #{alert_action.errors.full_messages.join(', ')}" 19 | end 20 | alert_action 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/api/utils.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Utils 3 | def self.build_href_slug(klass, id) 4 | return unless id 5 | collection = Api::CollectionConfig.new.name_for_subclass(klass) 6 | "#{collection}/#{ApplicationRecord.compress_id(id)}" if collection 7 | end 8 | 9 | def self.resource_search_by_href_slug(href_slug, user = User.current_user) 10 | return unless href_slug 11 | 12 | collection, id = href_slug.split('/') 13 | collection_config = Api::CollectionConfig.new if collection 14 | 15 | raise _("Invalid href_slug %{href_slug} specified") % {:href_slug => href_slug} unless collection && id && collection_config.collection?(collection) 16 | raise _("User must be defined") unless user 17 | 18 | klass = collection_config.klass(collection) 19 | Rbac.filtered_object(klass.find(ApplicationRecord.uncompress_id(id)), :user => user, :class => klass) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/lib/api/collection_config_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Api::CollectionConfig do 2 | describe "#name_for_klass" do 3 | it "returns the corresponding name for classes managed by the API" do 4 | expect(subject.name_for_klass(Vm)).to eq(:vms) 5 | end 6 | 7 | it "returns nil for classes unknown to the API" do 8 | expect(subject.name_for_klass(String)).to be_nil 9 | end 10 | end 11 | 12 | describe "#name_for_subclass" do 13 | it "returns the collection name for classes declared by the API" do 14 | expect(subject.name_for_subclass(Vm)).to eq(:vms) 15 | end 16 | 17 | it "returns the collection name for classes that are accessible via collection_class" do 18 | expect(subject.name_for_subclass(ManageIQ::Providers::Vmware::InfraManager::Vm)).to eq(:vms) 19 | end 20 | 21 | it "returns nil for classes unknown to the API" do 22 | expect(subject.name_for_subclass(String)).to be_nil 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/api/container_deployments_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ContainerDeploymentsController < BaseController 3 | def create_resource(_type, _id, data) 4 | deployment = ContainerDeployment.new 5 | deployment.create_deployment(data, User.current_user) 6 | end 7 | 8 | def options 9 | # TODO: this service is rendering resources which (a) require 10 | # authentication (problematic for CORS compatibility), and (b) 11 | # are not being properly filtered by RBAC 12 | if authentication_requested? 13 | require_api_user_or_token 14 | render_options(:container_deployments, ContainerDeploymentService.new.all_data) 15 | else 16 | super 17 | end 18 | end 19 | 20 | private 21 | 22 | def authentication_requested? 23 | [HttpHeaders::MIQ_TOKEN, HttpHeaders::AUTH_TOKEN, "HTTP_AUTHORIZATION"].any? do |header| 24 | request.headers.include?(header) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/requests/firmwares_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "firmwares API" do 2 | describe "display firmware details" do 3 | context "with a valid role" do 4 | it "shows its properties" do 5 | fw = FactoryGirl.create(:firmware, 6 | :name => "UEFI", 7 | :version => "D7E152CUS-2.11") 8 | 9 | api_basic_authorize action_identifier(:firmwares, :read, :resource_actions, :get) 10 | 11 | get(api_firmware_url(nil, fw)) 12 | 13 | expect_single_resource_query("name" => "UEFI", 14 | "version" => "D7E152CUS-2.11") 15 | end 16 | end 17 | 18 | context "with an invalid role" do 19 | it "fails to show its properties" do 20 | fw = FactoryGirl.create(:firmware) 21 | 22 | api_basic_authorize 23 | 24 | get(api_firmware_url(nil, fw)) 25 | 26 | expect(response).to have_http_status(:forbidden) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/service_dialogs.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module ServiceDialogs 4 | def service_dialogs_query_resource(object) 5 | object ? object.dialogs : [] 6 | end 7 | 8 | # 9 | # Virtual attribute accessors 10 | # 11 | def fetch_service_dialogs_content(resource) 12 | case @req.collection 13 | when "service_templates" 14 | service_template = parent_resource_obj 15 | when "services" 16 | service_template = parent_resource_obj.service_template 17 | end 18 | return resource.content if service_template.nil? 19 | resource_action = service_template.resource_actions.where(:dialog_id => resource.id).first 20 | if resource_action.nil? 21 | raise BadRequestError, 22 | "#{service_dialog_ident(resource)} is not referenced by #{service_template_ident(service_template)}" 23 | end 24 | resource.content(service_template, resource_action, true) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - ".git/" 4 | - "**.xml" 5 | - "**.yaml" 6 | - "**.yml" 7 | - "locale/" 8 | - "spec/" 9 | - "tools/" 10 | engines: 11 | brakeman: 12 | # very slow :sad_panda: 13 | enabled: false 14 | bundler-audit: 15 | # requires Gemfile.lock 16 | enabled: false 17 | csslint: 18 | enabled: false 19 | duplication: 20 | enabled: true 21 | config: 22 | languages: 23 | - ruby 24 | - javascript 25 | eslint: 26 | enabled: false 27 | channel: "eslint-3" 28 | fixme: 29 | # let's enable later 30 | enabled: false 31 | markdownlint: 32 | # let's enable later 33 | enabled: false 34 | rubocop: 35 | enabled: true 36 | config: '.rubocop_cc.yml' 37 | prepare: 38 | fetch: 39 | - url: "https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_base.yml" 40 | path: ".rubocop_base.yml" 41 | - url: "https://raw.githubusercontent.com/ManageIQ/guides/master/.rubocop_cc_base.yml" 42 | path: ".rubocop_cc_base.yml" 43 | ratings: 44 | paths: 45 | - Gemfile.lock 46 | - "**.rake" 47 | - "**.rb" 48 | -------------------------------------------------------------------------------- /lib/manageiq/api/engine.rb: -------------------------------------------------------------------------------- 1 | require 'rails/engine' 2 | module ManageIQ 3 | module Api 4 | class Engine < ::Rails::Engine 5 | isolate_namespace ManageIQ::Api 6 | config.autoload_paths << root.join('app', 'controllers', 'api').expand_path 7 | config.autoload_paths << root.join('lib', 'services').expand_path 8 | config.autoload_paths << root.join('lib').expand_path 9 | 10 | config.after_initialize do 11 | $api_log.info("Initializing Environment for #{::Api::ApiConfig.base[:name]}") 12 | $api_log.info("") 13 | $api_log.info("Static Configuration") 14 | ::Api::ApiConfig.base.each { |key, val| log_kv(key, val) } 15 | 16 | $api_log.info("") 17 | $api_log.info("Dynamic Configuration") 18 | ::Api::Environment.user_token_service.api_config.each { |key, val| log_kv(key, val) } 19 | end 20 | 21 | def self.log_kv(key, val) 22 | $api_log.info(" #{key.to_s.ljust([24, key.to_s.length].max, ' ')}: #{val}") 23 | end 24 | 25 | def vmdb_plugin? 26 | true 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/requests/container_deployments_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Container Deployments API" do 2 | it "supports OPTIONS requests without authorization" do 3 | options api_container_deployments_url 4 | expect(response).to have_http_status(:ok) 5 | expect(response.parsed_body["data"]).to be_empty 6 | end 7 | 8 | it "supports collect-data with OPTIONS" do 9 | allow_any_instance_of(ContainerDeploymentService).to receive(:cloud_init_template_id).and_return(1) 10 | api_basic_authorize 11 | options api_container_deployments_url 12 | expect(response).to have_http_status(:ok) 13 | expect_hash_to_have_only_keys(response.parsed_body["data"], %w(deployment_types providers provision)) 14 | end 15 | 16 | it "creates container deployment with POST" do 17 | allow_any_instance_of(ContainerDeployment).to receive(:create_deployment).and_return(true) 18 | api_basic_authorize collection_action_identifier(:container_deployments, :create) 19 | post(api_container_deployments_url, :params => gen_request(:create, :example_data => true)) 20 | expect(response).to have_http_status(:ok) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/services/api/metric_rollups_service.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class MetricRollupsService 3 | REQUIRED_PARAMS = %w(resource_type capture_interval start_date).freeze 4 | 5 | attr_reader :params 6 | 7 | def initialize(params) 8 | @params = params 9 | validate_required_params 10 | validate_capture_interval 11 | end 12 | 13 | def query_metric_rollups 14 | start_date = params[:start_date].to_date 15 | end_date = params[:end_date].try(:to_date) 16 | 17 | MetricRollup.rollups_in_range(params[:resource_type], params[:resource_ids], params[:capture_interval], start_date, end_date) 18 | end 19 | 20 | private 21 | 22 | def validate_required_params 23 | REQUIRED_PARAMS.each do |key| 24 | raise BadRequestError, "Must specify #{REQUIRED_PARAMS.join(', ')}" unless params[key.to_sym] 25 | end 26 | end 27 | 28 | def validate_capture_interval 29 | unless MetricRollup::CAPTURE_INTERVAL_NAMES.include?(params[:capture_interval]) 30 | raise BadRequestError, "Capture interval must be one of #{MetricRollup::CAPTURE_INTERVAL_NAMES.join(', ')}" 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/api/auth_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AuthController < BaseController 3 | skip_before_action :validate_api_action 4 | 5 | def show 6 | requester_type = fetch_and_validate_requester_type 7 | token_service = Environment.user_token_service 8 | auth_token = token_service.generate_token(User.current_user.userid, requester_type) 9 | token_info = token_service.token_mgr(requester_type).token_get_info(auth_token) 10 | res = { 11 | :auth_token => auth_token, 12 | :token_ttl => token_info[:token_ttl], 13 | :expires_on => token_info[:expires_on], 14 | } 15 | render_resource :auth, res 16 | end 17 | 18 | def destroy 19 | api_token_mgr.invalidate_token(request.headers[HttpHeaders::AUTH_TOKEN]) 20 | 21 | render_normal_destroy 22 | end 23 | 24 | private 25 | 26 | def fetch_and_validate_requester_type 27 | requester_type = params['requester_type'] || 'api' 28 | Environment.user_token_service.validate_requester_type(requester_type) 29 | requester_type 30 | rescue => err 31 | raise BadRequestError, err.to_s 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller/action.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class BaseController 3 | module Action 4 | private 5 | 6 | def api_action(type, id, options = {}) 7 | klass = collection_class(type) 8 | 9 | result = yield(klass) if block_given? 10 | 11 | add_href_to_result(result, type, id) unless options[:skip_href] 12 | log_result(result) 13 | result 14 | end 15 | 16 | def queue_object_action(object, summary, options) 17 | task_options = { 18 | :action => summary, 19 | :userid => User.current_user.userid 20 | } 21 | 22 | queue_options = { 23 | :class_name => options[:class_name] || object.class.name, 24 | :method_name => options[:method_name], 25 | :instance_id => object.id, 26 | :args => options[:args] || [], 27 | :role => options[:role] || nil, 28 | } 29 | 30 | queue_options[:zone] = object.my_zone if %w(ems_operations smartstate).include?(options[:role]) 31 | 32 | MiqTask.generic_action_with_callback(task_options, queue_options) 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/controllers/api/service_templates_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ServiceTemplatesController < BaseController 3 | include Subcollections::ServiceDialogs 4 | include Subcollections::Tags 5 | include Subcollections::ResourceActions 6 | include Subcollections::ServiceRequests 7 | 8 | before_action :set_additional_attributes, :only => [:show] 9 | 10 | def create_resource(_type, _id, data) 11 | catalog_item_type = ServiceTemplate.class_from_request_data(data) 12 | catalog_item_type.create_catalog_item(data.deep_symbolize_keys, User.current_user.userid) 13 | rescue => err 14 | raise BadRequestError, "Could not create Service Template - #{err}" 15 | end 16 | 17 | def edit_resource(type, id, data) 18 | catalog_item = resource_search(id, type, collection_class(:service_templates)) 19 | catalog_item.update_catalog_item(data.deep_symbolize_keys, User.current_user.userid) 20 | rescue => err 21 | raise BadRequestError, "Could not update Service Template - #{err}" 22 | end 23 | 24 | private 25 | 26 | def set_additional_attributes 27 | @additional_attributes = %w(config_info) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/requests/regions_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # REST API Request Tests - Regions 3 | # 4 | # Regions primary collections: 5 | # /api/regions 6 | # 7 | # Tests for: 8 | # GET /api/regions/:id 9 | # 10 | 11 | describe "Regions API" do 12 | it "forbids access to regions without an appropriate role" do 13 | api_basic_authorize 14 | 15 | get(api_regions_url) 16 | 17 | expect(response).to have_http_status(:forbidden) 18 | end 19 | 20 | it "forbids access to a region resource without an appropriate role" do 21 | api_basic_authorize 22 | 23 | region = FactoryGirl.create(:miq_region, :region => "2") 24 | 25 | get(api_region_url(nil, region)) 26 | 27 | expect(response).to have_http_status(:forbidden) 28 | end 29 | 30 | it "allows GETs of a region" do 31 | api_basic_authorize action_identifier(:regions, :read, :resource_actions, :get) 32 | 33 | region = FactoryGirl.create(:miq_region, :region => "2") 34 | 35 | get(api_region_url(nil, region)) 36 | 37 | expect(response).to have_http_status(:ok) 38 | expect(response.parsed_body).to include( 39 | "href" => api_region_url(nil, region), 40 | "id" => region.id.to_s 41 | ) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/api/hosts_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class HostsController < BaseController 3 | CREDENTIALS_ATTR = "credentials".freeze 4 | AUTH_TYPE_ATTR = "auth_type".freeze 5 | DEFAULT_AUTH_TYPE = "default".freeze 6 | 7 | include Subcollections::Policies 8 | include Subcollections::PolicyProfiles 9 | include Subcollections::Tags 10 | 11 | def edit_resource(type, id, data = {}) 12 | credentials = data.delete(CREDENTIALS_ATTR) 13 | raise BadRequestError, "Cannot update non-credentials attributes of host resource" if data.any? 14 | resource_search(id, type, collection_class(:hosts)).tap do |host| 15 | all_credentials = Array.wrap(credentials).each_with_object({}) do |creds, hash| 16 | auth_type = creds.delete(AUTH_TYPE_ATTR) || DEFAULT_AUTH_TYPE 17 | creds.reverse_merge!(:userid => host.authentication_userid(auth_type)) 18 | hash[auth_type.to_sym] = creds.symbolize_keys! 19 | end 20 | host.update_authentication(all_credentials) if all_credentials.present? 21 | end 22 | end 23 | 24 | def options 25 | render_options(:hosts, :node_types => Host.node_types) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/controllers/api/settings_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class SettingsController < BaseController 3 | def index 4 | render_resource :settings, whitelist_settings(settings_hash) 5 | end 6 | 7 | def show 8 | settings_value = entry_value(whitelist_settings(settings_hash), @req.c_suffix) 9 | 10 | raise NotFoundError, "Settings entry #{@req.c_suffix} not found" if settings_value.nil? 11 | render_resource :settings, settings_entry_to_hash(@req.c_suffix, settings_value) 12 | end 13 | 14 | private 15 | 16 | def whitelist_settings(settings) 17 | result_hash = {} 18 | ApiConfig.collections[:settings][:categories].each do |category_path| 19 | result_hash.deep_merge!(settings_entry_to_hash(category_path, entry_value(settings, category_path))) 20 | end 21 | result_hash 22 | end 23 | 24 | def settings_hash 25 | @settings_hash ||= Settings.to_hash.deep_stringify_keys 26 | end 27 | 28 | def entry_value(settings, path) 29 | settings.fetch_path(path.split('/')) 30 | end 31 | 32 | def settings_entry_to_hash(path, value) 33 | {}.tap { |h| h.store_path(path.split("/"), value) } 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/support/api/request_helpers.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Style/HashSyntax 2 | module Spec 3 | module Support 4 | module Api 5 | module RequestHelpers 6 | def get(path, **args) 7 | process(:get, path, **args) 8 | end 9 | 10 | def post(path, **args) 11 | process(:post, path, **args) 12 | end 13 | 14 | def patch(path, **args) 15 | process(:patch, path, **args) 16 | end 17 | 18 | def put(path, **args) 19 | process(:put, path, **args) 20 | end 21 | 22 | def delete(path, **args) 23 | process(:delete, path, **args) 24 | end 25 | 26 | def options(path, **args) 27 | process(:options, path, **args) 28 | end 29 | 30 | def process(method, path, params: nil, headers: nil, env: nil, xhr: false, as: nil) 31 | headers = request_headers.merge(Hash(headers)) 32 | as ||= :json if [:post, :put, :patch].include?(method) 33 | super(method, path, params: params, headers: headers, env: env, xhr: xhr, as: as) 34 | end 35 | 36 | def request_headers 37 | @request_headers ||= {} 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/requests/guest_devices_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "guest devices API" do 2 | describe "display guest device details" do 3 | context "with the user authorized" do 4 | it "responds with device properties" do 5 | device = FactoryGirl.create(:guest_device, 6 | :device_name => "Broadcom 2-port 1GbE NIC Card", 7 | :device_type => "ethernet", 8 | :location => "Bay 7") 9 | 10 | api_basic_authorize action_identifier(:guest_devices, :read, :resource_actions, :get) 11 | 12 | get(api_guest_device_url(nil, device)) 13 | 14 | expect_single_resource_query("device_name" => "Broadcom 2-port 1GbE NIC Card", 15 | "device_type" => "ethernet", 16 | "location" => "Bay 7") 17 | end 18 | end 19 | 20 | context "with the user unauthorized" do 21 | it "responds with a forbidden status" do 22 | device = FactoryGirl.create(:guest_device) 23 | 24 | api_basic_authorize 25 | 26 | get(api_guest_device_url(nil, device)) 27 | 28 | expect(response).to have_http_status(:forbidden) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/controllers/api/alert_definitions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AlertDefinitionsController < BaseController 3 | REQUIRED_FIELDS = %w(description db expression options).freeze 4 | 5 | before_action :set_additional_attributes 6 | 7 | def create_resource(type, id, data = {}) 8 | assert_id_not_specified(data, type) 9 | assert_all_required_fields_exists(data, type, REQUIRED_FIELDS) 10 | begin 11 | data["expression"] = MiqExpression.new(data["expression"]) 12 | data["enabled"] = true if data["enabled"].nil? 13 | super(type, id, data).serializable_hash.merge("expression" => data["expression"]) 14 | rescue => err 15 | raise BadRequestError, "Failed to create a new alert definition - #{err}" 16 | end 17 | end 18 | 19 | def edit_resource(type, id = nil, data = {}) 20 | raise BadRequestError, "Must specify an id for editing a #{type} resource" unless id 21 | begin 22 | data["expression"] = MiqExpression.new(data["expression"]) if data["expression"] 23 | super(type, id, data) 24 | rescue => err 25 | raise BadRequestError, "Failed to update alert definition - #{err}" 26 | end 27 | end 28 | 29 | private 30 | 31 | def set_additional_attributes 32 | @additional_attributes = %w(expression) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/services/api/options_serializer.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class OptionsSerializer 3 | attr_reader :klass, :data, :config 4 | 5 | def initialize(klass, data = {}) 6 | @klass = klass 7 | @data = data 8 | @config = CollectionConfig.new 9 | end 10 | 11 | def serialize 12 | { 13 | :attributes => attributes, 14 | :virtual_attributes => virtual_attributes, 15 | :relationships => relationships, 16 | :subcollections => subcollections, 17 | :data => data 18 | } 19 | end 20 | 21 | private 22 | 23 | def attributes 24 | return [] unless klass 25 | options_attribute_list(klass.attribute_names - klass.virtual_attribute_names) 26 | end 27 | 28 | def virtual_attributes 29 | return [] unless klass 30 | options_attribute_list(klass.virtual_attribute_names) 31 | end 32 | 33 | def relationships 34 | return [] unless klass 35 | (klass.reflections.keys | klass.virtual_reflections.keys.collect(&:to_s)).sort 36 | end 37 | 38 | def subcollections 39 | return [] unless klass 40 | Array(config[config.name_for_klass(klass)].subcollections).sort 41 | end 42 | 43 | def options_attribute_list(attrlist) 44 | attrlist.sort.select { |attr| !Api.encrypted_attribute?(attr) } 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ManageIQ::Api 2 | Short description and motivation. 3 | 4 | [![Gem Version](https://badge.fury.io/rb/manageiq-api.svg)](http://badge.fury.io/rb/manageiq-api) 5 | [![Build Status](https://travis-ci.org/ManageIQ/manageiq-api.svg)](https://travis-ci.org/ManageIQ/manageiq-api) 6 | [![Code Climate](https://codeclimate.com/github/ManageIQ/manageiq-api.svg)](https://codeclimate.com/github/ManageIQ/manageiq-api) 7 | [![Test Coverage](https://codeclimate.com/github/ManageIQ/manageiq-api/badges/coverage.svg)](https://codeclimate.com/github/ManageIQ/manageiq-api/coverage) 8 | [![Dependency Status](https://gemnasium.com/ManageIQ/manageiq-api.svg)](https://gemnasium.com/ManageIQ/manageiq-api) 9 | [![Security](https://hakiri.io/github/ManageIQ/manageiq-api/master.svg)](https://hakiri.io/github/ManageIQ/manageiq-api/master) 10 | 11 | [![Chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ManageIQ/api?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 12 | 13 | ## Usage 14 | How to use my plugin. 15 | 16 | ## Installation 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'manageiq-api' 21 | ``` 22 | 23 | And then execute: 24 | ```bash 25 | $ bundle 26 | ``` 27 | 28 | Or install it yourself as: 29 | ```bash 30 | $ gem install manageiq-api 31 | ``` 32 | 33 | ## Contributing 34 | Contribution directions go here. 35 | 36 | ## License 37 | 38 | See [LICENSE.txt](LICENSE.txt). 39 | -------------------------------------------------------------------------------- /app/controllers/api/tenants_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class TenantsController < BaseController 3 | INVALID_TENANT_ATTRS = %w(id href ancestry).freeze 4 | 5 | include Subcollections::Tags 6 | include Subcollections::Quotas 7 | 8 | def create_resource(_type, _id, data) 9 | bad_attrs = data_includes_invalid_attrs(data) 10 | if bad_attrs.present? 11 | raise BadRequestError, 12 | "Attribute(s) #{bad_attrs} should not be specified for creating a new tenant resource" 13 | end 14 | parse_set_parent(data) 15 | tenant = Tenant.create(data) 16 | if tenant.invalid? 17 | raise BadRequestError, "Failed to add a new tenant resource - #{tenant.errors.full_messages.join(', ')}" 18 | end 19 | tenant 20 | end 21 | 22 | def edit_resource(type, id, data) 23 | bad_attrs = data_includes_invalid_attrs(data) 24 | if bad_attrs.present? 25 | raise BadRequestError, "Attributes #{bad_attrs} should not be specified for updating a tenant resource" 26 | end 27 | parse_set_parent(data) 28 | super 29 | end 30 | 31 | private 32 | 33 | def parse_set_parent(data) 34 | parent = parse_fetch_tenant(data.delete("parent")) 35 | data["parent"] = parent if parent 36 | end 37 | 38 | def data_includes_invalid_attrs(data) 39 | data.keys.select { |k| INVALID_TENANT_ATTRS.include?(k) }.compact.join(", ") if data 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/requests/versioning_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # REST API Request Tests - /api versioning 3 | # 4 | describe "Versioning" do 5 | context "Versioning Queries" do 6 | it "test versioning query" do 7 | api_basic_authorize 8 | 9 | get api_entrypoint_url 10 | 11 | expect(response).to have_http_status(:ok) 12 | expect_result_to_have_keys(%w(name description version versions collections)) 13 | end 14 | 15 | it "test query with a valid version" do 16 | api_basic_authorize 17 | 18 | # Let's get the versions 19 | get api_entrypoint_url 20 | 21 | expect(response).to have_http_status(:ok) 22 | expect_result_to_have_keys(%w(versions)) 23 | 24 | versions = response.parsed_body["versions"] 25 | 26 | # Let's get the first version identifier 27 | expect(versions).to_not be_nil 28 | expect(versions[0]).to_not be_nil 29 | 30 | ver = versions[0] 31 | expect(ver).to have_key("href") 32 | 33 | ident = ver["href"].split("/").last 34 | 35 | # Let's try to access that version API URL 36 | get api_entrypoint_url(ident) 37 | 38 | expect(response).to have_http_status(:ok) 39 | expect_result_to_have_keys(%w(name description version versions collections)) 40 | end 41 | 42 | it "test query with an invalid version" do 43 | api_basic_authorize 44 | 45 | get api_entrypoint_url("v9999.9999") 46 | 47 | expect(response).to have_http_status(:bad_request) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/api/environment.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class Environment 3 | def self.url_attributes 4 | @url_attributes ||= Set.new(%w(href)) 5 | end 6 | 7 | def self.resource_attributes 8 | @resource_attributes ||= Set.new(%w(image_href)) 9 | end 10 | 11 | def self.encrypted_attributes 12 | @encrypted_attributes ||= Set.new(%w(password)) | 13 | ::MiqRequestWorkflow.all_encrypted_options_fields.map(&:to_s) | 14 | ::Vmdb::Settings::PASSWORD_FIELDS.map(&:to_s) 15 | end 16 | 17 | def self.time_attributes 18 | @time_attributes ||= ApiConfig.collections.each.with_object(Set.new(%w(expires_on))) do |(_, cspec), result| 19 | next if cspec[:klass].blank? 20 | klass = cspec[:klass].constantize 21 | klass.columns_hash.each do |name, typeobj| 22 | result << name if %w(date datetime).include?(typeobj.type.to_s) 23 | end 24 | end 25 | end 26 | 27 | def self.user_token_service 28 | @user_token_service ||= UserTokenService.new(ApiConfig, :log_init => true) 29 | end 30 | 31 | def self.fetch_encrypted_attribute_names(klass) 32 | return [] unless klass.respond_to?(:encrypted_columns) 33 | return if encrypted_objects_checked.include?(klass.name) 34 | encrypted_attributes.merge(klass.encrypted_columns) 35 | encrypted_objects_checked << klass.name 36 | end 37 | 38 | def self.encrypted_objects_checked 39 | @encrypted_objects_checked ||= Set.new 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/quotas.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Quotas 4 | INVALID_QUOTA_ATTRS = %w(id href tenant_id unit).freeze 5 | 6 | def quotas_create_resource(object, type, _id, data) 7 | bad_attrs = data.keys & INVALID_QUOTA_ATTRS 8 | errmsg = "Attributes %s should not be specified for creating a new tenant quota resource" 9 | raise(BadRequestError, errmsg % bad_attrs.join(", ")) if bad_attrs.any? 10 | 11 | data['tenant_id'] = object.id 12 | quota = collection_class(type).create(data) 13 | if quota.invalid? 14 | raise BadRequestError, "Failed to add a new tenant quota resource - #{quota.errors.full_messages.join(', ')}" 15 | end 16 | quota 17 | end 18 | 19 | def quotas_query_resource(object) 20 | klass = collection_class(:quotas) 21 | object ? klass.where(:tenant_id => object.id) : {} 22 | end 23 | 24 | def quotas_edit_resource(_object, type, id, data) 25 | bad_attrs = data.keys & INVALID_QUOTA_ATTRS 26 | errmsg = "Attributes %s should not be specified for updating a quota resource" 27 | raise(BadRequestError, errmsg % bad_attrs.join(", ")) if bad_attrs.any? 28 | 29 | edit_resource(type, id, data) 30 | end 31 | 32 | def delete_resource_quotas(_object, type, id, data) 33 | delete_resource(type, id, data) 34 | end 35 | 36 | def quotas_delete_resource(_object, type, id, data) 37 | delete_resource(type, id, data) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/lib/api/api_config_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'API configuration (config/api.yml)' do 2 | let(:api_settings) { Api::ApiConfig } 3 | 4 | describe 'collections' do 5 | let(:collection_settings) { api_settings.collections } 6 | 7 | it "is sorted a-z" do 8 | actual = collection_settings.keys 9 | expected = collection_settings.keys.sort 10 | expect(actual).to eq(expected) 11 | end 12 | 13 | describe 'identifiers' do 14 | let(:miq_product_features) { MiqProductFeature.seed.values.flatten.to_set } 15 | let(:api_feature_identifiers) do 16 | collection_settings.each_with_object(Set.new) do |(_, cfg), set| 17 | set.add(cfg[:identifier]) if cfg[:identifier] 18 | keys = [:collection_actions, :resource_actions, :subcollection_actions, :subresource_actions] 19 | Array(cfg[:subcollections]).each do |s| 20 | keys << "#{s}_subcollection_actions" << "#{s}_subresource_actions" 21 | end 22 | keys.each do |action_type| 23 | next unless cfg[action_type] 24 | cfg[action_type].each do |_, method_cfg| 25 | method_cfg.each do |action_cfg| 26 | set.add(action_cfg[:identifier]) if action_cfg[:identifier] 27 | end 28 | end 29 | end 30 | end 31 | end 32 | 33 | it 'is not empty' do 34 | expect(api_feature_identifiers).not_to be_empty 35 | end 36 | 37 | it 'contains only valid miq_feature identifiers' do 38 | dangling = api_feature_identifiers - miq_product_features 39 | expect(dangling).to be_empty 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/api/automate_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AutomateController < BaseController 3 | def index 4 | resources = fetch_resources 5 | render_resource :automate, :name => "automate", :subcount => resources.count, :resources => resources 6 | end 7 | 8 | def show 9 | resources = fetch_resources(@req.c_suffix) 10 | render_resource :automate, :name => "automate", :subcount => resources.count, :resources => resources 11 | end 12 | 13 | private 14 | 15 | def fetch_resources(object_ref = nil) 16 | resources = ae_browser.search(object_ref, ae_search_options) 17 | resources.collect! { |resource| resource.slice(*attribute_params) } if attribute_params.present? 18 | resources 19 | rescue => err 20 | raise BadRequestError, err.to_s 21 | end 22 | 23 | def ae_browser 24 | @ae_browser ||= MiqAeBrowser.new(User.current_user) 25 | end 26 | 27 | def attribute_params 28 | @attribute_params ||= params['attributes'] ? %w(fqname) | params['attributes'].to_s.split(',') : nil 29 | end 30 | 31 | def ae_search_options 32 | # For /api/automate (discovering domains, scope is 1 if unspecified) 33 | # Otherwise, we default depth to 0 (current object), use -1 for unlimited depth search 34 | depth = if params[:depth] 35 | params[:depth] == "-1" ? nil : params[:depth].to_i 36 | else 37 | @req.c_suffix.blank? ? 1 : 0 38 | end 39 | search_options = {:depth => depth, :serialize => true} 40 | search_options[:state_machines] = true if search_option?(:state_machines) 41 | search_options 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/api.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | # Semantic Versioning Regex for API, i.e. vMajor.minor.patch[-pre] 3 | VERSION_REGEX = /v[\d]+(\.[\da-zA-Z]+)*(\-[\da-zA-Z]+)?/ 4 | 5 | VERBS_ACTIONS_MAP = { 6 | :get => "show", 7 | :post => "update", 8 | :put => "update", 9 | :patch => "update", 10 | :delete => "destroy", 11 | :options => "options" 12 | }.freeze 13 | 14 | ApiError = Class.new(StandardError) 15 | AuthenticationError = Class.new(ApiError) 16 | ForbiddenError = Class.new(ApiError) 17 | BadRequestError = Class.new(ApiError) 18 | NotFoundError = Class.new(ApiError) 19 | UnsupportedMediaTypeError = Class.new(ApiError) 20 | 21 | def self.encrypted_attribute?(attr) 22 | Environment.encrypted_attributes.include?(attr.to_s) || attr.to_s.include?('password') 23 | end 24 | 25 | def self.time_attribute?(attr) 26 | Environment.time_attributes.include?(attr.to_s) 27 | end 28 | 29 | def self.url_attribute?(attr) 30 | Environment.url_attributes.include?(attr.to_s) 31 | end 32 | 33 | def self.resource_attribute?(attr) 34 | Environment.resource_attributes.include?(attr.to_s) 35 | end 36 | 37 | # Since `ApplicationRecord.compressed_id?` returns truthy for 38 | # uncompressed ids (it should be more properly named `cid_or_id?` or 39 | # similar), we need a way to distinquish compressed ids from 40 | # anything else so that we can deprecate their usage. 41 | def self.compressed_id?(thing) 42 | !!(ApplicationRecord::RE_COMPRESSED_ID =~ thing.to_s) 43 | end 44 | 45 | def self.uncompress_id(id) 46 | $api_log.warn("The use of compressed ids is deprecated, and the support for which will be removed in a future release.") 47 | ApplicationRecord.uncompress_id(id) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/requests/cloud_subnets_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'CloudSubnets API' do 2 | describe 'GET /api/cloud_subnets' do 3 | it 'lists all cloud subnets with an appropriate role' do 4 | cloud_subnet = FactoryGirl.create(:cloud_subnet) 5 | api_basic_authorize collection_action_identifier(:cloud_subnets, :read, :get) 6 | get(api_cloud_subnets_url) 7 | 8 | expected = { 9 | 'count' => 1, 10 | 'subcount' => 1, 11 | 'name' => 'cloud_subnets', 12 | 'resources' => [ 13 | hash_including('href' => api_cloud_subnet_url(nil, cloud_subnet)) 14 | ] 15 | } 16 | expect(response).to have_http_status(:ok) 17 | expect(response.parsed_body).to include(expected) 18 | end 19 | 20 | it 'forbids access to cloud subnets without an appropriate role' do 21 | api_basic_authorize 22 | 23 | get(api_cloud_subnets_url) 24 | 25 | expect(response).to have_http_status(:forbidden) 26 | end 27 | end 28 | 29 | describe 'GET /api/cloud_subnets/:id' do 30 | it 'will show a cloud subnet with an appropriate role' do 31 | cloud_subnet = FactoryGirl.create(:cloud_subnet) 32 | api_basic_authorize action_identifier(:cloud_subnets, :read, :resource_actions, :get) 33 | 34 | get(api_cloud_subnet_url(nil, cloud_subnet)) 35 | 36 | expect(response.parsed_body).to include('href' => api_cloud_subnet_url(nil, cloud_subnet)) 37 | expect(response).to have_http_status(:ok) 38 | end 39 | 40 | it 'forbids access to a cloud tenant without an appropriate role' do 41 | cloud_subnet = FactoryGirl.create(:cloud_subnet) 42 | api_basic_authorize 43 | 44 | get(api_cloud_subnet_url(nil, cloud_subnet)) 45 | 46 | expect(response).to have_http_status(:forbidden) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/requests/cloud_tenants_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'CloudTenants API' do 2 | describe 'GET /api/cloud_tenants' do 3 | it 'lists all cloud tenants with an appropriate role' do 4 | cloud_tenant = FactoryGirl.create(:cloud_tenant) 5 | api_basic_authorize collection_action_identifier(:cloud_tenants, :read, :get) 6 | get(api_cloud_tenants_url) 7 | 8 | expected = { 9 | 'count' => 1, 10 | 'subcount' => 1, 11 | 'name' => 'cloud_tenants', 12 | 'resources' => [ 13 | hash_including('href' => api_cloud_tenant_url(nil, cloud_tenant)) 14 | ] 15 | } 16 | expect(response).to have_http_status(:ok) 17 | expect(response.parsed_body).to include(expected) 18 | end 19 | 20 | it 'forbids access to cloud tenants without an appropriate role' do 21 | api_basic_authorize 22 | 23 | get(api_cloud_tenants_url) 24 | 25 | expect(response).to have_http_status(:forbidden) 26 | end 27 | end 28 | 29 | describe 'GET /api/cloud_tenants/:id' do 30 | it 'will show a cloud tenant with an appropriate role' do 31 | cloud_tenant = FactoryGirl.create(:cloud_tenant) 32 | api_basic_authorize action_identifier(:cloud_tenants, :read, :resource_actions, :get) 33 | 34 | get(api_cloud_tenant_url(nil, cloud_tenant)) 35 | 36 | expect(response.parsed_body).to include('href' => api_cloud_tenant_url(nil, cloud_tenant)) 37 | expect(response).to have_http_status(:ok) 38 | end 39 | 40 | it 'forbids access to a cloud tenant without an appropriate role' do 41 | cloud_tenant = FactoryGirl.create(:cloud_tenant) 42 | api_basic_authorize 43 | 44 | get(api_cloud_tenant_url(nil, cloud_tenant)) 45 | 46 | expect(response).to have_http_status(:forbidden) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/requests/load_balancers_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'LoadBalancers API' do 2 | describe 'GET /api/load_balancers' do 3 | it 'lists all load balancers with an appropriate role' do 4 | load_balancer = FactoryGirl.create(:load_balancer) 5 | api_basic_authorize collection_action_identifier(:load_balancers, :read, :get) 6 | 7 | expected = { 8 | 'count' => 1, 9 | 'subcount' => 1, 10 | 'name' => 'load_balancers', 11 | 'resources' => [ 12 | hash_including('href' => api_load_balancer_url(nil, load_balancer)) 13 | ] 14 | } 15 | get(api_load_balancers_url) 16 | 17 | expect(response).to have_http_status(:ok) 18 | expect(response.parsed_body).to include(expected) 19 | end 20 | 21 | it 'forbids access to load balancers without an appropriate role' do 22 | api_basic_authorize 23 | 24 | get(api_load_balancers_url) 25 | 26 | expect(response).to have_http_status(:forbidden) 27 | end 28 | end 29 | 30 | describe 'GET /api/load_balancers/:id' do 31 | it 'will show a load balancer with an appropriate role' do 32 | load_balancer = FactoryGirl.create(:load_balancer) 33 | api_basic_authorize action_identifier(:load_balancers, :read, :resource_actions, :get) 34 | 35 | get(api_load_balancer_url(nil, load_balancer)) 36 | 37 | expect(response.parsed_body).to include('href' => api_load_balancer_url(nil, load_balancer)) 38 | expect(response).to have_http_status(:ok) 39 | end 40 | 41 | it 'forbids access to a load balancer without an appropriate role' do 42 | load_balancer = FactoryGirl.create(:load_balancer) 43 | api_basic_authorize 44 | 45 | get(api_load_balancer_url(nil, load_balancer)) 46 | 47 | expect(response).to have_http_status(:forbidden) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/vms.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Vms 4 | def vms_query_resource(object) 5 | vms = object.try(:vms) || [] 6 | 7 | vm_attrs = attribute_selection_for("vms") 8 | vm_decorators = decorator_selection_for("vms") 9 | 10 | return vms if vm_attrs.blank? && vm_decorators.blank? 11 | 12 | vms.collect do |vm| 13 | attributes_hash = create_vm_attributes_hash(vm_attrs, vm) 14 | decorators_hash = create_vm_decorators_hash(vm_decorators, vm) 15 | 16 | conflictless_hash = attributes_hash.merge(decorators_hash || {}) do |key, _, _| 17 | raise BadRequestError, "Requested both an attribute and a decorator of the same name: #{key}" 18 | end 19 | 20 | vm.as_json.merge(conflictless_hash) 21 | end 22 | end 23 | 24 | private 25 | 26 | def decorator_selection 27 | params['decorators'].to_s.split(",") 28 | end 29 | 30 | def decorator_selection_for(collection) 31 | decorator_selection.collect do |attr| 32 | /\A#{collection}\.(?.*)\z/.match(attr) { |m| m[:name] } 33 | end.compact 34 | end 35 | 36 | def create_vm_attributes_hash(vm_attrs, vm) 37 | vm_attrs.each_with_object({}) do |attr, hash| 38 | hash[attr] = vm.public_send(attr.to_sym) if vm.respond_to?(attr.to_sym) 39 | end.compact 40 | end 41 | 42 | def create_vm_decorators_hash(vm_decorators, vm) 43 | hash = {} 44 | if vm_decorators.include? 'supports_console?' 45 | hash['supports_console?'] = vm.supports_console? 46 | end 47 | if vm_decorators.include? 'supports_cockpit?' 48 | hash['supports_cockpit?'] = vm.supports_launch_cockpit? 49 | end 50 | hash 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/api/links_builder.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class LinksBuilder 3 | def initialize(params, href, counts) 4 | @offset = params["offset"].to_i if params["offset"] 5 | @limit = params["limit"].to_i if params["limit"] 6 | @href = href 7 | @counts = counts if counts 8 | validate_limit 9 | end 10 | 11 | def links 12 | { 13 | :self => self_href, 14 | :next => next_href, 15 | :previous => previous_href, 16 | :first => first_href, 17 | :last => last_href 18 | }.compact 19 | end 20 | 21 | def pages 22 | (paging_count / limit.to_f).ceil 23 | end 24 | 25 | def links? 26 | offset && limit 27 | end 28 | 29 | private 30 | 31 | attr_reader :offset, :limit, :href, :counts 32 | 33 | def validate_limit 34 | raise BadRequestError, "Limit must be greater than zero if offset is specified" if links? && limit.zero? 35 | end 36 | 37 | def self_href 38 | @self_href ||= format_href(offset) 39 | end 40 | 41 | def format_href(new_offset) 42 | if href.include?("offset") 43 | href.sub("offset=#{offset}", "offset=#{new_offset}") 44 | else 45 | href + "&offset=#{new_offset}" 46 | end 47 | end 48 | 49 | def paging_count 50 | @paging_count ||= counts.subquery_count || counts.count 51 | end 52 | 53 | def next_href 54 | next_offset = offset + limit 55 | return if next_offset >= paging_count 56 | format_href(next_offset) 57 | end 58 | 59 | def previous_href 60 | return if offset.zero? 61 | prev_offset = offset - limit 62 | return first_href if prev_offset < 0 63 | format_href(prev_offset) 64 | end 65 | 66 | def first_href 67 | format_href(0) 68 | end 69 | 70 | def last_href 71 | last_offset = paging_count - (paging_count % limit) 72 | format_href(last_offset) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/requests/custom_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Custom Attributes API" do 2 | describe "GET /api//:cid/custom_attributes/:sid" do 3 | it "renders the actions available on custom attribute members" do 4 | vm = FactoryGirl.create(:vm_vmware) 5 | custom_attribute = FactoryGirl.create(:custom_attribute, :resource => vm) 6 | api_basic_authorize 7 | 8 | get(api_vm_custom_attribute_url(nil, vm, custom_attribute)) 9 | 10 | expected = { 11 | "actions" => a_collection_including( 12 | a_hash_including("name" => "edit", "method" => "post"), 13 | a_hash_including("name" => "delete", "method" => "post"), 14 | a_hash_including("name" => "delete", "method" => "delete") 15 | ) 16 | } 17 | expect(response.parsed_body).to include(expected) 18 | end 19 | end 20 | 21 | it "can delete a custom attribute through its nested URI" do 22 | vm = FactoryGirl.create(:vm_vmware) 23 | custom_attribute = FactoryGirl.create(:custom_attribute, :resource => vm) 24 | api_basic_authorize 25 | 26 | expect do 27 | delete(api_vm_custom_attribute_url(nil, vm, custom_attribute)) 28 | end.to change(CustomAttribute, :count).by(-1) 29 | 30 | expect(response).to have_http_status(:no_content) 31 | end 32 | 33 | it 'returns the correct href' do 34 | provider = FactoryGirl.create(:ext_management_system) 35 | custom_attribute = FactoryGirl.create(:custom_attribute, :resource => provider, :name => 'foo', :value => 'bar') 36 | api_basic_authorize subcollection_action_identifier(:providers, :custom_attributes, :edit, :post) 37 | 38 | post(api_provider_custom_attribute_url(nil, provider, custom_attribute), :params => { :action => :edit, :name => 'name1' }) 39 | 40 | expect(response).to have_http_status(:ok) 41 | expect(response.parsed_body['href']).to include(api_provider_custom_attribute_url(nil, provider, custom_attribute)) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/requests/floating_ips_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'FloatingIp API' do 2 | describe 'GET /api/floating_ips' do 3 | it 'lists all cloud subnets with an appropriate role' do 4 | floating_ip = FactoryGirl.create(:floating_ip) 5 | api_basic_authorize collection_action_identifier(:floating_ips, :read, :get) 6 | get(api_floating_ips_url) 7 | 8 | expected = { 9 | 'count' => 1, 10 | 'subcount' => 1, 11 | 'name' => 'floating_ips', 12 | 'resources' => [ 13 | hash_including('href' => api_floating_ip_url(nil, floating_ip)) 14 | ] 15 | } 16 | expect(response).to have_http_status(:ok) 17 | expect(response.parsed_body).to include(expected) 18 | end 19 | 20 | it 'forbids access to cloud subnets without an appropriate role' do 21 | api_basic_authorize 22 | 23 | get(api_floating_ips_url) 24 | 25 | expect(response).to have_http_status(:forbidden) 26 | end 27 | end 28 | 29 | describe 'GET /api/floating_ips/:id' do 30 | it 'will show a cloud subnet with an appropriate role' do 31 | floating_ip = FactoryGirl.create(:floating_ip) 32 | api_basic_authorize action_identifier(:floating_ips, :read, :resource_actions, :get) 33 | 34 | get(api_floating_ip_url(nil, floating_ip)) 35 | 36 | expect(response.parsed_body).to include('href' => api_floating_ip_url(nil, floating_ip)) 37 | expect(response).to have_http_status(:ok) 38 | end 39 | 40 | it 'forbids access to a cloud tenant without an appropriate role' do 41 | floating_ip = FactoryGirl.create(:floating_ip) 42 | api_basic_authorize 43 | 44 | get(api_floating_ip_url(nil, floating_ip)) 45 | 46 | expect(response).to have_http_status(:forbidden) 47 | end 48 | end 49 | 50 | describe 'POST /api/floating_ips' do 51 | it 'forbids access to floating ips without an appropriate role' do 52 | api_basic_authorize 53 | post(api_floating_ips_url, :params => gen_request(:query, "")) 54 | expect(response).to have_http_status(:forbidden) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/requests/network_routers_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'NetworkRouters API' do 2 | describe 'GET /api/network_routers' do 3 | it 'lists all cloud subnets with an appropriate role' do 4 | network_router = FactoryGirl.create(:network_router) 5 | api_basic_authorize collection_action_identifier(:network_routers, :read, :get) 6 | get(api_network_routers_url) 7 | 8 | expected = { 9 | 'count' => 1, 10 | 'subcount' => 1, 11 | 'name' => 'network_routers', 12 | 'resources' => [ 13 | hash_including('href' => api_network_router_url(nil, network_router)) 14 | ] 15 | } 16 | expect(response).to have_http_status(:ok) 17 | expect(response.parsed_body).to include(expected) 18 | end 19 | 20 | it 'forbids access to cloud subnets without an appropriate role' do 21 | api_basic_authorize 22 | get(api_network_routers_url) 23 | expect(response).to have_http_status(:forbidden) 24 | end 25 | end 26 | 27 | describe 'GET /api/network_routers/:id' do 28 | it 'will show a cloud subnet with an appropriate role' do 29 | network_router = FactoryGirl.create(:network_router) 30 | api_basic_authorize action_identifier(:network_routers, :read, :resource_actions, :get) 31 | get(api_network_router_url(nil, network_router)) 32 | expect(response.parsed_body).to include('href' => api_network_router_url(nil, network_router)) 33 | expect(response).to have_http_status(:ok) 34 | end 35 | 36 | it 'forbids access to a cloud tenant without an appropriate role' do 37 | network_router = FactoryGirl.create(:network_router) 38 | api_basic_authorize 39 | get(api_network_router_url(nil, network_router)) 40 | expect(response).to have_http_status(:forbidden) 41 | end 42 | end 43 | 44 | describe 'POST /api/network_routers' do 45 | it 'forbids access to network routers without an appropriate role' do 46 | api_basic_authorize 47 | post(api_network_routers_url, :params => gen_request(:query, "")) 48 | expect(response).to have_http_status(:forbidden) 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/api/automation_requests_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AutomationRequestsController < BaseController 3 | include Subcollections::RequestTasks 4 | 5 | def create_resource(type, _id, data) 6 | assert_id_not_specified(data, type) 7 | 8 | version_str = data["version"] || "1.1" 9 | uri_parts = hash_fetch(data, "uri_parts") 10 | parameters = hash_fetch(data, "parameters") 11 | requester = hash_fetch(data, "requester") 12 | 13 | AutomationRequest.create_from_ws(version_str, User.current_user, uri_parts, parameters, requester) 14 | end 15 | 16 | def edit_resource(type, id, data) 17 | request = resource_search(id, type, collection_class(:automation_requests)) 18 | RequestEditor.edit(request, data) 19 | request 20 | end 21 | 22 | def approve_resource(type, id, data) 23 | raise "Must specify a reason for approving an automation request" unless data["reason"].present? 24 | api_action(type, id) do |klass| 25 | request = resource_search(id, type, klass) 26 | request.approve(User.current_user.userid, data["reason"]) 27 | action_result(true, "Automation request #{id} approved") 28 | end 29 | rescue => err 30 | action_result(false, err.to_s) 31 | end 32 | 33 | def deny_resource(type, id, data) 34 | raise "Must specify a reason for denying an automation request" unless data["reason"].present? 35 | api_action(type, id) do |klass| 36 | request = resource_search(id, type, klass) 37 | request.deny(User.current_user.userid, data["reason"]) 38 | action_result(true, "Automation request #{id} denied") 39 | end 40 | rescue => err 41 | action_result(false, err.to_s) 42 | end 43 | 44 | def find_automation_requests(id) 45 | klass = collection_class(:requests) 46 | return klass.find(id) if User.current_user.admin_user? 47 | klass.find_by!(:requester => User.current_user, :id => id) 48 | end 49 | 50 | def automation_requests_search_conditions 51 | return {} if User.current_user.admin_user? 52 | {:requester => User.current_user} 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/requests/middleware_domains_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Middleware Domains API' do 2 | let(:domain) { FactoryGirl.create :middleware_domain } 3 | 4 | describe '/' do 5 | it 'forbids access without an appropriate role' do 6 | api_basic_authorize 7 | 8 | get api_middleware_domains_url 9 | 10 | expect(response).to have_http_status(:forbidden) 11 | end 12 | 13 | it 'returns an empty listing of domains' do 14 | api_basic_authorize collection_action_identifier(:middleware_domains, :read, :get) 15 | 16 | get api_middleware_domains_url 17 | 18 | expect(response).to have_http_status(:ok) 19 | expect(response.parsed_body).to include( 20 | 'name' => 'middleware_domains', 21 | 'count' => 0, 22 | 'resources' => [], 23 | 'subcount' => 0 24 | ) 25 | end 26 | 27 | it 'returns a listing of domains' do 28 | domain 29 | 30 | api_basic_authorize collection_action_identifier(:middleware_domains, :read, :get) 31 | 32 | get api_middleware_domains_url 33 | 34 | expect(response).to have_http_status(:ok) 35 | expect(response.parsed_body).to include( 36 | 'name' => 'middleware_domains', 37 | 'count' => 1, 38 | 'resources' => [{ 39 | 'href' => api_middleware_domain_url(nil, domain) 40 | }], 41 | 'subcount' => 1 42 | ) 43 | end 44 | end 45 | 46 | describe '/:id' do 47 | it 'forbids access to a domain without an appropriate role' do 48 | api_basic_authorize 49 | 50 | get api_middleware_domain_url(nil, domain) 51 | 52 | expect(response).to have_http_status(:forbidden) 53 | end 54 | 55 | it 'returns the attributes of one domain' do 56 | api_basic_authorize action_identifier(:middleware_domains, :read, :resource_actions, :get) 57 | 58 | get api_middleware_domain_url(nil, domain) 59 | 60 | expect(response).to have_http_status(:ok) 61 | expect(response.parsed_body).to include( 62 | 'id' => domain.id.to_s, 63 | 'href' => api_middleware_domain_url(nil, domain), 64 | 'name' => domain.name 65 | ) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /app/controllers/api/physical_servers_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PhysicalServersController < BaseController 3 | def blink_loc_led_resource(type, id, _data) 4 | change_resource_state(:blink_loc_led, type, id) 5 | end 6 | 7 | def turn_on_loc_led_resource(type, id, _data) 8 | change_resource_state(:turn_on_loc_led, type, id) 9 | end 10 | 11 | def turn_off_loc_led_resource(type, id, _data) 12 | change_resource_state(:turn_off_loc_led, type, id) 13 | end 14 | 15 | def power_on_resource(type, id, _data) 16 | change_resource_state(:power_on, type, id) 17 | end 18 | 19 | def power_off_resource(type, id, _data) 20 | change_resource_state(:power_off, type, id) 21 | end 22 | 23 | def power_off_now_resource(type, id, _data) 24 | change_resource_state(:power_off_now, type, id) 25 | end 26 | 27 | def restart_resource(type, id, _data) 28 | change_resource_state(:restart, type, id) 29 | end 30 | 31 | def restart_now_resource(type, id, _data) 32 | change_resource_state(:restart_now, type, id) 33 | end 34 | 35 | def restart_to_sys_setup_resource(type, id, _data) 36 | change_resource_state(:restart_to_sys_setup, type, id) 37 | end 38 | 39 | def restart_mgmt_controller_resource(type, id, _data) 40 | change_resource_state(:restart_mgmt_controller, type, id) 41 | end 42 | 43 | private 44 | 45 | def change_resource_state(state, type, id) 46 | raise BadRequestError, "Must specify an id for changing a #{type} resource" unless id 47 | 48 | api_action(type, id) do |klass| 49 | begin 50 | server = resource_search(id, type, klass) 51 | desc = "Requested server state #{state} for #{server_ident(server)}" 52 | api_log_info(desc) 53 | task_id = queue_object_action(server, desc, :method_name => state, :role => :ems_operations) 54 | action_result(true, desc, :task_id => task_id) 55 | rescue => err 56 | action_result(false, err.to_s) 57 | end 58 | end 59 | end 60 | 61 | def server_ident(server) 62 | "Server instance: #{server.id} name:'#{server.name}'" 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/requests/middleware_messagings_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Middleware Messagings API' do 2 | let(:messaging) { FactoryGirl.create(:middleware_messaging) } 3 | 4 | describe '/' do 5 | it 'forbids access without an appropriate role' do 6 | api_basic_authorize 7 | 8 | get api_middleware_messagings_url 9 | 10 | expect(response).to have_http_status(:forbidden) 11 | end 12 | 13 | it 'returns an empty listing of messagings' do 14 | api_basic_authorize collection_action_identifier(:middleware_messagings, :read, :get) 15 | 16 | get api_middleware_messagings_url 17 | 18 | expect(response).to have_http_status(:ok) 19 | expect(response.parsed_body).to include( 20 | 'name' => 'middleware_messagings', 21 | 'count' => 0, 22 | 'resources' => [], 23 | 'subcount' => 0 24 | ) 25 | end 26 | 27 | it 'returns a a listing of messagings' do 28 | messaging 29 | 30 | api_basic_authorize collection_action_identifier(:middleware_messagings, :read, :get) 31 | 32 | get api_middleware_messagings_url 33 | 34 | expect(response).to have_http_status(:ok) 35 | expect(response.parsed_body).to include( 36 | 'name' => 'middleware_messagings', 37 | 'count' => 1, 38 | 'resources' => [{ 39 | 'href' => api_middleware_messaging_url(nil, messaging) 40 | }], 41 | 'subcount' => 1 42 | ) 43 | end 44 | end 45 | 46 | describe '/:id' do 47 | it 'forbids access to a messaging without an appropriate role' do 48 | api_basic_authorize 49 | 50 | get api_middleware_messaging_url(nil, messaging) 51 | 52 | expect(response).to have_http_status(:forbidden) 53 | end 54 | 55 | it 'returns the attributes of one messaging' do 56 | api_basic_authorize action_identifier(:middleware_messagings, :read, :resource_actions, :get) 57 | 58 | get api_middleware_messaging_url(nil, messaging) 59 | 60 | expect(response).to have_http_status(:ok) 61 | expect(response.parsed_body).to include( 62 | 'id' => messaging.id.to_s, 63 | 'href' => api_middleware_messaging_url(nil, messaging) 64 | ) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/controllers/api/automate_domains_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AutomateDomainsController < BaseController 3 | def refresh_from_source_resource(type, id = nil, data = nil) 4 | raise BadRequestError, "Must specify an id for refreshing a #{type} resource from source" unless id 5 | 6 | api_action(type, id) do |klass| 7 | domain = resource_search(id, type, klass) 8 | api_log_info("Refreshing #{automate_domain_ident(domain)}") 9 | 10 | begin 11 | unless GitBasedDomainImportService.available? 12 | raise "Git owner role is not enabled to be able to import git repositories" 13 | end 14 | raise "#{automate_domain_ident(domain)} did not originate from git repository" unless domain.git_repository 15 | ref = data["ref"] || domain.ref 16 | ref_type = data["ref_type"] || domain.ref_type 17 | 18 | description = "Refreshing #{automate_domain_ident(domain)} from git repository" 19 | task_id = GitBasedDomainImportService.new.queue_refresh_and_import(domain.git_repository.url, 20 | ref, 21 | ref_type, 22 | current_tenant.id) 23 | action_result(true, description, :task_id => task_id) 24 | rescue => err 25 | action_result(false, err.to_s) 26 | end 27 | end 28 | end 29 | 30 | private 31 | 32 | def automate_domain_ident(domain) 33 | "Automate Domain id:#{domain.id} name:'#{domain.name}'" 34 | end 35 | 36 | def resource_search(id, type, klass) 37 | if ApplicationRecord.compressed_id?(id) 38 | super 39 | else 40 | begin 41 | domain = collection_class(:automate_domains).find_by!(:name => id) 42 | rescue 43 | raise NotFoundError, "Couldn't find #{klass} with 'name'=#{id}" 44 | end 45 | super(domain.id, type, klass) 46 | end 47 | end 48 | 49 | def current_tenant 50 | User.current_user.current_tenant || Tenant.default_tenant 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/requests/middleware_datasources_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Middleware Datasources API' do 2 | let(:datasource) { FactoryGirl.create(:middleware_datasource) } 3 | 4 | describe '/' do 5 | it 'forbids access without an appropriate role' do 6 | api_basic_authorize 7 | 8 | get api_middleware_datasources_url 9 | 10 | expect(response).to have_http_status(:forbidden) 11 | end 12 | 13 | it 'returns an empty listing of datasources' do 14 | api_basic_authorize collection_action_identifier(:middleware_datasources, :read, :get) 15 | 16 | get api_middleware_datasources_url 17 | 18 | expect(response).to have_http_status(:ok) 19 | expect(response.parsed_body).to include( 20 | 'name' => 'middleware_datasources', 21 | 'count' => 0, 22 | 'resources' => [], 23 | 'subcount' => 0 24 | ) 25 | end 26 | 27 | it 'returns a listing of datasources' do 28 | datasource 29 | 30 | api_basic_authorize collection_action_identifier(:middleware_datasources, :read, :get) 31 | 32 | get api_middleware_datasources_url 33 | 34 | expect(response).to have_http_status(:ok) 35 | expect(response.parsed_body).to include( 36 | 'name' => 'middleware_datasources', 37 | 'count' => 1, 38 | 'resources' => [{ 39 | 'href' => api_middleware_datasource_url(nil, datasource) 40 | }], 41 | 'subcount' => 1 42 | ) 43 | end 44 | end 45 | 46 | describe '/:id' do 47 | it 'forbids access to a datasource without an appropriate role' do 48 | api_basic_authorize 49 | 50 | get api_middleware_datasource_url(nil, datasource) 51 | 52 | expect(response).to have_http_status(:forbidden) 53 | end 54 | 55 | it 'returns the attributes of one datasource' do 56 | api_basic_authorize action_identifier(:middleware_datasources, :read, :resource_actions, :get) 57 | 58 | get api_middleware_datasource_url(nil, datasource) 59 | 60 | expect(response).to have_http_status(:ok) 61 | expect(response.parsed_body).to include( 62 | 'id' => datasource.id.to_s, 63 | 'href' => api_middleware_datasource_url(nil, datasource) 64 | ) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /app/controllers/api/tags_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class TagsController < BaseController 3 | def create_resource(type, _id, data) 4 | assert_id_not_specified(data, type) 5 | category_data = data.delete("category") { {} } 6 | category = fetch_category(category_data) 7 | unless category 8 | category_rep = category_data.map { |k, v| "#{k} = #{v}" }.join(', ') 9 | raise BadRequestError, "Could not find category with data #{category_rep}" 10 | end 11 | begin 12 | entry = category.add_entry(data) 13 | raise BadRequestError, entry.errors.full_messages.join(', ') unless entry.valid? 14 | entry.tag 15 | rescue => err 16 | raise BadRequestError, "Could not create a new tag - #{err}" 17 | end 18 | end 19 | 20 | def edit_resource(type, id, data) 21 | klass = collection_class(type) 22 | tag = resource_search(id, type, klass) 23 | entry = Classification.find_by(:tag_id => tag.id) 24 | raise BadRequestError, "Failed to find tag/#{id} resource" unless entry 25 | 26 | if data["name"].present? 27 | tag.update_attribute(:name, Classification.name2tag(data["name"], entry.parent_id, TAG_NAMESPACE)) 28 | end 29 | entry.update_attributes(data.except(*ID_ATTRS)) 30 | entry.tag 31 | end 32 | 33 | def delete_resource(_type, id, _data = {}) 34 | destroy_tag_and_classification(id) 35 | action_result(true, "tags id: #{id} deleting") 36 | rescue ActiveRecord::RecordNotFound 37 | raise 38 | rescue => err 39 | action_result(false, err.to_s) 40 | end 41 | 42 | private 43 | 44 | def fetch_category(data) 45 | category_id = parse_id(data, :categories) 46 | category_id ||= collection_class(:categories).find_by_name(data["name"]).try(:id) if data["name"] 47 | unless category_id 48 | raise BadRequestError, "Category id, href or name needs to be specified for creating a new tag resource" 49 | end 50 | Category.find_by(:id => category_id) 51 | end 52 | 53 | def destroy_tag_and_classification(tag_id) 54 | entry_or_tag = Classification.find_by(:tag_id => tag_id) || Tag.find(tag_id) 55 | entry_or_tag.destroy! 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/requests/automate_workspaces_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # REST API Request Tests - /api/automate_workspaces 3 | # 4 | describe "Automate Workspaces API" do 5 | describe 'GET' do 6 | let(:user) { FactoryGirl.create(:user_with_group, :userid => "admin") } 7 | let(:aw) { FactoryGirl.create(:automate_workspace, :user => user, :tenant => user.current_tenant) } 8 | 9 | it 'should not return resources when fetching the collection' do 10 | api_basic_authorize collection_action_identifier(:automate_workspaces, :read, :get) 11 | aw 12 | get(api_automate_workspaces_url) 13 | 14 | expect(response.parsed_body).not_to include("resources") 15 | expect(response).to have_http_status(:ok) 16 | end 17 | 18 | it 'should not allow fetching using id' do 19 | api_basic_authorize action_identifier(:automate_workspaces, :read, :resource_actions, :get) 20 | get(api_automate_workspace_url(nil, aw.id)) 21 | 22 | expect(response).to have_http_status(:not_found) 23 | end 24 | 25 | it 'should allow fetching using guid' do 26 | api_basic_authorize action_identifier(:automate_workspaces, :read, :resource_actions, :get) 27 | get(api_automate_workspace_url(nil, aw.guid)) 28 | 29 | expect(response).to have_http_status(:ok) 30 | end 31 | end 32 | 33 | describe 'POST' do 34 | let(:user) { FactoryGirl.create(:user_with_group, :userid => "admin") } 35 | let(:aw) { FactoryGirl.create(:automate_workspace, :user => user, :tenant => user.current_tenant) } 36 | let(:output) { { 'objects' => { 'root' => { 'a' => '1'} }, 'state_vars' => {'b' => 2}} } 37 | 38 | it 'should allow updating the object with valid data' do 39 | api_basic_authorize action_identifier(:automate_workspaces, :edit) 40 | post(api_automate_workspace_url(nil, aw.guid), :params => {:action => 'edit', :resource => output}) 41 | 42 | expect(response).to have_http_status(:ok) 43 | end 44 | 45 | it 'should send bad request with invalid data' do 46 | api_basic_authorize action_identifier(:automate_workspaces, :edit) 47 | 48 | post(api_automate_workspace_url(nil, aw.guid), :params => {:action => 'edit', :resource => {}}) 49 | 50 | expect(response).to have_http_status(:bad_request) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/requests/middleware_servers_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Middleware Servers API' do 2 | let(:server) { FactoryGirl.create(:middleware_server) } 3 | 4 | describe '/' do 5 | it 'forbids access without an appropriate role' do 6 | api_basic_authorize 7 | 8 | get api_middleware_servers_url 9 | 10 | expect(response).to have_http_status(:forbidden) 11 | end 12 | 13 | it 'returns an empty listing of servers' do 14 | api_basic_authorize collection_action_identifier(:middleware_servers, :read, :get) 15 | 16 | get api_middleware_servers_url 17 | 18 | expect(response).to have_http_status(:ok) 19 | expect(response.parsed_body).to include( 20 | 'name' => 'middleware_servers', 21 | 'count' => 0, 22 | 'resources' => [], 23 | 'subcount' => 0 24 | ) 25 | end 26 | 27 | it 'returns a a listing of servers' do 28 | server 29 | 30 | api_basic_authorize collection_action_identifier(:middleware_servers, :read, :get) 31 | 32 | get api_middleware_servers_url 33 | 34 | expect(response).to have_http_status(:ok) 35 | expect(response.parsed_body).to include( 36 | 'name' => 'middleware_servers', 37 | 'count' => 1, 38 | 'resources' => [{ 39 | 'href' => api_middleware_server_url(nil, server) 40 | }], 41 | 'subcount' => 1 42 | ) 43 | end 44 | end 45 | 46 | describe '/:id' do 47 | it 'forbids access to a server without an appropriate role' do 48 | api_basic_authorize 49 | 50 | get api_middleware_server_url(nil, server) 51 | 52 | expect(response).to have_http_status(:forbidden) 53 | end 54 | 55 | it 'returns the attributes of one server' do 56 | api_basic_authorize action_identifier(:middleware_servers, :read, :resource_actions, :get) 57 | 58 | get api_middleware_server_url(nil, server) 59 | 60 | expect(response).to have_http_status(:ok) 61 | expect(response.parsed_body).to include( 62 | 'id' => server.id.to_s, 63 | 'href' => api_middleware_server_url(nil, server), 64 | 'name' => server.name, 65 | 'feed' => server.feed, 66 | 'properties' => server.properties, 67 | ) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /app/controllers/api/roles_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RolesController < BaseController 3 | include Subcollections::Features 4 | 5 | def create_resource(type, _id = nil, data = {}) 6 | assert_id_not_specified(data, type) 7 | 8 | # Can't create a read-only role (reserved for out-of-box roles) 9 | if data['read_only'] 10 | raise BadRequestError, "Cannot create a read-only role." 11 | end 12 | 13 | role_klass = collection_class(type) 14 | 15 | get_settings_and_features(data) 16 | 17 | role = role_klass.create!(data.except(*ID_ATTRS)) 18 | api_log_info("Created new role #{role.name}") 19 | role 20 | rescue => err 21 | role.destroy if role 22 | raise err 23 | end 24 | 25 | def edit_resource(type, id = nil, data = {}) 26 | unless id 27 | raise BadRequestError, "Must specify an id for editing a #{type} resource" 28 | end 29 | 30 | # Can't set an existing role to read-only (reserved for out-of-box roles) 31 | if data['read_only'] 32 | raise BadRequestError, "Cannot set a non-system role to read-only." 33 | end 34 | 35 | role = resource_search(id, type, collection_class(:roles)) 36 | 37 | # Can't edit a read-only role 38 | if role.read_only 39 | raise BadRequestError, "Cannot edit a role that is read-only." 40 | end 41 | 42 | get_settings_and_features(data) 43 | 44 | role.update_attributes!(data.except(*ID_ATTRS)) 45 | api_log_info("Modified role #{role.name}") 46 | role 47 | end 48 | 49 | private 50 | 51 | def get_settings_and_features(data) 52 | if data['settings'] 53 | data['settings'][:restrictions] = get_role_settings(data) 54 | data.delete('settings') if data['settings'].empty? 55 | end 56 | 57 | # Build miq_product_features hash from passed in features, remove features 58 | if data['features'] 59 | data['miq_product_features'] = get_product_features(data['features']) 60 | data.delete('features') 61 | end 62 | end 63 | 64 | def get_role_settings(data) 65 | restrictions = {:vms => data['settings']['restrictions']['vms'].to_sym} 66 | data['settings'].delete('restrictions') 67 | restrictions 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/requests/middleware_deployments_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'Middleware Deployments API' do 2 | let(:deployment) { FactoryGirl.create(:middleware_deployment) } 3 | 4 | describe '/' do 5 | it 'forbids access without an appropriate role' do 6 | api_basic_authorize 7 | 8 | get api_middleware_deployments_url 9 | 10 | expect(response).to have_http_status(:forbidden) 11 | end 12 | 13 | it 'returns an empty listing of deployments' do 14 | api_basic_authorize collection_action_identifier(:middleware_deployments, :read, :get) 15 | 16 | get api_middleware_deployments_url 17 | 18 | expect(response).to have_http_status(:ok) 19 | expect(response.parsed_body).to include( 20 | 'name' => 'middleware_deployments', 21 | 'count' => 0, 22 | 'resources' => [], 23 | 'subcount' => 0 24 | ) 25 | end 26 | 27 | it 'returns a a listing of deployments' do 28 | deployment 29 | 30 | api_basic_authorize collection_action_identifier(:middleware_deployments, :read, :get) 31 | 32 | get api_middleware_deployments_url 33 | 34 | expect(response).to have_http_status(:ok) 35 | expect(response.parsed_body).to include( 36 | 'name' => 'middleware_deployments', 37 | 'count' => 1, 38 | 'resources' => [{ 39 | 'href' => api_middleware_deployment_url(nil, deployment) 40 | }], 41 | 'subcount' => 1 42 | ) 43 | end 44 | end 45 | 46 | describe '/:id' do 47 | it 'forbids access to a deployment without an appropriate role' do 48 | api_basic_authorize 49 | 50 | get api_middleware_deployment_url(nil, deployment) 51 | 52 | expect(response).to have_http_status(:forbidden) 53 | end 54 | 55 | it 'returns the attributes of one deployment' do 56 | api_basic_authorize action_identifier(:middleware_deployments, :read, :resource_actions, :get) 57 | 58 | get api_middleware_deployment_url(nil, deployment) 59 | 60 | expect(response).to have_http_status(:ok) 61 | expect(response.parsed_body).to include( 62 | 'id' => deployment.id.to_s, 63 | 'ems_id' => deployment.ems_id.to_s, 64 | 'href' => api_middleware_deployment_url(nil, deployment), 65 | 'name' => deployment.name 66 | ) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/controllers/api/provision_requests_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ProvisionRequestsController < BaseController 3 | include Subcollections::RequestTasks 4 | 5 | def create_resource(type, _id, data) 6 | assert_id_not_specified(data, type) 7 | 8 | version_str = data["version"] || "1.1" 9 | template_fields = hash_fetch(data, "template_fields") 10 | vm_fields = hash_fetch(data, "vm_fields") 11 | requester = hash_fetch(data, "requester") 12 | tags = hash_fetch(data, "tags") 13 | additional_values = hash_fetch(data, "additional_values") 14 | ems_custom_attrs = hash_fetch(data, "ems_custom_attributes") 15 | miq_custom_attrs = hash_fetch(data, "miq_custom_attributes") 16 | 17 | MiqProvisionVirtWorkflow.from_ws(version_str, User.current_user, template_fields, vm_fields, requester, tags, 18 | additional_values, ems_custom_attrs, miq_custom_attrs) 19 | end 20 | 21 | def edit_resource(type, id, data) 22 | req = resource_search(id, type, collection_class(:provision_requests)) 23 | RequestEditor.edit(req, data) 24 | req 25 | end 26 | 27 | def deny_resource(type, id, data) 28 | api_action(type, id) do |klass| 29 | provreq = resource_search(id, type, klass) 30 | provreq.deny(User.current_user.userid, data['reason']) 31 | action_result(true, "Provision request #{id} denied") 32 | end 33 | rescue => err 34 | action_result(false, err.to_s) 35 | end 36 | 37 | def approve_resource(type, id, data) 38 | api_action(type, id) do |klass| 39 | provreq = resource_search(id, type, klass) 40 | provreq.approve(User.current_user.userid, data['reason']) 41 | action_result(true, "Provision request #{id} approved") 42 | end 43 | rescue => err 44 | action_result(false, err.to_s) 45 | end 46 | 47 | def find_provision_requests(id) 48 | klass = collection_class(:requests) 49 | return klass.find(id) if User.current_user.admin_user? 50 | klass.find_by!(:requester => User.current_user, :id => id) 51 | end 52 | 53 | def provision_requests_search_conditions 54 | return {} if User.current_user.admin_user? 55 | {:requester => User.current_user} 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/lib/services/api/request_parser_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Api::RequestParser do 2 | describe ".parse_user" do 3 | it "returns nil if no username is provided" do 4 | expect(described_class.parse_user({})).to be_nil 5 | end 6 | 7 | it "returns the user identified with the username" do 8 | username = "foo" 9 | user = instance_double(User) 10 | allow(User).to receive(:lookup_by_identity).with(username).and_return(user) 11 | 12 | actual = described_class.parse_user("requester" => {"user_name" => username}) 13 | 14 | expect(actual).to eq(user) 15 | end 16 | 17 | it "raises if the user cannot be found" do 18 | allow(User).to receive(:lookup_by_identity).and_return(nil) 19 | 20 | expect do 21 | described_class.parse_user("requester" => {"user_name" => "foo"}) 22 | end.to raise_error(Api::BadRequestError, /unknown requester/i) 23 | end 24 | end 25 | 26 | describe ".parse_options" do 27 | it "raises if there are no options" do 28 | expect { described_class.parse_options({}) }.to raise_error(Api::BadRequestError, /missing options/) 29 | end 30 | 31 | it "symbolizes keys" do 32 | actual = described_class.parse_options("options" => {"foo" => "bar"}) 33 | expected = {:foo => "bar"} 34 | expect(actual).to eq(expected) 35 | end 36 | end 37 | 38 | describe ".parse_auto_approve" do 39 | it "returns true if true" do 40 | expect(described_class.parse_auto_approve("auto_approve" => true)).to be true 41 | end 42 | 43 | it "returns true if 'true'" do 44 | expect(described_class.parse_auto_approve("auto_approve" => "true")).to be true 45 | end 46 | 47 | it "returns false if false" do 48 | expect(described_class.parse_auto_approve("auto_approve" => false)).to be false 49 | end 50 | 51 | it "returns false if 'false'" do 52 | expect(described_class.parse_auto_approve("auto_approve" => "false")).to be false 53 | end 54 | 55 | it "returns false if nil" do 56 | expect(described_class.parse_auto_approve({})).to be false 57 | end 58 | 59 | it "raises if anything else" do 60 | expect do 61 | described_class.parse_auto_approve("auto_approve" => "foo") 62 | end.to raise_error(Api::BadRequestError, /invalid requester auto_approve value/i) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /spec/requests/entrypoint_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "API entrypoint" do 2 | it "returns a :settings hash" do 3 | api_basic_authorize 4 | 5 | get api_entrypoint_url 6 | 7 | expect(response).to have_http_status(:ok) 8 | expect_result_to_have_keys(%w(settings)) 9 | expect(response.parsed_body['settings']).to be_kind_of(Hash) 10 | end 11 | 12 | it "returns a locale" do 13 | api_basic_authorize 14 | 15 | get api_entrypoint_url 16 | 17 | expect(%w(en en_US)).to include(response.parsed_body['settings']['locale']) 18 | end 19 | 20 | it "returns users's settings" do 21 | api_basic_authorize 22 | 23 | test_settings = {:cartoons => {:saturday => {:tom_jerry => 'n', :bugs_bunny => 'y'}}} 24 | @user.update_attributes!(:settings => test_settings) 25 | 26 | get api_entrypoint_url 27 | 28 | expect(response.parsed_body).to include("settings" => a_hash_including(test_settings.deep_stringify_keys)) 29 | end 30 | 31 | it "collection query is sorted" do 32 | api_basic_authorize 33 | 34 | get api_entrypoint_url 35 | 36 | collection_names = response.parsed_body['collections'].map { |c| c['name'] } 37 | expect(collection_names).to eq(collection_names.sort) 38 | end 39 | 40 | it "returns server_info" do 41 | api_basic_authorize 42 | 43 | get api_entrypoint_url 44 | 45 | expect(response.parsed_body).to include( 46 | "server_info" => a_hash_including( 47 | "version" => Vmdb::Appliance.VERSION, 48 | "build" => Vmdb::Appliance.BUILD, 49 | "appliance" => MiqServer.my_server.name, 50 | "server_href" => api_server_url(nil, MiqServer.my_server), 51 | "zone_href" => api_zone_url(nil, MiqServer.my_server.zone), 52 | "region_href" => api_region_url(nil, MiqRegion.my_region) 53 | ) 54 | ) 55 | end 56 | 57 | it "returns product_info" do 58 | api_basic_authorize 59 | 60 | get api_entrypoint_url 61 | 62 | expect(response.parsed_body).to include( 63 | "product_info" => a_hash_including( 64 | "name" => I18n.t("product.name"), 65 | "name_full" => I18n.t("product.name_full"), 66 | "copyright" => I18n.t("product.copyright"), 67 | "support_website" => I18n.t("product.support_website"), 68 | "support_website_text" => I18n.t("product.support_website_text"), 69 | ) 70 | ) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/requests/base_controller/parser_spec.rb: -------------------------------------------------------------------------------- 1 | describe Api::BaseController do 2 | describe "Parser" do 3 | describe "#parse_href" do 4 | context "with a full url for the href" do 5 | let(:href) { "http://localhost:3000/api/collection/123" } 6 | 7 | it "returns ['collection', 123]" do 8 | expect(subject.parse_href(href)).to eq([:collection, 123]) 9 | end 10 | end 11 | 12 | context "with a full url for the href and a API version" do 13 | let(:href) { "http://localhost:3000/api/v1.2.3/collection/123" } 14 | 15 | it "returns ['collection', 123]" do 16 | expect(subject.parse_href(href)).to eq([:collection, 123]) 17 | end 18 | end 19 | 20 | context "with a partial URL that starts with '/api/...'" do 21 | let(:href) { "/api/collection/123" } 22 | 23 | it "returns ['collection', 123]" do 24 | expect(subject.parse_href(href)).to eq([:collection, 123]) 25 | end 26 | end 27 | 28 | context "with a partial URL that starts with '/api/v1.2.3/...'" do 29 | let(:href) { "/api/v1.2.3/collection/123" } 30 | 31 | it "returns ['collection', 123]" do 32 | expect(subject.parse_href(href)).to eq([:collection, 123]) 33 | end 34 | end 35 | 36 | context "with a partial URL that starts with 'api/...'" do 37 | let(:href) { "api/collection/123" } 38 | 39 | it "returns ['collection', 123]" do 40 | expect(subject.parse_href(href)).to eq([:collection, 123]) 41 | end 42 | end 43 | 44 | context "with a partial URL that starts with 'api/v1.2.3/...'" do 45 | let(:href) { "api/v1.2.3/collection/123" } 46 | 47 | it "returns ['collection', 123]" do 48 | expect(subject.parse_href(href)).to eq([:collection, 123]) 49 | end 50 | end 51 | 52 | context "with a partial URL without '/api/...'" do 53 | let(:href) { "collection/123" } 54 | 55 | it "returns ['collection', 123]" do 56 | expect(subject.parse_href(href)).to eq([:collection, 123]) 57 | end 58 | end 59 | 60 | context "with a partial URL without '/api/...' and a leading slash" do 61 | let(:href) { "/collection/123" } 62 | 63 | it "returns ['collection', 123]" do 64 | expect(subject.parse_href(href)).to eq([:collection, 123]) 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/snapshots.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Snapshots 4 | def snapshots_query_resource(object) 5 | object.snapshots 6 | end 7 | 8 | def snapshots_create_resource(parent, _type, _id, data) 9 | raise "Must specify a name for the snapshot" unless data["name"].present? 10 | raise parent.unsupported_reason(:snapshot_create) unless parent.supports_snapshot_create? 11 | 12 | message = "Creating snapshot #{data["name"]} for #{snapshot_ident(parent)}" 13 | task_id = queue_object_action( 14 | parent, 15 | message, 16 | :method_name => "create_snapshot", 17 | :args => [data["name"], data["description"], data.fetch("memory", false)] 18 | ) 19 | 20 | action_result(true, message, :task_id => task_id) 21 | rescue => e 22 | action_result(false, e.to_s) 23 | end 24 | 25 | def delete_resource_snapshots(parent, type, id, _data) 26 | snapshot = resource_search(id, type, collection_class(type)) 27 | begin 28 | raise parent.unsupported_reason(:remove_snapshot) unless parent.supports_remove_snapshot? 29 | 30 | message = "Deleting snapshot #{snapshot.name} for #{snapshot_ident(parent)}" 31 | task_id = queue_object_action(parent, message, :method_name => "remove_snapshot", :args => [id]) 32 | action_result(true, message, :task_id => task_id) 33 | rescue => e 34 | action_result(false, e.to_s) 35 | end 36 | end 37 | alias snapshots_delete_resource delete_resource_snapshots 38 | 39 | def snapshots_revert_resource(parent, type, id, _data) 40 | raise parent.unsupported_reason(:revert_to_snapshot) unless parent.supports_revert_to_snapshot? 41 | snapshot = resource_search(id, type, collection_class(type)) 42 | 43 | message = "Reverting to snapshot #{snapshot.name} for #{snapshot_ident(parent)}" 44 | task_id = queue_object_action(parent, message, :method_name => "revert_to_snapshot", :args => [id]) 45 | action_result(true, message, :task_id => task_id) 46 | rescue => e 47 | action_result(false, e.to_s) 48 | end 49 | 50 | private 51 | 52 | def snapshot_ident(parent) 53 | parent_ident = collection_config[@req.collection].description.singularize 54 | "#{parent_ident} id:#{parent.id} name:'#{parent.name}'" 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /app/controllers/api/generic_objects_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class GenericObjectsController < BaseController 3 | include Subcollections::Tags 4 | 5 | ADDITIONAL_ATTRS = %w(generic_object_definition associations).freeze 6 | 7 | before_action :set_additional_attributes 8 | before_action :set_associations, :only => [:index, :show] 9 | 10 | def create_resource(_type, _id, data) 11 | object_def = retrieve_generic_object_definition(data) 12 | object_def.create_object(data.except(*ADDITIONAL_ATTRS)).tap do |generic_object| 13 | add_associations(generic_object, data, object_def) if data.key?('associations') 14 | generic_object.save! 15 | end 16 | rescue => err 17 | raise BadRequestError, "Failed to create new generic object - #{err}" 18 | end 19 | 20 | def edit_resource(type, id, data) 21 | resource_search(id, type, collection_class(type)).tap do |generic_object| 22 | generic_object.update_attributes!(data.except(*ADDITIONAL_ATTRS)) 23 | add_associations(generic_object, data, generic_object.generic_object_definition) if data.key?('associations') 24 | generic_object.save! 25 | end 26 | rescue => err 27 | raise BadRequestError, "Failed to update generic object - #{err}" 28 | end 29 | 30 | private 31 | 32 | def set_additional_attributes 33 | @additional_attributes = %w(property_attributes) 34 | end 35 | 36 | def set_associations 37 | return unless params[:associations] 38 | params[:associations].split(',').each do |prop| 39 | @additional_attributes << prop 40 | end 41 | end 42 | 43 | def retrieve_generic_object_definition(data) 44 | definition_id = parse_id(data['generic_object_definition'], :generic_object_definitions) 45 | resource_search(definition_id, :generic_object_definitions, collection_class(:generic_object_definitions)) 46 | end 47 | 48 | def add_associations(generic_object, data, object_definition) 49 | invalid_associations = data['associations'].keys - object_definition.property_associations.keys 50 | raise BadRequestError, "Invalid associations #{invalid_associations.join(', ')}" unless invalid_associations.empty? 51 | 52 | data['associations'].each do |association, resource_refs| 53 | resources = resource_refs.collect do |ref| 54 | collection, id = parse_href(ref['href']) 55 | resource_search(id, collection, collection_class(collection)) 56 | end 57 | generic_object.public_send("#{association}=", resources) 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/controllers/api/groups_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class GroupsController < BaseController 3 | INVALID_GROUP_ATTRS = %w(id href group_type).freeze 4 | 5 | include Subcollections::Tags 6 | 7 | def groups_search_conditions 8 | ["group_type != ?", MiqGroup::TENANT_GROUP] 9 | end 10 | 11 | def find_groups(id) 12 | MiqGroup.non_tenant_groups.find(id) 13 | end 14 | 15 | def create_resource(_type, _id, data) 16 | validate_group_data(data) 17 | parse_set_role(data) 18 | parse_set_tenant(data) 19 | parse_set_filters(data) 20 | group = collection_class(:groups).create(data) 21 | if group.invalid? 22 | raise BadRequestError, "Failed to add a new group - #{group.errors.full_messages.join(', ')}" 23 | end 24 | group 25 | end 26 | 27 | def edit_resource(type, id, data) 28 | validate_group_data(data) 29 | parse_set_role(data) 30 | parse_set_tenant(data) 31 | group = resource_search(id, type, collection_class(:groups)) 32 | parse_set_filters(data, :entitlement_id => group.entitlement.try(:id)) 33 | raise ForbiddenError, "Cannot edit a read-only group" if group.read_only 34 | super 35 | end 36 | 37 | def delete_resource(type, id, data = {}) 38 | raise ForbiddenError, "Cannot delete a read-only group" if MiqGroup.find(id).read_only? 39 | super 40 | end 41 | 42 | private 43 | 44 | def parse_set_role(data) 45 | role = parse_fetch_role(data.delete("role")) 46 | data["miq_user_role"] = role if role 47 | end 48 | 49 | def parse_set_tenant(data) 50 | tenant = parse_fetch_tenant(data.delete("tenant")) 51 | data["tenant"] = tenant if tenant 52 | end 53 | 54 | # HACK: Format attrs to use accepts_nested_attributes_for (Entitlements) 55 | # Required for backwards compatibility of creating filters via group 56 | def parse_set_filters(data, entitlement_id: nil) 57 | filters = data.delete("filters") 58 | data["entitlement_attributes"] = {"id" => entitlement_id, "filters" => filters} if filters 59 | end 60 | 61 | def group_data_includes_invalid_attrs(data) 62 | data.keys.select { |k| INVALID_GROUP_ATTRS.include?(k) }.compact.join(", ") if data 63 | end 64 | 65 | def validate_group_data(data) 66 | bad_attrs = group_data_includes_invalid_attrs(data) 67 | raise BadRequestError, "Invalid attribute(s) #{bad_attrs} specified for a group" if bad_attrs.present? 68 | raise BadRequestError, "Invalid filters specified" unless Entitlement.valid_filters?(data["filters"]) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /app/controllers/api/policies_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class PoliciesController < BaseController 3 | include Subcollections::Conditions 4 | include Subcollections::Events 5 | include Subcollections::PolicyActions 6 | REQUIRED_FIELDS = %w(name mode description towhat conditions_ids policy_contents).freeze 7 | 8 | def create_resource(type, _id, data = {}) 9 | assert_id_not_specified(data, type) 10 | assert_all_required_fields_exists(data, type, REQUIRED_FIELDS) 11 | create_policy(data) 12 | end 13 | 14 | def edit_resource(type, id = nil, data = {}) 15 | raise BadRequestError, "Must specify an id for editing a #{type} resource" unless id 16 | policy = resource_search(id, type, collection_class(:policies)) 17 | begin 18 | add_policies_content(data, policy) if data["policy_contents"] 19 | policy.conditions = Condition.where(:id => data.delete("conditions_ids")) if data["conditions_ids"] 20 | policy.update_attributes(data) 21 | rescue => err 22 | raise BadRequestError, "Could not edit the policy - #{err}" 23 | end 24 | policy 25 | end 26 | 27 | private 28 | 29 | def create_policy(data) 30 | policy = MiqPolicy.create!(:name => data.delete("name"), 31 | :description => data.delete("description"), 32 | :towhat => data.delete("towhat"), 33 | :mode => data.delete("mode"), 34 | :active => true) 35 | add_policies_content(data, policy) 36 | policy.conditions = Condition.where(:id => data.delete("conditions_ids")) if data["conditions_ids"] 37 | policy 38 | rescue => err 39 | policy.destroy if policy 40 | raise BadRequestError, "Could not create the new policy - #{err}" 41 | end 42 | 43 | def add_policies_content(data, policy) 44 | policy.miq_policy_contents.destroy_all 45 | data.delete("policy_contents").each do |policy_content| 46 | add_policy_content(policy_content, policy) 47 | end 48 | end 49 | 50 | def add_policy_content(policy_content, policy) 51 | actions_list = [] 52 | policy_content["actions"].each do |action| 53 | actions_list << [MiqAction.find(action["action_id"]), action["opts"]] 54 | end 55 | policy.replace_actions_for_event(MiqEventDefinition.find(policy_content["event_id"]), actions_list) 56 | policy.save! 57 | end 58 | 59 | def policy_ident(policy) 60 | "Policy id:#{policy.id} name:'#{policy.name}'" 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | 7 | 8 | ## Unreleased as of Sprint 69 ending 2017-09-18 9 | 10 | ### Added 11 | - REST API 12 | - Express ids as uncompressed strings [(#55)](https://github.com/ManageIQ/manageiq-api/pull/55) 13 | - Load developer bundler stuff in the Gemfile [(#53)](https://github.com/ManageIQ/manageiq-api/pull/53) 14 | - Generic Objects REST API [(#50)](https://github.com/ManageIQ/manageiq-api/pull/50) 15 | - Fix generic object definition spec [(#48)](https://github.com/ManageIQ/manageiq-api/pull/48) 16 | - Allow current group update [(#44)](https://github.com/ManageIQ/manageiq-api/pull/44) 17 | - Use OPTIONS /api/providers [(#43)](https://github.com/ManageIQ/manageiq-api/pull/43) 18 | - OPTIONS - Generic Object Definition [(#42)](https://github.com/ManageIQ/manageiq-api/pull/42) 19 | - Add a basic ping endpoint [(#39)](https://github.com/ManageIQ/manageiq-api/pull/39) 20 | - Add PUT to generic object definitions API [(#38)](https://github.com/ManageIQ/manageiq-api/pull/38) 21 | - Generic Object Definition Actions [(#37)](https://github.com/ManageIQ/manageiq-api/pull/37) 22 | - Send back full dialog on create and edit [(#32)](https://github.com/ManageIQ/manageiq-api/pull/32) 23 | - Add API support for additional power operations [(#11)](https://github.com/ManageIQ/manageiq-api/pull/11) 24 | - Add guest_devices support to the API [(#7)](https://github.com/ManageIQ/manageiq-api/pull/7) 25 | 26 | ### Fixed 27 | - REST API 28 | - Preserve contract for expressions of alert definitions [(#46)](https://github.com/ManageIQ/manageiq-api/pull/46) 29 | - Raise not found error on DELETE [(#17)](https://github.com/ManageIQ/manageiq-api/pull/17) 30 | 31 | ## Unreleased as of Sprint 68 ending 2017-09-04 32 | 33 | ### Added 34 | - REST API 35 | - Adds Metric Rollups as a subcollection to services and vms [(#33)](https://github.com/ManageIQ/manageiq-api/pull/33) 36 | - Generic Object Definition CRUD [(#15)](https://github.com/ManageIQ/manageiq-api/pull/15) 37 | - Metric rollups api [(#4)](https://github.com/ManageIQ/manageiq-api/pull/4) 38 | 39 | ## Unreleased as of Sprint 67 ending 2017-08-21 40 | 41 | ### Fixed 42 | - REST API 43 | - Require credentials when creating a provider [(#16)](https://github.com/ManageIQ/manageiq-api/pull/16) 44 | - Check if the User.current_user is set before calling userid [(#13)](https://github.com/ManageIQ/manageiq-api/pull/13) 45 | - Don't require authorization for OPTIONS [(#8)](https://github.com/ManageIQ/manageiq-api/pull/8) 46 | -------------------------------------------------------------------------------- /app/controllers/api/service_requests_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ServiceRequestsController < BaseController 3 | include Subcollections::RequestTasks 4 | 5 | def approve_resource(type, id, data) 6 | raise "Must specify a reason for approving a service request" unless data["reason"].present? 7 | api_action(type, id) do |klass| 8 | provreq = resource_search(id, type, klass) 9 | provreq.approve(User.current_user.userid, data['reason']) 10 | action_result(true, "Service request #{id} approved") 11 | end 12 | rescue => err 13 | action_result(false, err.to_s) 14 | end 15 | 16 | def deny_resource(type, id, data) 17 | raise "Must specify a reason for denying a service request" unless data["reason"].present? 18 | api_action(type, id) do |klass| 19 | provreq = resource_search(id, type, klass) 20 | provreq.deny(User.current_user.userid, data['reason']) 21 | action_result(true, "Service request #{id} denied") 22 | end 23 | rescue => err 24 | action_result(false, err.to_s) 25 | end 26 | 27 | def edit_resource(type, id, data) 28 | request = resource_search(id, type, collection_class(:service_requests)) 29 | RequestEditor.edit(request, data) 30 | request 31 | end 32 | 33 | def find_service_requests(id) 34 | klass = collection_class(:service_requests) 35 | return klass.find(id) if User.current_user.admin_user? 36 | klass.find_by!(:requester => User.current_user, :id => id) 37 | end 38 | 39 | def service_requests_search_conditions 40 | return {} if User.current_user.admin_user? 41 | {:requester => User.current_user} 42 | end 43 | 44 | def get_user(data) 45 | user_id = data['user_id'] || parse_id(data['user'], :users) 46 | raise 'Must specify a valid user_id or user' unless user_id 47 | User.find(user_id) 48 | end 49 | 50 | def add_approver_resource(type, id, data) 51 | user = get_user(data) 52 | miq_approval = MiqApproval.create(:approver => user) 53 | resource_search(id, type, collection_class(:service_requests)).tap do |service_request| 54 | service_request.miq_approvals << miq_approval 55 | end 56 | rescue => err 57 | raise BadRequestError, "Cannot add approver - #{err}" 58 | end 59 | 60 | def remove_approver_resource(type, id, data) 61 | user = get_user(data) 62 | resource_search(id, type, collection_class(:service_requests)).tap do |service_request| 63 | miq_approval = service_request.miq_approvals.find_by(:approver_name => user.name) 64 | miq_approval.destroy if miq_approval 65 | end 66 | rescue => err 67 | raise BadRequestError, "Cannot remove approver - #{err}" 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/services/api/user_token_service.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class UserTokenService 3 | TYPES = %w(api ui ws).freeze 4 | # Additional Requester type token ttl's for authentication 5 | TYPE_TO_TTL_OVERRIDE = {'ui' => ::Settings.session.timeout}.freeze 6 | 7 | def initialize(config = ApiConfig, args = {}) 8 | @config = config 9 | @svc_options = args 10 | end 11 | 12 | def token_mgr(type) 13 | @token_mgr ||= {} 14 | case type 15 | when 'api', 'ui' # The default API token and UI token share the same TokenStore 16 | @token_mgr['api'] ||= new_token_mgr(base_config[:module], base_config[:name], api_config) 17 | when 'ws' 18 | @token_mgr['ws'] ||= TokenManager.new('ws', :token_ttl => -> { ::Settings.session.timeout }) 19 | end 20 | end 21 | 22 | # API Settings with additional token ttl's 23 | # 24 | def api_config 25 | @api_config ||= ::Settings[base_config[:module]].to_hash 26 | end 27 | 28 | def generate_token(userid, requester_type) 29 | validate_userid(userid) 30 | validate_requester_type(requester_type) 31 | 32 | $api_log.info("Generating Authentication Token for userid: #{userid} requester_type: #{requester_type}") 33 | 34 | token_mgr(requester_type).gen_token(:userid => userid, 35 | :token_ttl_override => TYPE_TO_TTL_OVERRIDE[requester_type]) 36 | end 37 | 38 | def validate_requester_type(requester_type) 39 | return if TYPES.include?(requester_type) 40 | requester_types = TYPES.join(', ') 41 | raise "Invalid requester_type #{requester_type} specified, valid types are: #{requester_types}" 42 | end 43 | 44 | private 45 | 46 | def base_config 47 | @config[:base] 48 | end 49 | 50 | def log_kv(key, val, pref = "") 51 | $api_log.info("#{pref} #{key.to_s.ljust([24, key.to_s.length].max, ' ')}: #{val}") 52 | end 53 | 54 | def new_token_mgr(mod, name, api_config) 55 | token_ttl = api_config[:token_ttl] 56 | 57 | options = {} 58 | options[:token_ttl] = -> { token_ttl.to_i_with_method } if token_ttl 59 | 60 | log_init(mod, name, options) if @svc_options[:log_init] 61 | TokenManager.new(mod, options) 62 | end 63 | 64 | def log_init(mod, name, options) 65 | $api_log.info("") 66 | $api_log.info("Creating new Token Manager for the #{name}") 67 | $api_log.info(" Token Manager module: #{mod}") 68 | $api_log.info(" Token Manager options:") 69 | options.each { |key, val| log_kv(key, val, " ") } 70 | $api_log.info("") 71 | end 72 | 73 | def validate_userid(userid) 74 | raise "Invalid userid #{userid} specified" unless User.exists?(:userid => userid) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /app/controllers/api/authentications_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class AuthenticationsController < BaseController 3 | def edit_resource(type, id, data) 4 | auth = resource_search(id, type, collection_class(:authentications)) 5 | raise "Update not supported for #{authentication_ident(auth)}" unless auth.respond_to?(:update_in_provider_queue) 6 | task_id = auth.update_in_provider_queue(data.deep_symbolize_keys) 7 | action_result(true, "Updating #{authentication_ident(auth)}", :task_id => task_id) 8 | rescue => err 9 | action_result(false, err.to_s) 10 | end 11 | 12 | def create_resource(_type, _id, data) 13 | manager_resource, attrs = validate_auth_attrs(data) 14 | task_id = AuthenticationService.create_authentication_task(manager_resource, attrs) 15 | action_result(true, 'Creating Authentication', :task_id => task_id) 16 | rescue => err 17 | action_result(false, err.to_s) 18 | end 19 | 20 | def delete_resource(type, id, _data = {}) 21 | auth = resource_search(id, type, collection_class(:authentications)) 22 | raise "Delete not supported for #{authentication_ident(auth)}" unless auth.respond_to?(:delete_in_provider_queue) 23 | task_id = auth.delete_in_provider_queue 24 | action_result(true, "Deleting #{authentication_ident(auth)}", :task_id => task_id) 25 | rescue ActiveRecord::RecordNotFound => err 26 | @req.method == :delete ? raise(err) : action_result(false, err.to_s) 27 | rescue => err 28 | action_result(false, err.to_s) 29 | end 30 | 31 | def refresh_resource(type, id, _data) 32 | auth = resource_search(id, type, collection_class(type)) 33 | task_ids = EmsRefresh.queue_refresh_task(auth) 34 | action_result(true, "Refreshing #{authentication_ident(auth)}", :task_ids => task_ids) 35 | rescue => err 36 | action_result(false, err.to_s) 37 | end 38 | 39 | def options 40 | render_options(:authentications, build_additional_fields) 41 | end 42 | 43 | private 44 | 45 | def authentication_ident(auth) 46 | "Authentication id:#{auth.id} name: '#{auth.name}'" 47 | end 48 | 49 | def build_additional_fields 50 | { 51 | :credential_types => ::Authentication.build_credential_options 52 | } 53 | end 54 | 55 | def validate_auth_attrs(data) 56 | raise 'must supply a manager resource' unless data['manager_resource'] 57 | attrs = data.dup.except('manager_resource') 58 | manager_collection, manager_id = parse_href(data['manager_resource']['href']) 59 | raise 'invalid manger_resource href specified' unless manager_collection && manager_id 60 | manager_resource = resource_search(manager_id, manager_collection, collection_class(manager_collection)) 61 | [manager_resource, attrs] 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller/results.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class BaseController 3 | module Results 4 | private 5 | 6 | def action_result(success, message = nil, options = {}) 7 | res = {:success => success} 8 | res[:message] = message if message.present? 9 | res[:result] = options[:result] unless options[:result].nil? 10 | add_task_to_result(res, options[:task_id]) if options[:task_id].present? 11 | add_tasks_to_result(res, options[:task_ids]) if options[:task_ids].present? 12 | add_parent_href_to_result(res, options[:parent_id]) if options[:parent_id].present? 13 | res 14 | end 15 | 16 | def add_href_to_result(hash, type, id) 17 | hash[:href] = "#{@req.api_prefix}/#{type}/#{id}" 18 | hash 19 | end 20 | 21 | def add_parent_href_to_result(hash, parent_id = nil) 22 | return if hash[:href].present? 23 | hash[:href] = "#{@req.api_prefix}/#{@req.collection}/#{parent_id ? parent_id : @req.c_id}" 24 | hash 25 | end 26 | 27 | def add_task_to_result(hash, task_id) 28 | hash[:task_id] = task_id.to_s 29 | hash[:task_href] = task_href(task_id) 30 | hash 31 | end 32 | 33 | def add_tasks_to_result(hash, task_ids) 34 | add_task_to_result(hash, task_ids.first) 35 | hash[:tasks] = task_ids.collect do |task_id| 36 | { :id => task_id.to_s, :href => task_href(task_id) } 37 | end 38 | end 39 | 40 | def add_tag_to_result(hash, tag_spec) 41 | hash[:tag_category] = tag_spec[:category] if tag_spec[:category].present? 42 | hash[:tag_name] = tag_spec[:name] if tag_spec[:name].present? 43 | hash[:tag_href] = "#{@req.api_prefix}/tags/#{tag_spec[:id]}" if tag_spec[:id].present? 44 | hash 45 | end 46 | 47 | def task_href(task_id) 48 | "#{@req.api_prefix}/tasks/#{task_id}" 49 | end 50 | 51 | def add_subcollection_resource_to_result(hash, ctype, object) 52 | return hash if object.blank? 53 | ctype_pref = ctype.to_s.singularize 54 | hash["#{ctype_pref}_id".to_sym] = object.id 55 | hash["#{ctype_pref}_href".to_sym] = "#{@req.api_prefix}/#{ctype}/#{object.id}" 56 | hash 57 | end 58 | 59 | def add_report_result_to_result(hash, result_id) 60 | hash[:result_id] = result_id 61 | hash[:result_href] = "#{@req.api_prefix}/results/#{result_id}" 62 | hash 63 | end 64 | 65 | def add_report_schedule_to_result(hash, schedule_id, report_id) 66 | hash[:schedule_id] = schedule_id 67 | hash[:schedule_href] = "#{@req.api_prefix}/reports/#{report_id}/schedules/#{schedule_id}" 68 | hash 69 | end 70 | 71 | def log_result(hash) 72 | hash.each { |k, v| api_log_info("Result: #{k}=#{v}") } 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/service_requests.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module ServiceRequests 4 | def service_requests_query_resource(object) 5 | return {} unless object 6 | klass = collection_class(:service_requests) 7 | 8 | case object 9 | when collection_class(:service_orders) 10 | klass.where(:service_order_id => object.id) 11 | else 12 | klass.where(:source_id => object.id) 13 | end 14 | end 15 | 16 | def service_requests_add_resource(target, _type, _id, data) 17 | result = add_service_request(target, data) 18 | add_parent_href_to_result(result) 19 | log_result(result) 20 | result 21 | end 22 | 23 | def service_requests_remove_resource(target, type, id, _data) 24 | service_request_subcollection_action(type, id) do |service_request| 25 | api_log_info("Removing #{service_request_ident(service_request)}") 26 | remove_service_request(target, service_request) 27 | end 28 | end 29 | 30 | private 31 | 32 | def service_request_ident(service_request) 33 | "Service Request id:#{service_request.id} description:'#{service_request.description}'" 34 | end 35 | 36 | def service_request_subcollection_action(type, id) 37 | klass = collection_class(:service_requests) 38 | result = 39 | begin 40 | service_request = resource_search(id, type, klass) 41 | yield(service_request) if block_given? 42 | rescue => e 43 | action_result(false, e.to_s) 44 | end 45 | add_subcollection_resource_to_result(result, type, service_request) if service_request 46 | add_parent_href_to_result(result) 47 | log_result(result) 48 | result 49 | end 50 | 51 | def add_service_request(target, data) 52 | if target.state != ServiceOrder::STATE_CART 53 | raise BadRequestError, "Must specify a cart to add a service request to" 54 | end 55 | workflow = service_request_workflow(data) 56 | validation = add_request_to_cart(workflow) 57 | if validation[:errors].present? 58 | action_result(false, validation[:errors].join(", ")) 59 | elsif validation[:request].nil? 60 | action_result(false, "Unable to add service request") 61 | else 62 | result = action_result(true, "Adding service_request") 63 | add_subcollection_resource_to_result(result, :service_requests, validation[:request]) 64 | result 65 | end 66 | rescue => e 67 | action_result(false, e.to_s) 68 | end 69 | 70 | def remove_service_request(target, service_request) 71 | target.class.remove_from_cart(service_request, User.current_user) 72 | action_result(true, "Removing #{service_request_ident(service_request)}") 73 | rescue => e 74 | action_result(false, e.to_s) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /app/controllers/api/configuration_script_sources_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ConfigurationScriptSourcesController < BaseController 3 | include Subcollections::ConfigurationScriptPayloads 4 | 5 | def edit_resource(type, id, data) 6 | config_script_src = resource_search(id, type, collection_class(:configuration_script_sources)) 7 | raise "Update not supported for #{config_script_src_ident(config_script_src)}" unless config_script_src.respond_to?(:update_in_provider_queue) 8 | task_id = config_script_src.update_in_provider_queue(data.deep_symbolize_keys) 9 | action_result(true, "Updating #{config_script_src_ident(config_script_src)}", :task_id => task_id) 10 | rescue => err 11 | action_result(false, err.to_s) 12 | end 13 | 14 | def delete_resource(type, id, _data = {}) 15 | config_script_src = resource_search(id, type, collection_class(:configuration_script_sources)) 16 | raise "Delete not supported for #{config_script_src_ident(config_script_src)}" unless config_script_src.respond_to?(:delete_in_provider_queue) 17 | task_id = config_script_src.delete_in_provider_queue 18 | action_result(true, "Deleting #{config_script_src_ident(config_script_src)}", :task_id => task_id) 19 | rescue => err 20 | action_result(false, err.to_s) 21 | end 22 | 23 | def create_resource(_type, _id, data) 24 | validate_attrs(data) 25 | manager_id = parse_id(data['manager_resource'], :providers) 26 | raise 'Must specify a valid manager_resource href or id' unless manager_id 27 | manager = resource_search(manager_id, :providers, collection_class(:providers)) 28 | 29 | type = "#{manager.type}::ConfigurationScriptSource" 30 | klass = ConfigurationScriptSource.descendant_get(type) 31 | raise "ConfigurationScriptSource cannot be added to #{manager_ident(manager)}" unless klass.respond_to?(:create_in_provider_queue) 32 | 33 | task_id = klass.create_in_provider_queue(manager.id, data.except('manager_resource')) 34 | action_result(true, "Creating ConfigurationScriptSource for #{manager_ident(manager)}", :task_id => task_id) 35 | rescue => err 36 | action_result(false, err.to_s) 37 | end 38 | 39 | def refresh_resource(type, id, _data) 40 | config_script_src = resource_search(id, type, collection_class(type)) 41 | task_ids = EmsRefresh.queue_refresh_task(config_script_src) 42 | action_result(true, "Refreshing #{config_script_src_ident(config_script_src)}", :task_ids => task_ids) 43 | rescue => err 44 | action_result(false, err.to_s) 45 | end 46 | 47 | private 48 | 49 | def config_script_src_ident(config_script_src) 50 | "ConfigurationScriptSource id:#{config_script_src.id} name: '#{config_script_src.name}'" 51 | end 52 | 53 | def validate_attrs(data) 54 | raise 'Must supply a manager resource' unless data['manager_resource'] 55 | end 56 | 57 | def manager_ident(manager) 58 | "Manager id:#{manager.id} name: '#{manager.name}'" 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller/parameters.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class BaseController 3 | module Parameters 4 | def hash_fetch(hash, element, default = {}) 5 | hash[element] || default 6 | end 7 | 8 | # 9 | # Returns an MiqExpression based on the filter attributes specified. 10 | # 11 | def filter_param(klass) 12 | return nil if params['filter'].blank? 13 | Filter.parse(params["filter"], klass) 14 | end 15 | 16 | def by_tag_param 17 | params['by_tag'] 18 | end 19 | 20 | def search_options 21 | params['search_options'].to_s.split(",") 22 | end 23 | 24 | def search_option?(what) 25 | search_options.map(&:downcase).include?(what.to_s) 26 | end 27 | 28 | def format_attributes 29 | params['format_attributes'].to_s.split(",").map { |af| af.split("=").map(&:strip) } 30 | end 31 | 32 | def attribute_format(attr) 33 | format_attributes.detect { |af| af.first == attr }.try(:second) 34 | end 35 | 36 | def attribute_selection 37 | if @req.attributes.empty? && @additional_attributes 38 | Array(@additional_attributes) | ID_ATTRS 39 | elsif !@req.attributes.empty? 40 | @req.attributes | ID_ATTRS 41 | else 42 | "all" 43 | end 44 | end 45 | 46 | def attribute_selection_for(collection) 47 | Array(attribute_selection).collect do |attr| 48 | /\A#{collection}\.(?.*)\z/.match(attr) { |m| m[:name] } 49 | end.compact 50 | end 51 | 52 | def render_attr(attr) 53 | as = attribute_selection 54 | as == "all" || as.include?(attr) 55 | end 56 | 57 | # 58 | # Returns the ActiveRecord's option for :order 59 | # 60 | # i.e. ['attr1 [asc|desc]', 'attr2 [asc|desc]', ...] 61 | # 62 | def sort_params(klass) 63 | return [] if params['sort_by'].blank? 64 | 65 | orders = String(params['sort_order']).split(",") 66 | options = String(params['sort_options']).split(",") 67 | params['sort_by'].split(",").zip(orders).collect do |attr, order| 68 | if klass.virtual_attribute?(attr) && !klass.attribute_supported_by_sql?(attr) 69 | raise BadRequestError, "#{klass.name} cannot be sorted by #{attr}" 70 | elsif klass.attribute_supported_by_sql?(attr) 71 | sort_directive(klass, attr, order, options) 72 | else 73 | raise BadRequestError, "#{attr} is not a valid attribute for #{klass.name}" 74 | end 75 | end.compact 76 | end 77 | 78 | def sort_directive(klass, attr, order, options) 79 | arel = klass.arel_attribute(attr) 80 | if order 81 | arel = arel.lower if options.map(&:downcase).include?("ignore_case") 82 | arel = arel.desc if order.downcase == "desc" 83 | arel = arel.asc if order.downcase == "asc" 84 | else 85 | arel = arel.asc 86 | end 87 | arel 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/requests/automate_domains_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # REST API Request Tests - /api/automate_domains 3 | # 4 | describe "Automate Domains API" do 5 | describe 'refresh_from_source action' do 6 | let(:git_domain) { FactoryGirl.create(:miq_ae_git_domain) } 7 | it 'forbids access for users without proper permissions' do 8 | api_basic_authorize 9 | 10 | post(api_automate_domain_url(nil, git_domain), :params => gen_request(:refresh_from_source)) 11 | 12 | expect(response).to have_http_status(:forbidden) 13 | end 14 | 15 | it 'fails to refresh git when the region misses git_owner role' do 16 | api_basic_authorize action_identifier(:automate_domains, :refresh_from_source) 17 | expect(GitBasedDomainImportService).to receive(:available?).and_return(false) 18 | 19 | post(api_automate_domain_url(nil, git_domain), :params => gen_request(:refresh_from_source)) 20 | expect_single_action_result(:success => false, 21 | :message => 'Git owner role is not enabled to be able to import git repositories') 22 | end 23 | 24 | context 'with proper git_owner role' do 25 | let(:non_git_domain) { FactoryGirl.create(:miq_ae_domain) } 26 | before do 27 | expect(GitBasedDomainImportService).to receive(:available?).and_return(true) 28 | end 29 | 30 | it 'fails to refresh when domain did not originate from git' do 31 | api_basic_authorize action_identifier(:automate_domains, :refresh_from_source) 32 | 33 | post(api_automate_domain_url(nil, non_git_domain), :params => gen_request(:refresh_from_source)) 34 | expect_single_action_result( 35 | :success => false, 36 | :message => a_string_matching(/Automate Domain .* did not originate from git repository/) 37 | ) 38 | end 39 | 40 | it 'refreshes domain from git_repository' do 41 | api_basic_authorize action_identifier(:automate_domains, :refresh_from_source) 42 | 43 | expect_any_instance_of(GitBasedDomainImportService).to receive(:queue_refresh_and_import) 44 | post(api_automate_domain_url(nil, git_domain), :params => gen_request(:refresh_from_source)) 45 | expect_single_action_result( 46 | :success => true, 47 | :message => a_string_matching(/Refreshing Automate Domain .* from git repository/), 48 | :href => api_automate_domain_url(nil, git_domain) 49 | ) 50 | end 51 | 52 | it 'refreshes domain from git_repository by domain name' do 53 | api_basic_authorize action_identifier(:automate_domains, :refresh_from_source) 54 | 55 | expect_any_instance_of(GitBasedDomainImportService).to receive(:queue_refresh_and_import) 56 | post(api_automate_domain_url(nil, git_domain.name), :params => gen_request(:refresh_from_source)) 57 | expect_single_action_result( 58 | :success => true, 59 | :message => a_string_matching(/Refreshing Automate Domain .* from git repository/), 60 | :href => api_automate_domain_url(nil, git_domain.name) 61 | ) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /app/controllers/api/service_dialogs_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ServiceDialogsController < BaseController 3 | before_action :set_additional_attributes, :only => [:index, :show] 4 | 5 | def refresh_dialog_fields_resource(type, id = nil, data = nil) 6 | raise BadRequestError, "Must specify an id for Reconfiguring a #{type} resource" unless id 7 | 8 | api_action(type, id) do |klass| 9 | service_dialog = resource_search(id, type, klass) 10 | api_log_info("Refreshing Dialog Fields for #{service_dialog_ident(service_dialog)}") 11 | 12 | refresh_dialog_fields_service_dialog(service_dialog, data) 13 | end 14 | end 15 | 16 | def fetch_service_dialogs_content(resource) 17 | resource.content(nil, nil, true) 18 | end 19 | 20 | def create_resource(_type, _id, data) 21 | dialog = DialogImportService.new.import(data) 22 | fetch_service_dialogs_content(dialog).first 23 | rescue => e 24 | raise BadRequestError, "Failed to create a new dialog - #{e}" 25 | end 26 | 27 | def edit_resource(type, id, data) 28 | service_dialog = resource_search(id, type, Dialog) 29 | begin 30 | service_dialog.update!(data.except('content')) 31 | service_dialog.update_tabs(data['content']['dialog_tabs']) if data['content'] 32 | rescue => err 33 | raise BadRequestError, "Failed to update service dialog - #{err}" 34 | end 35 | fetch_service_dialogs_content(service_dialog).first 36 | end 37 | 38 | def copy_resource(type, id, data) 39 | service_dialog = resource_search(id, type, Dialog) 40 | attributes = data.dup 41 | attributes['label'] = "Copy of #{service_dialog.label}" unless attributes.key?('label') 42 | service_dialog.deep_copy(attributes).tap(&:save!) 43 | rescue => err 44 | raise BadRequestError, "Failed to copy service dialog - #{err}" 45 | end 46 | 47 | private 48 | 49 | def set_additional_attributes 50 | @additional_attributes = %w(content) if attribute_selection == "all" 51 | end 52 | 53 | def refresh_dialog_fields_service_dialog(service_dialog, data) 54 | data ||= {} 55 | dialog_fields = Hash(data["dialog_fields"]) 56 | refresh_fields = data["fields"] 57 | return action_result(false, "Must specify fields to refresh") if refresh_fields.blank? 58 | 59 | define_service_dialog_fields(service_dialog, dialog_fields) 60 | 61 | refresh_dialog_fields_action(service_dialog, refresh_fields, service_dialog_ident(service_dialog)) 62 | rescue => err 63 | action_result(false, err.to_s) 64 | end 65 | 66 | def define_service_dialog_fields(service_dialog, dialog_fields) 67 | ident = service_dialog_ident(service_dialog) 68 | dialog_fields.each do |key, value| 69 | dialog_field = service_dialog.field(key) 70 | raise BadRequestError, "Dialog field #{key} specified does not exist in #{ident}" if dialog_field.nil? 71 | dialog_field.value = value 72 | end 73 | end 74 | 75 | def service_dialog_ident(service_dialog) 76 | "Service Dialog id:#{service_dialog.id} label:'#{service_dialog.label}'" 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/requests/events_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # REST API Request Tests - Events 3 | # 4 | # Event primary collection: 5 | # /api/events 6 | # 7 | # Event subcollection: 8 | # /api/policies/:id/events 9 | # 10 | describe "Events API" do 11 | let(:miq_event_guid_list) { MiqEventDefinition.pluck(:guid) } 12 | 13 | def create_events(count) 14 | count.times { FactoryGirl.create(:miq_event_definition) } 15 | end 16 | 17 | context "Event collection" do 18 | it "query invalid event" do 19 | api_basic_authorize action_identifier(:events, :read, :resource_actions, :get) 20 | 21 | get api_event_url(nil, 999_999) 22 | 23 | expect(response).to have_http_status(:not_found) 24 | end 25 | 26 | it "query events with no events defined" do 27 | api_basic_authorize collection_action_identifier(:events, :read, :get) 28 | 29 | get api_events_url 30 | 31 | expect_empty_query_result(:events) 32 | end 33 | 34 | it "query events" do 35 | api_basic_authorize collection_action_identifier(:events, :read, :get) 36 | create_events(3) 37 | 38 | get api_events_url 39 | 40 | expect_query_result(:events, 3, 3) 41 | expect_result_resources_to_include_hrefs( 42 | "resources", 43 | MiqEventDefinition.select(:id).collect { |med| api_event_url(nil, med) } 44 | ) 45 | end 46 | 47 | it "query events in expanded form" do 48 | api_basic_authorize collection_action_identifier(:events, :read, :get) 49 | create_events(3) 50 | 51 | get api_events_url, :params => { :expand => "resources" } 52 | 53 | expect_query_result(:events, 3, 3) 54 | expect_result_resources_to_include_data("resources", "guid" => miq_event_guid_list) 55 | end 56 | end 57 | 58 | context "Event subcollection" do 59 | let(:policy) { FactoryGirl.create(:miq_policy, :name => "Policy 1") } 60 | 61 | def relate_events_to(policy) 62 | MiqEventDefinition.all.collect(&:id).each do |event_id| 63 | MiqPolicyContent.create(:miq_policy_id => policy.id, :miq_event_definition_id => event_id) 64 | end 65 | end 66 | 67 | it "query events with no events defined" do 68 | api_basic_authorize 69 | 70 | get api_policy_events_url(nil, policy) 71 | 72 | expect_empty_query_result(:events) 73 | end 74 | 75 | it "query events" do 76 | api_basic_authorize 77 | create_events(3) 78 | relate_events_to(policy) 79 | 80 | get api_policy_events_url(nil, policy), :params => { :expand => "resources" } 81 | 82 | expect_query_result(:events, 3, 3) 83 | expect_result_resources_to_include_data("resources", "guid" => miq_event_guid_list) 84 | end 85 | 86 | it "query policy with expanded events" do 87 | api_basic_authorize action_identifier(:policies, :read, :resource_actions, :get) 88 | create_events(3) 89 | relate_events_to(policy) 90 | 91 | get api_policy_url(nil, policy), :params => { :expand => "events" } 92 | 93 | expect_single_resource_query("name" => policy.name, "description" => policy.description, "guid" => policy.guid) 94 | expect_result_resources_to_include_data("events", "guid" => miq_event_guid_list) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /app/controllers/api/requests_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class RequestsController < BaseController 3 | include Subcollections::RequestTasks 4 | 5 | def create_resource(type, _id, data) 6 | assert_id_not_specified(data, type) 7 | request_klass = collection_class(:requests) 8 | request_options = RequestParser.parse_options(data) 9 | 10 | begin 11 | typed_request_klass = request_klass.class_from_request_data(request_options) 12 | rescue => err 13 | raise BadRequestError, "Invalid request - #{err}" 14 | end 15 | 16 | # We must authorize the user based on the request type klass 17 | authorize_request(typed_request_klass) 18 | 19 | user = RequestParser.parse_user(data) || User.current_user 20 | auto_approve = RequestParser.parse_auto_approve(data) 21 | 22 | begin 23 | typed_request_klass.create_request(request_options, user, auto_approve) 24 | rescue => err 25 | raise BadRequestError, "Could not create the request - #{err}" 26 | end 27 | end 28 | 29 | def edit_resource(type, id = nil, data = {}) 30 | raise BadRequestError, "Must specify a id for editing a #{type} resource" unless id 31 | request = resource_search(id, type, collection_class(:requests)) 32 | RequestEditor.edit(request, data) 33 | request 34 | end 35 | 36 | def approve_resource(type, id, data) 37 | raise "Must specify a reason for approving a request" if data["reason"].blank? 38 | api_action(type, id) do |klass| 39 | request = resource_search(id, type, klass) 40 | request.approve(User.current_user.userid, data["reason"]) 41 | action_result(true, "Request #{id} approved") 42 | end 43 | rescue => err 44 | action_result(false, err.to_s) 45 | end 46 | 47 | def deny_resource(type, id, data) 48 | raise "Must specify a reason for denying a request" if data["reason"].blank? 49 | api_action(type, id) do |klass| 50 | request = resource_search(id, type, klass) 51 | request.deny(User.current_user.userid, data["reason"]) 52 | action_result(true, "Request #{id} denied") 53 | end 54 | rescue => err 55 | action_result(false, err.to_s) 56 | end 57 | 58 | def find_requests(id) 59 | klass = collection_class(:requests) 60 | return klass.find(id) if User.current_user.admin_user? 61 | klass.find_by!(:requester => User.current_user, :id => id) 62 | end 63 | 64 | def requests_search_conditions 65 | return {} if User.current_user.admin_user? 66 | {:requester => User.current_user} 67 | end 68 | 69 | private 70 | 71 | def authorize_request(typed_request_klass) 72 | create_action = collection_config["requests"].collection_actions.post.detect { |a| a.name == "create" } 73 | request_spec = create_action.identifiers.detect { |i| i.klass == typed_request_klass.name } 74 | raise BadRequestError, "Unsupported request class #{typed_request_klass}" if request_spec.blank? 75 | 76 | if request_spec.identifier && !api_user_role_allows?(request_spec.identifier) 77 | raise ForbiddenError, "Create action is forbidden for #{typed_request_klass} requests" 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /app/controllers/api/service_orders_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ServiceOrdersController < BaseController 3 | include Subcollections::ServiceRequests 4 | USER_CART_ID = 'cart'.freeze 5 | 6 | def create_resource(type, id, data) 7 | raise BadRequestError, "Can't create an ordered service order" if data["state"] == ServiceOrder::STATE_ORDERED 8 | service_requests = data.delete("service_requests") 9 | data["state"] ||= ServiceOrder::STATE_CART 10 | if service_requests.blank? 11 | super 12 | else 13 | create_service_order_with_service_requests(service_requests) 14 | ServiceOrder.cart_for(User.current_user) 15 | end 16 | end 17 | 18 | def clear_resource(type, id, _data) 19 | service_order = resource_search(id, type, collection_class(type)) 20 | begin 21 | service_order.clear 22 | rescue => e 23 | raise BadRequestError, e.message 24 | end 25 | service_order 26 | end 27 | 28 | def order_resource(type, id, _data) 29 | service_order = resource_search(id, type, collection_class(type)) 30 | service_order.checkout 31 | service_order 32 | end 33 | 34 | def validate_id(id, klass) 35 | id == USER_CART_ID || super(id, klass) 36 | end 37 | 38 | def find_service_orders(id) 39 | if id == USER_CART_ID 40 | ServiceOrder.cart_for(User.current_user) 41 | else 42 | ServiceOrder.find_for_user(User.current_user, id) 43 | end 44 | end 45 | 46 | def service_orders_search_conditions 47 | {:user => User.current_user, :tenant => User.current_user.current_tenant} 48 | end 49 | 50 | def copy_resource(type, id, data) 51 | service_order = resource_search(id, type, collection_class(type)) 52 | service_order.deep_copy(data) 53 | rescue => err 54 | raise BadRequestError, "Could not copy service order - #{err}" 55 | end 56 | 57 | private 58 | 59 | def add_request_to_cart(workflow) 60 | workflow.add_request_to_cart 61 | end 62 | 63 | def create_service_order_with_service_requests(service_requests) 64 | workflows = validate_service_requests(service_requests) 65 | workflows.each do |workflow| 66 | check_validation(add_request_to_cart(workflow)) 67 | end 68 | end 69 | 70 | def validate_service_requests(service_requests) 71 | service_requests.collect do |service_request| 72 | workflow = service_request_workflow(service_request) 73 | check_validation(workflow.validate_dialog) 74 | workflow 75 | end 76 | end 77 | 78 | def service_request_workflow(service_request) 79 | service_template_id = href_id(service_request.delete("service_template_href"), :service_templates) 80 | if service_template_id.blank? 81 | raise BadRequestError, "Must specify a service_template_href for adding a service_request" 82 | end 83 | service_template = resource_search(service_template_id, :service_templates, ServiceTemplate) 84 | service_template.provision_workflow(User.current_user, service_request) 85 | end 86 | 87 | def check_validation(validation) 88 | if validation[:errors].present? 89 | raise BadRequestError, "Invalid service request - #{validation[:errors].join(", ")}" 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/custom_attributes.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module CustomAttributes 4 | def custom_attributes_query_resource(object) 5 | object.respond_to?(:custom_attributes) ? object.custom_attributes : [] 6 | end 7 | 8 | def custom_attributes_add_resource(object, _type, _id, data = nil) 9 | if object.respond_to?(:custom_attributes) 10 | add_custom_attribute(object, data) 11 | else 12 | raise BadRequestError, "#{object.class.name} does not support management of custom attributes" 13 | end 14 | end 15 | 16 | def custom_attributes_edit_resource(object, _type, id = nil, data = nil) 17 | ca = find_custom_attribute(object, id, data) 18 | edit_custom_attribute(object, ca, data) 19 | end 20 | 21 | def custom_attributes_delete_resource(object, _type, id = nil, data = nil) 22 | ca = find_custom_attribute(object, id, data) 23 | delete_custom_attribute(object, ca) 24 | end 25 | 26 | def delete_resource_custom_attributes(parent, _type, id, data) 27 | ca = find_custom_attribute(parent, id, data) 28 | delete_custom_attribute(parent, ca) 29 | end 30 | 31 | private 32 | 33 | def add_custom_attribute(object, data) 34 | ca = find_custom_attribute_by_data(object, data) 35 | if ca.present? 36 | update_custom_attributes(ca, data) 37 | else 38 | ca = new_custom_attribute(data) 39 | object.custom_attributes << ca 40 | end 41 | update_custom_field(object, ca) 42 | ca 43 | end 44 | 45 | def edit_custom_attribute(object, ca, data) 46 | return if ca.blank? 47 | update_custom_attributes(ca, data) 48 | update_custom_field(object, ca) 49 | ca 50 | end 51 | 52 | def delete_custom_attribute(object, ca) 53 | return if ca.blank? 54 | object.set_custom_field(ca.name, '') if ca.stored_on_provider? 55 | ca.delete 56 | ca 57 | end 58 | 59 | def update_custom_attributes(ca, data) 60 | ca.update_attributes(data.slice("name", "value", "section")) 61 | end 62 | 63 | def update_custom_field(object, ca) 64 | object.set_custom_field(ca.name.to_s, ca.value.to_s) if ca.stored_on_provider? 65 | end 66 | 67 | def find_custom_attribute(object, id, data) 68 | if object.respond_to?(:custom_attributes) 69 | id.present? && id > 0 ? object.custom_attributes.find(id) : find_custom_attribute_by_data(object, data) 70 | else 71 | raise BadRequestError, "#{object.class.name} does not support management of custom attributes" 72 | end 73 | end 74 | 75 | def find_custom_attribute_by_data(object, data) 76 | object.custom_attributes.detect do |ca| 77 | ca.section.to_s == data["section"].to_s && ca.name.downcase == data["name"].downcase 78 | end 79 | end 80 | 81 | def new_custom_attribute(data) 82 | name = data["name"].to_s.strip 83 | raise BadRequestError, "Must specify a name for a custom attribute to be added" if name.blank? 84 | CustomAttribute.new(:name => name, 85 | :value => data["value"], 86 | :source => data["source"].blank? ? "EVM" : data["source"], 87 | :section => data["section"]) 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/requests/headers_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Headers" do 2 | describe "Accept" do 3 | it "returns JSON when set to application/json" do 4 | api_basic_authorize 5 | 6 | get api_entrypoint_url, :headers => {"Accept" => "application/json"} 7 | 8 | expect(response.parsed_body).to include("name" => "API", "description" => "REST API") 9 | expect(response).to have_http_status(:ok) 10 | end 11 | 12 | it "returns JSON when not provided" do 13 | api_basic_authorize 14 | 15 | get api_entrypoint_url 16 | 17 | expect(response.parsed_body).to include("name" => "API", "description" => "REST API") 18 | expect(response).to have_http_status(:ok) 19 | end 20 | 21 | it "responds with an error for unsupported mime-types" do 22 | api_basic_authorize 23 | 24 | get api_entrypoint_url, :headers => {"Accept" => "application/xml"} 25 | 26 | expected = { 27 | "error" => a_hash_including( 28 | "kind" => "unsupported_media_type", 29 | "message" => "Invalid Response Format application/xml requested", 30 | "klass" => "Api::UnsupportedMediaTypeError" 31 | ) 32 | } 33 | expect(response.parsed_body).to include(expected) 34 | expect(response).to have_http_status(:unsupported_media_type) 35 | end 36 | end 37 | 38 | describe "Content-Type" do 39 | it "accepts JSON by default" do 40 | api_basic_authorize(collection_action_identifier(:groups, :create)) 41 | 42 | post api_groups_url, :params => {:description => "foo"} 43 | 44 | expect(response).to have_http_status(:ok) 45 | end 46 | 47 | it "will accept JSON when set to application/json" do 48 | api_basic_authorize(collection_action_identifier(:groups, :create)) 49 | 50 | post(api_groups_url, 51 | :params => {:description => "foo"}, 52 | :headers => {"Content-Type" => "application/json"}) 53 | 54 | expect(response).to have_http_status(:ok) 55 | end 56 | 57 | it "will ignore the Content-Type" do 58 | api_basic_authorize(collection_action_identifier(:groups, :create)) 59 | 60 | post(api_groups_url, 61 | :params => {:description => "foo"}, 62 | :headers => {"Content-Type" => "application/xml"}) 63 | 64 | expect(response).to have_http_status(:ok) 65 | end 66 | end 67 | 68 | describe "Response Headers" do 69 | it "returns some headers related to security" do 70 | api_basic_authorize 71 | 72 | get(api_entrypoint_url) 73 | 74 | expected = { 75 | "X-Content-Type-Options" => "nosniff", 76 | "X-Download-Options" => "noopen", 77 | "X-Frame-Options" => "SAMEORIGIN", 78 | "X-Permitted-Cross-Domain-Policies" => "none", 79 | "X-XSS-Protection" => "1; mode=block" 80 | } 81 | expect(response.headers.to_h).to include(expected) 82 | expect(content_security_policy_for("default-src")).to include("'self'") 83 | expect(content_security_policy_for("connect-src")).to include("'self'") 84 | expect(content_security_policy_for("frame-src")).to include("'self'") 85 | expect(content_security_policy_for("script-src")).to include("'unsafe-eval'", "'unsafe-inline'", "'self'") 86 | expect(content_security_policy_for("style-src")).to include("'unsafe-inline'", "'self'") 87 | end 88 | 89 | def content_security_policy_for(src) 90 | response.headers["Content-Security-Policy"].split(/\s*;\s*/).detect { |p| p.start_with?(src) } 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/requests/templates_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Templates API" do 2 | describe "POST /api/templates/:c_id with DELETE action" do 3 | it "deletes a template with an appropriate role" do 4 | api_basic_authorize(action_identifier(:templates, :delete)) 5 | template = FactoryGirl.create(:template) 6 | 7 | expect do 8 | post(api_template_url(nil, template), :params => { :action => "delete" }) 9 | end.to change(MiqTemplate, :count).by(-1) 10 | 11 | expected = { 12 | "href" => api_template_url(nil, template), 13 | "message" => "templates id: #{template.id} deleting", 14 | "success" => true 15 | } 16 | expect(response.parsed_body).to include(expected) 17 | expect(response).to have_http_status(:ok) 18 | end 19 | 20 | it "won't delete a template without an appropriate role" do 21 | api_basic_authorize 22 | template = FactoryGirl.create(:template) 23 | 24 | expect do 25 | post(api_template_url(nil, template), :params => { :action => "delete" }) 26 | end.not_to change(MiqTemplate, :count) 27 | 28 | expect(response).to have_http_status(:forbidden) 29 | end 30 | end 31 | 32 | describe "tags subcollection" do 33 | it "can list a template's tags" do 34 | template = FactoryGirl.create(:template) 35 | FactoryGirl.create(:classification_department_with_tags) 36 | Classification.classify(template, "department", "finance") 37 | api_basic_authorize 38 | 39 | get(api_template_tags_url(nil, template)) 40 | 41 | expect(response.parsed_body).to include("subcount" => 1) 42 | expect(response).to have_http_status(:ok) 43 | end 44 | 45 | it "can assign a tag to a template" do 46 | template = FactoryGirl.create(:template) 47 | FactoryGirl.create(:classification_department_with_tags) 48 | api_basic_authorize(subcollection_action_identifier(:templates, :tags, :assign)) 49 | 50 | post(api_template_tags_url(nil, template), :params => { :action => "assign", :category => "department", :name => "finance" }) 51 | 52 | expected = { 53 | "results" => [ 54 | a_hash_including( 55 | "success" => true, 56 | "message" => a_string_matching(/assigning tag/i), 57 | "tag_category" => "department", 58 | "tag_name" => "finance" 59 | ) 60 | ] 61 | } 62 | expect(response.parsed_body).to include(expected) 63 | expect(response).to have_http_status(:ok) 64 | end 65 | 66 | it "can unassign a tag from a template" do 67 | template = FactoryGirl.create(:template) 68 | FactoryGirl.create(:classification_department_with_tags) 69 | Classification.classify(template, "department", "finance") 70 | api_basic_authorize(subcollection_action_identifier(:templates, :tags, :unassign)) 71 | 72 | post( 73 | api_template_tags_url(nil, template), 74 | :params => { 75 | :action => "unassign", 76 | :category => "department", 77 | :name => "finance" 78 | } 79 | ) 80 | 81 | expected = { 82 | "results" => [ 83 | a_hash_including( 84 | "success" => true, 85 | "message" => a_string_matching(/unassigning tag/i), 86 | "tag_category" => "department", 87 | "tag_name" => "finance" 88 | ) 89 | ] 90 | } 91 | expect(response.parsed_body).to include(expected) 92 | expect(response).to have_http_status(:ok) 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /app/controllers/api/subcollections/features.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module Subcollections 3 | module Features 4 | def features_query_resource(object) 5 | object.miq_product_features 6 | end 7 | 8 | def features_assign_resource(object, _type, id = nil, data = nil) 9 | # If id != 0, an href was submitted instead of id or identifier. Set id key in data hash 10 | data['id'] = id unless id == 0 11 | 12 | # Handle a features array if passed in, otherwise look for a single feature id 13 | if data.key?('features') && data['features'].kind_of?(Array) 14 | new_features = get_product_features(data['features']) 15 | elsif data.key?('id') || data.key?('identifier') 16 | new_features = get_product_features(Array.wrap(data)) 17 | else 18 | raise BadRequestError, "Invalid feature assignment format specified." 19 | end 20 | 21 | existing_features = object.miq_product_features.dup.to_a 22 | 23 | # Find new features that already exist in the role remove from further processing 24 | existing_features.each do |existing_feature| 25 | new_features.delete_if do |new_feature| 26 | existing_feature['id'] == new_feature['id'] 27 | end 28 | end 29 | 30 | existing_features.concat(new_features) 31 | 32 | object.update_attribute(:miq_product_features, existing_features) 33 | api_log_info("Modified role #{object.name}: assigned features: #{new_features.collect(&:identifier)}") 34 | object 35 | end 36 | 37 | def features_unassign_resource(object, _type, id = nil, data = nil) 38 | # If id != 0, an href was submitted instead of id or identifier. Set id key in data hash 39 | data['id'] = id unless id == 0 40 | 41 | # Handle a features array if passed in, otherwise look for a single feature id 42 | if data.key?('features') && data['features'].kind_of?(Array) 43 | removed_features = get_product_features(data['features']) 44 | elsif data.key?('id') || data.key?('identifier') 45 | removed_features = get_product_features(Array.wrap(data)) 46 | else 47 | raise BadRequestError, "Invalid feature un-assignment format specified." 48 | end 49 | 50 | existing_features = object.miq_product_features.dup.to_a 51 | 52 | removed_features.each do |removed_feature| 53 | existing_features.delete_if do |existing_feature| 54 | removed_feature['id'] == existing_feature['id'] 55 | end 56 | end 57 | 58 | object.update_attribute(:miq_product_features, existing_features) 59 | api_log_info("Modified role #{object.name}: un-assigned features: #{existing_features.collect(&:identifier)}") 60 | object 61 | end 62 | 63 | def get_product_features(features_ids) 64 | feature_klass = collection_class(:features) 65 | 66 | new_features = features_ids.map do |feature| 67 | # Look for the feature identifier field first as this will remain constant 68 | if feature.key?('identifier') && !feature['identifier'].nil? 69 | resource_search_by_criteria('identifier', feature['identifier'], feature_klass) 70 | else # Fallback to a feature id or href field. 71 | resource_search(parse_id(feature, 'features'), 'features', feature_klass) 72 | end 73 | end 74 | new_features.compact 75 | end 76 | 77 | def resource_search_by_criteria(criteria, search_val, klass) 78 | Rbac.filtered(klass.where(criteria => search_val), :user => User.current_user).first 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class BaseController < ActionController::API 3 | TAG_NAMESPACE = "/managed".freeze 4 | 5 | # 6 | # Attributes used for identification 7 | # 8 | ID_ATTRS = %w(href id).freeze 9 | 10 | include_concern 'Parameters' 11 | include_concern 'Parser' 12 | include_concern 'Manager' 13 | include_concern 'Action' 14 | include_concern 'Logger' 15 | include_concern 'Normalizer' 16 | include_concern 'Renderer' 17 | include_concern 'Results' 18 | include_concern 'Generic' 19 | include_concern 'Authentication' 20 | include ActionController::HttpAuthentication::Basic::ControllerMethods 21 | 22 | before_action :log_request_initiated 23 | before_action :require_api_user_or_token, :except => [:options] 24 | before_action :set_gettext_locale, :set_access_control_headers, :parse_api_request, :log_api_request, 25 | :validate_api_request 26 | before_action :validate_api_action, :except => [:options] 27 | before_action :validate_response_format, :except => [:destroy] 28 | after_action :log_api_response 29 | 30 | respond_to :json 31 | 32 | # Order *Must* be from most generic to most specific 33 | rescue_from(StandardError) { |e| api_error(:internal_server_error, e) } 34 | rescue_from(NoMethodError) { |e| api_error(:internal_server_error, e) } 35 | rescue_from(ActiveRecord::RecordNotFound) { |e| api_error(:not_found, e) } 36 | rescue_from(ActiveRecord::StatementInvalid) { |e| api_error(:bad_request, e) } 37 | rescue_from(JSON::ParserError) { |e| api_error(:bad_request, e) } 38 | rescue_from(MultiJson::LoadError) { |e| api_error(:bad_request, e) } 39 | rescue_from(MiqException::MiqEVMLoginError) { |e| api_error(:unauthorized, e) } 40 | rescue_from(AuthenticationError) { |e| api_error(:unauthorized, e) } 41 | rescue_from(ForbiddenError) { |e| api_error(:forbidden, e) } 42 | rescue_from(BadRequestError) { |e| api_error(:bad_request, e) } 43 | rescue_from(NotFoundError) { |e| api_error(:not_found, e) } 44 | rescue_from(UnsupportedMediaTypeError) { |e| api_error(:unsupported_media_type, e) } 45 | rescue_from(ArgumentError) { |e| api_error(:bad_request, e) } 46 | 47 | private 48 | 49 | def set_gettext_locale 50 | FastGettext.set_locale(LocaleResolver.resolve(User.current_user, headers)) 51 | end 52 | 53 | def validate_response_format 54 | accept = request.headers["Accept"] 55 | return if accept.blank? || accept.include?("json") || accept.include?("*/*") 56 | raise UnsupportedMediaTypeError, "Invalid Response Format #{accept} requested" 57 | end 58 | 59 | def set_access_control_headers 60 | headers['Access-Control-Allow-Origin'] = '*' 61 | headers['Access-Control-Allow-Headers'] = 'origin, content-type, authorization, x-auth-token' 62 | headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS' 63 | end 64 | 65 | def collection_config 66 | @collection_config ||= CollectionConfig.new 67 | end 68 | 69 | def api_error(type, error) 70 | api_log_error("#{error.class.name}: #{error.message}") 71 | # We don't want to return the stack trace, but only log it in case of an internal error 72 | api_log_error("\n\n#{error.backtrace.join("\n")}") if type == :internal_server_error && !error.backtrace.empty? 73 | 74 | render :json => ErrorSerializer.new(type, error).serialize, :status => Rack::Utils.status_code(type) 75 | log_api_response 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Enablement for the REST API 3 | 4 | namespace :api, :path => "api(/:version)", :version => Api::VERSION_REGEX, :defaults => {:format => "json"} do 5 | root :to => "api#index", :as => :entrypoint 6 | match "/", :to => "api#options", :via => :options 7 | 8 | get "/ping" => "ping#index" 9 | 10 | # Redirect of /tasks subcollections to /request_tasks 11 | [:automation_requests, :provision_requests, :requests, :service_requests].each do |collection_name| 12 | get "/#{collection_name}/:c_id/tasks", :to => redirect { |path_params, _req| "/api/#{collection_name}/#{path_params[:c_id]}/request_tasks" } 13 | get "/#{collection_name}/:c_id/tasks/:s_id", :to => redirect { |path_params, _req| "/api/#{collection_name}/#{path_params[:c_id]}/request_tasks/#{path_params[:s_id]}" } 14 | end 15 | 16 | Api::ApiConfig.collections.each do |collection_name, collection| 17 | # OPTIONS action for each collection 18 | match collection_name.to_s, :controller => collection_name, :action => :options, :via => :options, :as => nil 19 | 20 | scope collection_name, :controller => collection_name do 21 | collection.verbs.each do |verb| 22 | if collection.options.include?(:primary) 23 | case verb 24 | when :get 25 | root :action => Api::VERBS_ACTIONS_MAP[verb], :as => collection_name 26 | else 27 | root :action => Api::VERBS_ACTIONS_MAP[verb], :via => verb 28 | end 29 | end 30 | 31 | next unless collection.options.include?(:collection) 32 | 33 | if collection.options.include?(:arbitrary_resource_path) 34 | case verb 35 | when :get 36 | root :action => :index, :as => collection_name.to_s.pluralize 37 | get "/*c_suffix", :action => :show, :as => collection_name.to_s.singularize 38 | else 39 | match "(/*c_suffix)", :action => Api::VERBS_ACTIONS_MAP[verb], :via => verb 40 | end 41 | else 42 | case verb 43 | when :get 44 | root :action => :index, :as => collection_name 45 | get "/:c_id", :action => :show, :as => collection_name.to_s.singularize 46 | when :put 47 | put "/:c_id", :action => :update 48 | when :patch 49 | patch "/:c_id", :action => :update 50 | when :delete 51 | delete "/:c_id", :action => :destroy 52 | when :post 53 | post "(/:c_id)", :action => :update 54 | end 55 | end 56 | end 57 | 58 | Array(collection.subcollections).each do |subcollection_name| 59 | Api::ApiConfig.collections[subcollection_name].verbs.each do |verb| 60 | case verb 61 | when :get 62 | get "/:c_id/#{subcollection_name}", :action => :index, :as => "#{collection_name.to_s.singularize}_#{subcollection_name.to_s.pluralize}" 63 | get "/:c_id/#{subcollection_name}/:s_id", :action => :show, :as => "#{collection_name.to_s.singularize}_#{subcollection_name.to_s.singularize}" 64 | when :put 65 | put "/:c_id/#{subcollection_name}/:s_id", :action => :update 66 | when :patch 67 | patch "/:c_id/#{subcollection_name}/:s_id", :action => :update 68 | when :delete 69 | delete "/:c_id/#{subcollection_name}/:s_id", :action => :destroy 70 | when :post 71 | post "/:c_id/#{subcollection_name}(/:s_id)", :action => :update 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/requests/policy_actions_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # REST API Request Tests - Policy Actions 3 | # 4 | # Policy Action primary collection: 5 | # /api/policy_actions 6 | # 7 | # Policy Action subcollection: 8 | # /api/policies/:id/policy_actions 9 | # 10 | describe "Policy Actions API" do 11 | let(:miq_action_guid_list) { MiqAction.pluck(:guid) } 12 | 13 | def create_actions(count) 14 | 1.upto(count) do |i| 15 | FactoryGirl.create(:miq_action, :name => "custom_action_#{i}", :description => "Custom Action #{i}") 16 | end 17 | end 18 | 19 | context "Policy Action collection" do 20 | it "query invalid action" do 21 | api_basic_authorize action_identifier(:policy_actions, :read, :resource_actions, :get) 22 | 23 | get api_policy_action_url(nil, 999_999) 24 | 25 | expect(response).to have_http_status(:not_found) 26 | end 27 | 28 | it "query policy actions with no actions defined" do 29 | api_basic_authorize collection_action_identifier(:policy_actions, :read, :get) 30 | 31 | get api_policy_actions_url 32 | 33 | expect_empty_query_result(:policy_actions) 34 | end 35 | 36 | it "query policy actions" do 37 | api_basic_authorize collection_action_identifier(:policy_actions, :read, :get) 38 | create_actions(4) 39 | 40 | get api_policy_actions_url 41 | 42 | expect_query_result(:policy_actions, 4, 4) 43 | expect_result_resources_to_include_hrefs( 44 | "resources", 45 | MiqAction.select(:id).collect { |ma| api_policy_action_url(nil, ma) } 46 | ) 47 | end 48 | 49 | it "query policy actions in expanded form" do 50 | api_basic_authorize collection_action_identifier(:policy_actions, :read, :get) 51 | create_actions(4) 52 | 53 | get api_policy_actions_url, :params => { :expand => "resources" } 54 | 55 | expect_query_result(:policy_actions, 4, 4) 56 | expect_result_resources_to_include_data("resources", "guid" => miq_action_guid_list) 57 | end 58 | end 59 | 60 | context "Policy Action subcollection" do 61 | let(:policy) { FactoryGirl.create(:miq_policy, :name => "Policy 1") } 62 | 63 | def relate_actions_to(policy) 64 | MiqAction.all.collect(&:id).each do |action_id| 65 | MiqPolicyContent.create(:miq_policy_id => policy.id, :miq_action_id => action_id) 66 | end 67 | end 68 | 69 | it "query policy actions with no actions defined" do 70 | api_basic_authorize collection_action_identifier(:policy_actions, :read, :get) 71 | 72 | get api_policy_policy_actions_url(nil, policy) 73 | 74 | expect_empty_query_result(:policy_actions) 75 | end 76 | 77 | it "query policy actions" do 78 | api_basic_authorize collection_action_identifier(:policy_actions, :read, :get) 79 | create_actions(4) 80 | relate_actions_to(policy) 81 | 82 | get api_policy_policy_actions_url(nil, policy), :params => { :expand => "resources" } 83 | 84 | expect_query_result(:policy_actions, 4, 4) 85 | expect_result_resources_to_include_data("resources", "guid" => miq_action_guid_list) 86 | end 87 | 88 | it "query policy with expanded policy actions" do 89 | api_basic_authorize action_identifier(:policies, :read, :resource_actions, :get) 90 | create_actions(4) 91 | relate_actions_to(policy) 92 | 93 | get api_policy_url(nil, policy), :params => { :expand => "policy_actions" } 94 | 95 | expect_single_resource_query("name" => policy.name, "description" => policy.description, "guid" => policy.guid) 96 | expect_result_resources_to_include_data("policy_actions", "guid" => miq_action_guid_list) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /app/controllers/api/reports_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | class ReportsController < BaseController 3 | SCHEDULE_ATTRS_TO_TRANSFORM = %w(start_date interval time_zone send_email).freeze 4 | 5 | include Subcollections::Results 6 | include Subcollections::Schedules 7 | 8 | before_action :set_additional_attributes, :only => [:index, :show] 9 | 10 | def reports_search_conditions 11 | MiqReport.for_user(User.current_user).where_clause.ast unless User.current_user.admin? 12 | end 13 | 14 | def find_reports(id) 15 | MiqReport.for_user(User.current_user).find(id) 16 | end 17 | 18 | def run_resource(type, id, _data) 19 | report = resource_search(id, type, MiqReport) 20 | report_result = MiqReportResult.find(report.queue_generate_table(:userid => User.current_user.userid)) 21 | run_report_result(true, 22 | "running report #{report.id}", 23 | :task_id => report_result.miq_task_id, 24 | :report_result_id => report_result.id) 25 | rescue => err 26 | run_report_result(false, err.to_s) 27 | end 28 | 29 | def run_report_result(success, message = nil, options = {}) 30 | res = {:success => success} 31 | res[:message] = message if message.present? 32 | add_parent_href_to_result(res) 33 | add_report_result_to_result(res, options[:report_result_id]) if options[:report_result_id].present? 34 | add_task_to_result(res, options[:task_id]) if options[:task_id].present? 35 | res 36 | end 37 | 38 | def import_resource(_type, _id, data) 39 | options = data.fetch("options", {}).symbolize_keys.merge(:user => User.current_user) 40 | result, meta = MiqReport.import_from_hash(data["report"], options) 41 | action_result(meta[:level] == :info, meta[:message], :result => result) 42 | end 43 | 44 | def schedule_resource(type, id, data) 45 | api_action(type, id) do |klass| 46 | report = resource_search(id, type, klass) 47 | schedule_reports(report, type, id, data) 48 | end 49 | end 50 | 51 | private 52 | 53 | def set_additional_attributes 54 | if @req.subcollection == "results" && (@req.s_id || @req.expand?(:resources)) && attribute_selection == "all" 55 | @additional_attributes = %w(result_set) 56 | end 57 | end 58 | 59 | def schedule_reports(report, type, id, data) 60 | desc = "scheduling of report #{report.id}" 61 | schedule = report.add_schedule fetch_schedule_data(data) 62 | res = action_result(true, desc) 63 | add_report_schedule_to_result(res, schedule.id, report.id) 64 | add_href_to_result(res, type, id) 65 | res 66 | rescue => err 67 | action_result(false, err.to_s) 68 | end 69 | 70 | def fetch_schedule_data(data) 71 | schedule_data = data.except(*SCHEDULE_ATTRS_TO_TRANSFORM) 72 | 73 | schedule_data['userid'] = User.current_user.userid 74 | schedule_data['run_at'] = { 75 | :start_time => data['start_date'], 76 | :tz => data['time_zone'], 77 | :interval => {:unit => data['interval']['unit'], 78 | :value => data['interval']['value']} 79 | } 80 | 81 | email_url_prefix = url_for(:controller => "/report", 82 | :action => "show_saved") + "/" 83 | 84 | schedule_options = { 85 | :send_email => data['send_email'] || false, 86 | :email_url_prefix => email_url_prefix, 87 | :miq_group_id => User.current_user.current_group_id 88 | } 89 | schedule_data['sched_action'] = {:method => "run_report", 90 | :options => schedule_options} 91 | schedule_data 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/requests/cloud_networks_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'Cloud Networks API' do 2 | context 'cloud networks index' do 3 | it 'rejects request without appropriate role' do 4 | api_basic_authorize 5 | 6 | get api_cloud_networks_url 7 | 8 | expect(response).to have_http_status(:forbidden) 9 | end 10 | 11 | it 'can list cloud networks' do 12 | FactoryGirl.create_list(:cloud_network, 2) 13 | api_basic_authorize collection_action_identifier(:cloud_networks, :read, :get) 14 | 15 | get api_cloud_networks_url 16 | 17 | expect_query_result(:cloud_networks, 2) 18 | expect(response).to have_http_status(:ok) 19 | end 20 | end 21 | 22 | context 'Providers cloud_networks subcollection' do 23 | let(:provider) { FactoryGirl.create(:ems_amazon_with_cloud_networks) } 24 | 25 | it 'queries Providers cloud_networks' do 26 | cloud_network_ids = provider.cloud_networks.pluck(:id) 27 | api_basic_authorize subcollection_action_identifier(:providers, :cloud_networks, :read, :get) 28 | 29 | get api_provider_cloud_networks_url(nil, provider), :params => { :expand => 'resources' } 30 | 31 | expect_query_result(:cloud_networks, 2) 32 | expect_result_resources_to_include_data('resources', 'id' => cloud_network_ids.collect(&:to_s)) 33 | end 34 | 35 | it "will not list cloud networks of a provider without the appropriate role" do 36 | api_basic_authorize 37 | 38 | get api_provider_cloud_networks_url(nil, provider) 39 | 40 | expect(response).to have_http_status(:forbidden) 41 | end 42 | 43 | it 'queries individual provider cloud_network' do 44 | api_basic_authorize(action_identifier(:cloud_networks, :read, :subresource_actions, :get)) 45 | network = provider.cloud_networks.first 46 | 47 | get(api_provider_cloud_network_url(nil, provider, network)) 48 | 49 | expect_single_resource_query('name' => network.name, 'id' => network.id.to_s, 'ems_ref' => network.ems_ref) 50 | end 51 | 52 | it "will not show the cloud network of a provider without the appropriate role" do 53 | api_basic_authorize 54 | network = provider.cloud_networks.first 55 | 56 | get(api_provider_cloud_network_url(nil, provider, network)) 57 | 58 | expect(response).to have_http_status(:forbidden) 59 | end 60 | 61 | it 'successfully returns providers on query when providers do not have cloud_networks attribute' do 62 | FactoryGirl.create(:ems_openshift) # Openshift does not respond to #cloud_networks 63 | FactoryGirl.create(:ems_amazon_with_cloud_networks) # Provider with cloud networks 64 | api_basic_authorize collection_action_identifier(:providers, :read, :get) 65 | 66 | get api_providers_url, :params => { :expand => 'resources,cloud_networks' } 67 | 68 | expected = { 69 | 'resources' => a_collection_including( 70 | a_hash_including( 71 | 'type' => 'ManageIQ::Providers::Amazon::CloudManager', 72 | 'cloud_networks' => a_collection_including 73 | ), 74 | a_hash_including( 75 | 'type' => 'ManageIQ::Providers::Openshift::ContainerManager' 76 | ) 77 | ) 78 | } 79 | expect(response).to have_http_status(:ok) 80 | expect(response.parsed_body).to include(expected) 81 | end 82 | 83 | it 'returns empty resources array when querying on a provider with no cloud_networks attribute' do 84 | openshift = FactoryGirl.create(:ems_openshift) 85 | api_basic_authorize subcollection_action_identifier(:providers, :cloud_networks, :read, :get) 86 | 87 | get(api_provider_cloud_networks_url(nil, openshift), :params => { :expand => 'resources' }) 88 | 89 | expected = { 90 | 'name' => 'cloud_networks', 91 | 'count' => 0, 92 | 'subcount' => 0, 93 | 'resources' => [] 94 | } 95 | 96 | expect(response.parsed_body).to include(expected) 97 | expect(response).to have_http_status(:ok) 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/requests/hosts_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "hosts API" do 2 | describe "editing a host's password" do 3 | context "with an appropriate role" do 4 | it "can edit the password on a host" do 5 | host = FactoryGirl.create(:host_with_authentication) 6 | api_basic_authorize action_identifier(:hosts, :edit) 7 | options = {:credentials => {:authtype => "default", :password => "abc123"}} 8 | 9 | expect do 10 | post api_host_url(nil, host), :params => gen_request(:edit, options) 11 | end.to change { host.reload.authentication_password(:default) }.to("abc123") 12 | expect(response).to have_http_status(:ok) 13 | end 14 | 15 | it "will update the default authentication if no type is given" do 16 | host = FactoryGirl.create(:host_with_authentication) 17 | api_basic_authorize action_identifier(:hosts, :edit) 18 | options = {:credentials => {:password => "abc123"}} 19 | 20 | expect do 21 | post api_host_url(nil, host), :params => gen_request(:edit, options) 22 | end.to change { host.reload.authentication_password(:default) }.to("abc123") 23 | expect(response).to have_http_status(:ok) 24 | end 25 | 26 | it "sending non-credentials attributes will result in a bad request error" do 27 | host = FactoryGirl.create(:host_with_authentication) 28 | api_basic_authorize action_identifier(:hosts, :edit) 29 | options = {:name => "new name"} 30 | 31 | expect do 32 | post api_host_url(nil, host), :params => gen_request(:edit, options) 33 | end.not_to change { host.reload.name } 34 | expect(response).to have_http_status(:bad_request) 35 | end 36 | 37 | it "can update passwords on multiple hosts by href" do 38 | host1 = FactoryGirl.create(:host_with_authentication) 39 | host2 = FactoryGirl.create(:host_with_authentication) 40 | api_basic_authorize action_identifier(:hosts, :edit) 41 | options = [ 42 | {:href => api_host_url(nil, host1), :credentials => {:password => "abc123"}}, 43 | {:href => api_host_url(nil, host2), :credentials => {:password => "def456"}} 44 | ] 45 | 46 | post api_hosts_url, :params => gen_request(:edit, options) 47 | expect(response).to have_http_status(:ok) 48 | expect(host1.reload.authentication_password(:default)).to eq("abc123") 49 | expect(host2.reload.authentication_password(:default)).to eq("def456") 50 | end 51 | 52 | it "can update passwords on multiple hosts by id" do 53 | host1 = FactoryGirl.create(:host_with_authentication) 54 | host2 = FactoryGirl.create(:host_with_authentication) 55 | api_basic_authorize action_identifier(:hosts, :edit) 56 | options = [ 57 | {:id => host1.id, :credentials => {:password => "abc123"}}, 58 | {:id => host2.id, :credentials => {:password => "def456"}} 59 | ] 60 | 61 | post api_hosts_url, :params => gen_request(:edit, options) 62 | expect(response).to have_http_status(:ok) 63 | expect(host1.reload.authentication_password(:default)).to eq("abc123") 64 | expect(host2.reload.authentication_password(:default)).to eq("def456") 65 | end 66 | end 67 | 68 | context "without an appropriate role" do 69 | it "cannot edit the password on a host" do 70 | host = FactoryGirl.create(:host_with_authentication) 71 | api_basic_authorize 72 | options = {:credentials => {:authtype => "default", :password => "abc123"}} 73 | 74 | expect do 75 | post api_host_url(nil, host), :params => gen_request(:edit, options) 76 | end.not_to change { host.reload.authentication_password(:default) } 77 | expect(response).to have_http_status(:forbidden) 78 | end 79 | end 80 | 81 | context 'OPTIONS /api/hosts' do 82 | it 'returns hosts node_types' do 83 | expected_data = {"node_types" => Host.node_types.to_s} 84 | 85 | options(api_hosts_url) 86 | expect_options_results(:hosts, expected_data) 87 | end 88 | end 89 | end 90 | end 91 | --------------------------------------------------------------------------------