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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +62 -0
  4. data/Rakefile +18 -0
  5. data/app/models/authentication.rb +19 -0
  6. data/app/models/concerns/encryption_concern.rb +52 -0
  7. data/app/models/encryption.rb +13 -0
  8. data/lib/generators/shared_utilities/migration_generator.rb +79 -0
  9. data/lib/generators/shared_utilities/orm_helper.rb +25 -0
  10. data/lib/generators/shared_utilities/templates/migration.rb +27 -0
  11. data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
  12. data/lib/manageiq-api-common.rb +1 -0
  13. data/lib/manageiq/api/common.rb +13 -0
  14. data/lib/manageiq/api/common/api_error.rb +21 -0
  15. data/lib/manageiq/api/common/application_controller_mixins/api_doc.rb +39 -0
  16. data/lib/manageiq/api/common/application_controller_mixins/common.rb +27 -0
  17. data/lib/manageiq/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
  18. data/lib/manageiq/api/common/application_controller_mixins/parameters.rb +113 -0
  19. data/lib/manageiq/api/common/application_controller_mixins/request_body_validation.rb +61 -0
  20. data/lib/manageiq/api/common/application_controller_mixins/request_path.rb +75 -0
  21. data/lib/manageiq/api/common/engine.rb +20 -0
  22. data/lib/manageiq/api/common/entitlement.rb +35 -0
  23. data/lib/manageiq/api/common/error_document.rb +29 -0
  24. data/lib/manageiq/api/common/filter.rb +160 -0
  25. data/lib/manageiq/api/common/graphql.rb +117 -0
  26. data/lib/manageiq/api/common/graphql/associated_records.rb +44 -0
  27. data/lib/manageiq/api/common/graphql/association_loader.rb +35 -0
  28. data/lib/manageiq/api/common/graphql/generator.rb +149 -0
  29. data/lib/manageiq/api/common/graphql/templates/model_type.erb +35 -0
  30. data/lib/manageiq/api/common/graphql/templates/query_type.erb +47 -0
  31. data/lib/manageiq/api/common/graphql/templates/schema.erb +6 -0
  32. data/lib/manageiq/api/common/graphql/types/big_int.rb +23 -0
  33. data/lib/manageiq/api/common/graphql/types/date_time.rb +16 -0
  34. data/lib/manageiq/api/common/graphql/types/query_filter.rb +16 -0
  35. data/lib/manageiq/api/common/inflections.rb +28 -0
  36. data/lib/manageiq/api/common/logging.rb +17 -0
  37. data/lib/manageiq/api/common/metrics.rb +39 -0
  38. data/lib/manageiq/api/common/middleware/web_server_metrics.rb +62 -0
  39. data/lib/manageiq/api/common/open_api.rb +2 -0
  40. data/lib/manageiq/api/common/open_api/docs.rb +54 -0
  41. data/lib/manageiq/api/common/open_api/docs/component_collection.rb +67 -0
  42. data/lib/manageiq/api/common/open_api/docs/doc_v3.rb +92 -0
  43. data/lib/manageiq/api/common/open_api/docs/object_definition.rb +27 -0
  44. data/lib/manageiq/api/common/open_api/generator.rb +441 -0
  45. data/lib/manageiq/api/common/open_api/serializer.rb +31 -0
  46. data/lib/manageiq/api/common/option_redirect_enhancements.rb +23 -0
  47. data/lib/manageiq/api/common/paginated_response.rb +92 -0
  48. data/lib/manageiq/api/common/request.rb +107 -0
  49. data/lib/manageiq/api/common/routing.rb +26 -0
  50. data/lib/manageiq/api/common/user.rb +48 -0
  51. data/lib/manageiq/api/common/version.rb +7 -0
  52. data/lib/tasks/manageiq/api/common_tasks.rake +4 -0
  53. data/spec/support/default_as_json.rb +17 -0
  54. data/spec/support/requests_spec_helper.rb +7 -0
  55. data/spec/support/user_header_spec_helper.rb +62 -0
  56. 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,13 @@
1
+ module ManageIQ
2
+ module API
3
+ module Common
4
+ module ApplicationControllerMixins
5
+ module OpenapiEnabled
6
+ def self.included(other)
7
+ other.class_attribute :openapi_enabled, :default => true
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
13
+ 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