manageiq-api-common 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|