manageiq-api-common 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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