manageiq-api-common 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +62 -0
- data/Rakefile +18 -0
- data/app/models/authentication.rb +19 -0
- data/app/models/concerns/encryption_concern.rb +52 -0
- data/app/models/encryption.rb +13 -0
- data/lib/generators/shared_utilities/migration_generator.rb +79 -0
- data/lib/generators/shared_utilities/orm_helper.rb +25 -0
- data/lib/generators/shared_utilities/templates/migration.rb +27 -0
- data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
- data/lib/manageiq-api-common.rb +1 -0
- data/lib/manageiq/api/common.rb +13 -0
- data/lib/manageiq/api/common/api_error.rb +21 -0
- data/lib/manageiq/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/manageiq/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/manageiq/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/manageiq/api/common/application_controller_mixins/parameters.rb +113 -0
- data/lib/manageiq/api/common/application_controller_mixins/request_body_validation.rb +61 -0
- data/lib/manageiq/api/common/application_controller_mixins/request_path.rb +75 -0
- data/lib/manageiq/api/common/engine.rb +20 -0
- data/lib/manageiq/api/common/entitlement.rb +35 -0
- data/lib/manageiq/api/common/error_document.rb +29 -0
- data/lib/manageiq/api/common/filter.rb +160 -0
- data/lib/manageiq/api/common/graphql.rb +117 -0
- data/lib/manageiq/api/common/graphql/associated_records.rb +44 -0
- data/lib/manageiq/api/common/graphql/association_loader.rb +35 -0
- data/lib/manageiq/api/common/graphql/generator.rb +149 -0
- data/lib/manageiq/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/manageiq/api/common/graphql/templates/query_type.erb +47 -0
- data/lib/manageiq/api/common/graphql/templates/schema.erb +6 -0
- data/lib/manageiq/api/common/graphql/types/big_int.rb +23 -0
- data/lib/manageiq/api/common/graphql/types/date_time.rb +16 -0
- data/lib/manageiq/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/manageiq/api/common/inflections.rb +28 -0
- data/lib/manageiq/api/common/logging.rb +17 -0
- data/lib/manageiq/api/common/metrics.rb +39 -0
- data/lib/manageiq/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/manageiq/api/common/open_api.rb +2 -0
- data/lib/manageiq/api/common/open_api/docs.rb +54 -0
- data/lib/manageiq/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/manageiq/api/common/open_api/docs/doc_v3.rb +92 -0
- data/lib/manageiq/api/common/open_api/docs/object_definition.rb +27 -0
- data/lib/manageiq/api/common/open_api/generator.rb +441 -0
- data/lib/manageiq/api/common/open_api/serializer.rb +31 -0
- data/lib/manageiq/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/manageiq/api/common/paginated_response.rb +92 -0
- data/lib/manageiq/api/common/request.rb +107 -0
- data/lib/manageiq/api/common/routing.rb +26 -0
- data/lib/manageiq/api/common/user.rb +48 -0
- data/lib/manageiq/api/common/version.rb +7 -0
- data/lib/tasks/manageiq/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/user_header_spec_helper.rb +62 -0
- metadata +375 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module SharedUtilities
|
2
|
+
module OrmHelper
|
3
|
+
private
|
4
|
+
|
5
|
+
def model_exists?
|
6
|
+
File.exist?(File.join(destination_root, model_path))
|
7
|
+
end
|
8
|
+
|
9
|
+
def migration_exists?(table_name)
|
10
|
+
Dir.glob("#{File.join(destination_root, migration_path)}/[0-9]*_*.rb").grep(/\d+_add_devise_to_#{table_name}.rb$/).first
|
11
|
+
end
|
12
|
+
|
13
|
+
def migration_path
|
14
|
+
if Rails.version >= '5.0.3'
|
15
|
+
db_migrate_path
|
16
|
+
else
|
17
|
+
@migration_path ||= File.join("db", "migrate")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def model_path
|
22
|
+
@model_path ||= File.join("app", "models", "#{file_path}.rb")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Create<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def change
|
3
|
+
create_table :<%= table_name %><%= primary_key_type %> do |t|
|
4
|
+
<%= migration_data -%>
|
5
|
+
|
6
|
+
<% attributes.each do |attribute| -%>
|
7
|
+
t.<%= attribute.type %> :<%= attribute.name %>
|
8
|
+
<% end -%>
|
9
|
+
t.references "resource", :polymorphic => true, :index => true
|
10
|
+
t.string :name
|
11
|
+
t.string :authtype
|
12
|
+
t.string :status
|
13
|
+
t.string :status_details
|
14
|
+
t.bigint :tenant_id
|
15
|
+
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :encryptions<%= primary_key_type %> do |t|
|
20
|
+
t.references "authentication", :index => true
|
21
|
+
t.string :secret
|
22
|
+
t.bigint :tenant_id
|
23
|
+
|
24
|
+
t.timestamps
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class UpdateOn<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
|
2
|
+
def self.up
|
3
|
+
change_table :<%= table_name %> do |t|
|
4
|
+
<%= migration_data -%>
|
5
|
+
|
6
|
+
<% attributes.each do |attribute| -%>
|
7
|
+
t.<%= attribute.type %> :<%= attribute.name %>
|
8
|
+
<% end -%>
|
9
|
+
|
10
|
+
# Uncomment below if timestamps were not included in your original model.
|
11
|
+
# t.timestamps null: false
|
12
|
+
end
|
13
|
+
|
14
|
+
# Assumption is made there is no 'encryptions' table, so creating it here
|
15
|
+
create_table :encryptions<%= primary_key_type %> do |t|
|
16
|
+
t.references "<%= table_name %>", :index => true
|
17
|
+
t.string :secret
|
18
|
+
t.bigint :tenant_id
|
19
|
+
|
20
|
+
t.timestamps
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.down
|
25
|
+
# By default, we don't want to make any assumption about how to roll back a migration when your
|
26
|
+
# model already existed. Please edit below which fields you would like to remove in this migration.
|
27
|
+
raise ActiveRecord::IrreversibleMigration
|
28
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'manageiq/api/common'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "manageiq/api/common/api_error"
|
2
|
+
require "manageiq/api/common/engine"
|
3
|
+
require "manageiq/api/common/entitlement"
|
4
|
+
require "manageiq/api/common/error_document"
|
5
|
+
require "manageiq/api/common/filter"
|
6
|
+
require "manageiq/api/common/inflections"
|
7
|
+
require "manageiq/api/common/logging"
|
8
|
+
require "manageiq/api/common/metrics"
|
9
|
+
require "manageiq/api/common/open_api"
|
10
|
+
require "manageiq/api/common/option_redirect_enhancements"
|
11
|
+
require "manageiq/api/common/request"
|
12
|
+
require "manageiq/api/common/routing"
|
13
|
+
require "manageiq/api/common/user"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class ApiError < StandardError
|
5
|
+
attr_reader :errors
|
6
|
+
|
7
|
+
def initialize(status, detail)
|
8
|
+
@errors = ErrorDocument.new.add(status, detail)
|
9
|
+
end
|
10
|
+
|
11
|
+
def status
|
12
|
+
@errors.status
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(status, detail)
|
16
|
+
@errors.add(status, detail)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module ApplicationControllerMixins
|
5
|
+
module ApiDoc
|
6
|
+
def self.included(other)
|
7
|
+
other.extend(self::ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def api_doc
|
13
|
+
self.class.send(:api_doc)
|
14
|
+
end
|
15
|
+
|
16
|
+
def api_doc_definition
|
17
|
+
self.class.send(:api_doc_definition)
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
private
|
22
|
+
|
23
|
+
def api_doc
|
24
|
+
@api_doc ||= ::ManageIQ::API::Common::OpenApi::Docs.instance[api_version[1..-1].sub(/x/, ".")]
|
25
|
+
end
|
26
|
+
|
27
|
+
def api_doc_definition
|
28
|
+
@api_doc_definition ||= api_doc.definitions[model.name]
|
29
|
+
end
|
30
|
+
|
31
|
+
def api_version
|
32
|
+
@api_version ||= name.split("::")[1].downcase
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module ApplicationControllerMixins
|
5
|
+
module Common
|
6
|
+
def self.included(other)
|
7
|
+
other.extend(self::ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def model
|
13
|
+
self.class.send(:model)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
private
|
18
|
+
|
19
|
+
def model
|
20
|
+
@model ||= controller_name.classify.constantize
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module ApplicationControllerMixins
|
5
|
+
module Parameters
|
6
|
+
|
7
|
+
def self.included(other)
|
8
|
+
other.include(OpenapiEnabled)
|
9
|
+
end
|
10
|
+
|
11
|
+
def params_for_create
|
12
|
+
check_if_openapi_enabled
|
13
|
+
# We already validate this with OpenAPI validator, that validates every request, so we shouldn't do it again here.
|
14
|
+
body_params.permit!
|
15
|
+
end
|
16
|
+
|
17
|
+
def safe_params_for_list
|
18
|
+
check_if_openapi_enabled
|
19
|
+
# :limit & :offset can be passed in for pagination purposes, but shouldn't show up as params for filtering purposes
|
20
|
+
@safe_params_for_list ||= params.merge(params_for_polymorphic_subcollection).permit(*permitted_params, :filter => {})
|
21
|
+
end
|
22
|
+
|
23
|
+
def permitted_params
|
24
|
+
check_if_openapi_enabled
|
25
|
+
api_doc_definition.all_attributes + [:limit, :offset] + [subcollection_foreign_key]
|
26
|
+
end
|
27
|
+
|
28
|
+
def subcollection_foreign_key
|
29
|
+
"#{request_path_parts["primary_collection_name"].singularize}_id"
|
30
|
+
end
|
31
|
+
|
32
|
+
def params_for_polymorphic_subcollection
|
33
|
+
return {} unless subcollection?
|
34
|
+
return {} unless reflection = primary_collection_model&.reflect_on_association(request_path_parts["subcollection_name"])
|
35
|
+
return {} unless as = reflection.options[:as]
|
36
|
+
{"#{as}_type" => primary_collection_model.name, "#{as}_id" => request_path_parts["primary_collection_id"]}
|
37
|
+
end
|
38
|
+
|
39
|
+
def primary_collection_model
|
40
|
+
@primary_collection_model ||= request_path_parts["primary_collection_name"].singularize.classify.safe_constantize
|
41
|
+
end
|
42
|
+
|
43
|
+
def params_for_list
|
44
|
+
check_if_openapi_enabled
|
45
|
+
safe_params = safe_params_for_list.slice(*all_attributes_for_index)
|
46
|
+
if safe_params[subcollection_foreign_key_using_through_relation]
|
47
|
+
# If this is a through relation, we need to replace the :foreign_key by the foreign key with right table
|
48
|
+
# information. So e.g. :container_images with :tags subcollection will have {:container_image_id => ID} and we need
|
49
|
+
# to replace it with {:container_images_tags => {:container_image_id => ID}}, where :container_images_tags is the
|
50
|
+
# name of the mapping table.
|
51
|
+
safe_params[through_relation_klass.table_name.to_sym] = {
|
52
|
+
subcollection_foreign_key_using_through_relation => safe_params.delete(subcollection_foreign_key_using_through_relation)
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
safe_params
|
57
|
+
end
|
58
|
+
|
59
|
+
def through_relation_klass
|
60
|
+
check_if_openapi_enabled
|
61
|
+
return unless subcollection?
|
62
|
+
return unless reflection = primary_collection_model&.reflect_on_association(request_path_parts["subcollection_name"])
|
63
|
+
return unless through = reflection.options[:through]
|
64
|
+
|
65
|
+
primary_collection_model&.reflect_on_association(through).klass
|
66
|
+
end
|
67
|
+
|
68
|
+
def through_relation_name
|
69
|
+
check_if_openapi_enabled
|
70
|
+
# Through relation name taken from the subcollection model side, so we can use this for table join.
|
71
|
+
return unless through_relation_klass
|
72
|
+
return unless through_relation_association = model.reflect_on_all_associations.detect { |x| !x.polymorphic? && x.klass == through_relation_klass }
|
73
|
+
|
74
|
+
through_relation_association.name
|
75
|
+
end
|
76
|
+
|
77
|
+
def subcollection_foreign_key_using_through_relation
|
78
|
+
return unless through_relation_klass
|
79
|
+
|
80
|
+
subcollection_foreign_key
|
81
|
+
end
|
82
|
+
|
83
|
+
def all_attributes_for_index
|
84
|
+
check_if_openapi_enabled
|
85
|
+
api_doc_definition.all_attributes + [subcollection_foreign_key_using_through_relation]
|
86
|
+
end
|
87
|
+
|
88
|
+
def filtered
|
89
|
+
check_if_openapi_enabled
|
90
|
+
ManageIQ::API::Common::Filter.new(model, safe_params_for_list[:filter], api_doc_definition).apply
|
91
|
+
end
|
92
|
+
|
93
|
+
def pagination_limit
|
94
|
+
safe_params_for_list[:limit]
|
95
|
+
end
|
96
|
+
|
97
|
+
def pagination_offset
|
98
|
+
safe_params_for_list[:offset]
|
99
|
+
end
|
100
|
+
|
101
|
+
def params_for_update
|
102
|
+
check_if_openapi_enabled
|
103
|
+
body_params.permit(*api_doc_definition.all_attributes - api_doc_definition.read_only_attributes)
|
104
|
+
end
|
105
|
+
|
106
|
+
def check_if_openapi_enabled
|
107
|
+
raise ArgumentError, "Openapi not enabled" unless self.class.openapi_enabled
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module ApplicationControllerMixins
|
5
|
+
module RequestBodyValidation
|
6
|
+
class BodyParseError < ::RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.included(other)
|
10
|
+
ActionController::Parameters.action_on_unpermitted_parameters = :raise
|
11
|
+
|
12
|
+
other.include(OpenapiEnabled)
|
13
|
+
|
14
|
+
other.before_action(:validate_request)
|
15
|
+
|
16
|
+
other.rescue_from(ActionController::UnpermittedParameters) do |exception|
|
17
|
+
error_document = ManageIQ::API::Common::ErrorDocument.new.add(400, exception.message)
|
18
|
+
render :json => error_document.to_h, :status => error_document.status
|
19
|
+
end
|
20
|
+
|
21
|
+
other.rescue_from(ManageIQ::API::Common::ApplicationControllerMixins::RequestBodyValidation::BodyParseError) do |_exception|
|
22
|
+
error_document = ManageIQ::API::Common::ErrorDocument.new.add(400, "Failed to parse request body, expected JSON")
|
23
|
+
render :json => error_document.to_h, :status => error_document.status
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def body_params
|
30
|
+
@body_params ||= begin
|
31
|
+
raw_body = request.body.read
|
32
|
+
parsed_body = raw_body.blank? ? {} : JSON.parse(raw_body)
|
33
|
+
ActionController::Parameters.new(parsed_body).permit!
|
34
|
+
rescue JSON::ParserError
|
35
|
+
raise ManageIQ::API::Common::ApplicationControllerMixins::RequestBodyValidation::BodyParseError
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validates against openapi.json
|
40
|
+
# - only for HTTP POST/PATCH
|
41
|
+
def validate_request
|
42
|
+
return unless request.post? || request.patch?
|
43
|
+
return unless self.class.openapi_enabled
|
44
|
+
|
45
|
+
api_version = self.class.send(:api_version)[1..-1].sub(/x/, ".")
|
46
|
+
|
47
|
+
self.class.send(:api_doc).validate!(
|
48
|
+
request.method,
|
49
|
+
request.path,
|
50
|
+
api_version,
|
51
|
+
body_params.as_json
|
52
|
+
)
|
53
|
+
rescue OpenAPIParser::OpenAPIError => exception
|
54
|
+
error_document = ManageIQ::API::Common::ErrorDocument.new.add(400, exception.message)
|
55
|
+
render :json => error_document.to_h, :status => :bad_request
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
module ApplicationControllerMixins
|
5
|
+
module RequestPath
|
6
|
+
class RequestPathError < ::RuntimeError
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.included(other)
|
10
|
+
other.extend(self::ClassMethods)
|
11
|
+
|
12
|
+
other.before_action(:validate_primary_collection_id)
|
13
|
+
|
14
|
+
other.rescue_from(ManageIQ::API::Common::ApplicationControllerMixins::RequestPath::RequestPathError) do |exception|
|
15
|
+
error_document = ManageIQ::API::Common::ErrorDocument.new.add(400, exception.message)
|
16
|
+
render :json => error_document.to_h, :status => error_document.status
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def request_path
|
21
|
+
request.env["REQUEST_URI"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def request_path_parts
|
25
|
+
@request_path_parts ||= begin
|
26
|
+
path, _query = request_path.split("?")
|
27
|
+
path.match(/\/(?<full_version_string>v\d+.\d+)\/(?<primary_collection_name>\w+)(\/(?<primary_collection_id>[^\/]+)(\/(?<subcollection_name>\w+))?)?/)&.named_captures || {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def subcollection?
|
32
|
+
!!(request_path_parts["subcollection_name"] && request_path_parts["primary_collection_id"] && request_path_parts["primary_collection_name"])
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def id_regexp
|
38
|
+
self.class.send(:id_regexp, request_path_parts["primary_collection_name"])
|
39
|
+
end
|
40
|
+
|
41
|
+
def validate_primary_collection_id
|
42
|
+
id = request_path_parts["primary_collection_id"]
|
43
|
+
return if id.blank?
|
44
|
+
|
45
|
+
raise RequestPathError, "ID is invalid" unless id.match(id_regexp)
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
private
|
50
|
+
|
51
|
+
def id_regexp(primary_collection_name)
|
52
|
+
@id_regexp ||= begin
|
53
|
+
id_parameter = id_parameter_from_api_doc(primary_collection_name)
|
54
|
+
id_parameter ? id_parameter.fetch_path("schema", "pattern") : /^\d+$/
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def id_parameter_from_api_doc(primary_collection_name)
|
59
|
+
# Find the id parameter in the documented route
|
60
|
+
id_parameter = api_doc.paths.fetch_path("/#{primary_collection_name}/{id}", "get", "parameters", 0)
|
61
|
+
# The route isn't documented, return nil
|
62
|
+
return unless id_parameter
|
63
|
+
|
64
|
+
# Return the id parameter or resolve the reference to it and return that
|
65
|
+
reference = id_parameter["$ref"]
|
66
|
+
return id_parameter unless reference
|
67
|
+
|
68
|
+
api_doc.parameters[reference.split("parameters/").last]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|