insights-api-common 3.0.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +102 -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/insights.rb +1 -0
  13. data/lib/insights/api/common.rb +12 -0
  14. data/lib/insights/api/common/application_controller_mixins/api_doc.rb +39 -0
  15. data/lib/insights/api/common/application_controller_mixins/common.rb +27 -0
  16. data/lib/insights/api/common/application_controller_mixins/exception_handling.rb +41 -0
  17. data/lib/insights/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
  18. data/lib/insights/api/common/application_controller_mixins/parameters.rb +134 -0
  19. data/lib/insights/api/common/application_controller_mixins/request_body_validation.rb +48 -0
  20. data/lib/insights/api/common/application_controller_mixins/request_parameter_validation.rb +29 -0
  21. data/lib/insights/api/common/application_controller_mixins/request_path.rb +70 -0
  22. data/lib/insights/api/common/engine.rb +20 -0
  23. data/lib/insights/api/common/entitlement.rb +37 -0
  24. data/lib/insights/api/common/error_document.rb +29 -0
  25. data/lib/insights/api/common/filter.rb +175 -0
  26. data/lib/insights/api/common/graphql.rb +127 -0
  27. data/lib/insights/api/common/graphql/associated_records.rb +44 -0
  28. data/lib/insights/api/common/graphql/association_loader.rb +35 -0
  29. data/lib/insights/api/common/graphql/generator.rb +148 -0
  30. data/lib/insights/api/common/graphql/templates/model_type.erb +35 -0
  31. data/lib/insights/api/common/graphql/templates/query_type.erb +49 -0
  32. data/lib/insights/api/common/graphql/templates/schema.erb +6 -0
  33. data/lib/insights/api/common/graphql/types/big_int.rb +23 -0
  34. data/lib/insights/api/common/graphql/types/date_time.rb +16 -0
  35. data/lib/insights/api/common/graphql/types/query_filter.rb +16 -0
  36. data/lib/insights/api/common/graphql/types/query_sort_by.rb +16 -0
  37. data/lib/insights/api/common/inflections.rb +28 -0
  38. data/lib/insights/api/common/logging.rb +17 -0
  39. data/lib/insights/api/common/metrics.rb +39 -0
  40. data/lib/insights/api/common/middleware/web_server_metrics.rb +62 -0
  41. data/lib/insights/api/common/open_api.rb +2 -0
  42. data/lib/insights/api/common/open_api/docs.rb +54 -0
  43. data/lib/insights/api/common/open_api/docs/component_collection.rb +67 -0
  44. data/lib/insights/api/common/open_api/docs/doc_v3.rb +102 -0
  45. data/lib/insights/api/common/open_api/docs/object_definition.rb +39 -0
  46. data/lib/insights/api/common/open_api/generator.rb +520 -0
  47. data/lib/insights/api/common/open_api/serializer.rb +31 -0
  48. data/lib/insights/api/common/option_redirect_enhancements.rb +23 -0
  49. data/lib/insights/api/common/paginated_response.rb +108 -0
  50. data/lib/insights/api/common/rbac/access.rb +66 -0
  51. data/lib/insights/api/common/rbac/acl.rb +74 -0
  52. data/lib/insights/api/common/rbac/policies.rb +33 -0
  53. data/lib/insights/api/common/rbac/query_shared_resource.rb +45 -0
  54. data/lib/insights/api/common/rbac/roles.rb +77 -0
  55. data/lib/insights/api/common/rbac/seed.rb +140 -0
  56. data/lib/insights/api/common/rbac/service.rb +67 -0
  57. data/lib/insights/api/common/rbac/share_resource.rb +60 -0
  58. data/lib/insights/api/common/rbac/unshare_resource.rb +32 -0
  59. data/lib/insights/api/common/rbac/utilities.rb +30 -0
  60. data/lib/insights/api/common/request.rb +111 -0
  61. data/lib/insights/api/common/routing.rb +26 -0
  62. data/lib/insights/api/common/user.rb +48 -0
  63. data/lib/insights/api/common/version.rb +7 -0
  64. data/lib/tasks/insights/api/common_tasks.rake +4 -0
  65. data/spec/support/default_as_json.rb +17 -0
  66. data/spec/support/rbac_shared_contexts.rb +44 -0
  67. data/spec/support/requests_spec_helper.rb +7 -0
  68. data/spec/support/service_spec_helper.rb +26 -0
  69. data/spec/support/user_header_spec_helper.rb +68 -0
  70. metadata +403 -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 'insights/api/common'
@@ -0,0 +1,12 @@
1
+ require "insights/api/common/engine"
2
+ require "insights/api/common/entitlement"
3
+ require "insights/api/common/error_document"
4
+ require "insights/api/common/filter"
5
+ require "insights/api/common/inflections"
6
+ require "insights/api/common/logging"
7
+ require "insights/api/common/metrics"
8
+ require "insights/api/common/open_api"
9
+ require "insights/api/common/option_redirect_enhancements"
10
+ require "insights/api/common/request"
11
+ require "insights/api/common/routing"
12
+ require "insights/api/common/user"
@@ -0,0 +1,39 @@
1
+ module Insights
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 ||= ::Insights::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 Insights
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,41 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module ApplicationControllerMixins
5
+ module ExceptionHandling
6
+ DEFAULT_ERROR_CODE = 400
7
+
8
+ def self.included(other)
9
+ other.rescue_from(StandardError, RuntimeError) do |exception|
10
+ errors = Insights::API::Common::ErrorDocument.new.tap do |error_document|
11
+ exception_list_from(exception).each do |exc|
12
+ code = exc.respond_to?(:code) ? exc.code : error_code_from_class(exc)
13
+ error_document.add(code, "#{exc.class}: #{exc.message}")
14
+ end
15
+ end
16
+
17
+ render :json => errors.to_h, :status => error_code_from_class(exception)
18
+ end
19
+ end
20
+
21
+ def exception_list_from(exception)
22
+ [].tap do |arr|
23
+ until exception.nil?
24
+ arr << exception
25
+ exception = exception.cause
26
+ end
27
+ end
28
+ end
29
+
30
+ def error_code_from_class(exception)
31
+ if ActionDispatch::ExceptionWrapper.rescue_responses.key?(exception.class.to_s)
32
+ ActionDispatch::ExceptionWrapper.rescue_responses[exception.class.to_s]
33
+ else
34
+ DEFAULT_ERROR_CODE
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ module Insights
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,134 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module ApplicationControllerMixins
5
+ module Parameters
6
+ def self.included(other)
7
+ other.include(OpenapiEnabled)
8
+ end
9
+
10
+ def params_for_create
11
+ check_if_openapi_enabled
12
+ # We already validate this with OpenAPI validator, that validates every request, so we shouldn't do it again here.
13
+ body_params.permit!
14
+ end
15
+
16
+ def safe_params_for_list
17
+ check_if_openapi_enabled
18
+ # :limit & :offset can be passed in for pagination purposes, but shouldn't show up as params for filtering purposes
19
+ @safe_params_for_list ||= params.merge(params_for_polymorphic_subcollection).permit(*permitted_params, :filter => {}, :sort_by => [])
20
+ end
21
+
22
+ def permitted_params
23
+ check_if_openapi_enabled
24
+ api_doc_definition.all_attributes + [:limit, :offset, :sort_by] + [subcollection_foreign_key]
25
+ end
26
+
27
+ def subcollection_foreign_key
28
+ "#{request_path_parts["primary_collection_name"].singularize}_id"
29
+ end
30
+
31
+ def params_for_polymorphic_subcollection
32
+ return {} unless subcollection?
33
+ return {} unless reflection = primary_collection_model&.reflect_on_association(request_path_parts["subcollection_name"])
34
+ return {} unless as = reflection.options[:as]
35
+
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
+ Insights::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 query_sort_by
102
+ safe_params_for_list[:sort_by]
103
+ end
104
+
105
+ def params_for_update
106
+ check_if_openapi_enabled
107
+ # We already validate this with OpenAPI validator, here only to satisfy the strong parameters check
108
+ attr_list = *api_doc_definition.all_attributes - api_doc_definition.read_only_attributes
109
+ strong_params_hash = sanctified_permit_param(api_doc_definition, attr_list)
110
+ body_params.permit(strong_params_hash)
111
+ end
112
+
113
+ def sanctified_permit_param(api_doc_definition, attributes)
114
+ api_doc_definition['properties'].each_with_object([]) do |(k, v), memo|
115
+ next unless attributes.each { |attr| attr.include?(k) }
116
+
117
+ memo << if v['type'] == 'array'
118
+ { k => [] }
119
+ elsif v['type'] == 'object'
120
+ { k => {} }
121
+ else
122
+ k
123
+ end
124
+ end
125
+ end
126
+
127
+ def check_if_openapi_enabled
128
+ raise ArgumentError, "Openapi not enabled" unless self.class.openapi_enabled
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,48 @@
1
+ module Insights
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
+ end
16
+
17
+ private
18
+
19
+ def body_params
20
+ @body_params ||= begin
21
+ raw_body = request.body.read
22
+ parsed_body = raw_body.blank? ? {} : JSON.parse(raw_body)
23
+ ActionController::Parameters.new(parsed_body).permit!
24
+ rescue JSON::ParserError
25
+ raise Insights::API::Common::ApplicationControllerMixins::RequestBodyValidation::BodyParseError, "Failed to parse request body, expected JSON"
26
+ end
27
+ end
28
+
29
+ # Validates against openapi.json
30
+ # - only for HTTP POST/PATCH
31
+ def validate_request
32
+ return unless request.post? || request.patch?
33
+ return unless self.class.openapi_enabled
34
+
35
+ api_version = self.class.send(:api_version)[1..-1].sub(/x/, ".")
36
+
37
+ self.class.send(:api_doc).validate!(
38
+ request.method,
39
+ request.path,
40
+ api_version,
41
+ body_params.as_json
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module ApplicationControllerMixins
5
+ module RequestParameterValidation
6
+ def self.included(other)
7
+ other.include(OpenapiEnabled)
8
+
9
+ other.before_action(:validate_request_parameters)
10
+ end
11
+
12
+ private
13
+
14
+ def validate_request_parameters
15
+ api_version = self.class.send(:api_version)[1..-1].sub(/x/, ".")
16
+
17
+ api_doc.try(
18
+ :validate_parameters!,
19
+ request.method,
20
+ request.path,
21
+ api_version,
22
+ params.slice(:sort_by)
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,70 @@
1
+ module Insights
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
+ end
14
+
15
+ def request_path
16
+ request.env["REQUEST_URI"]
17
+ end
18
+
19
+ def request_path_parts
20
+ @request_path_parts ||= begin
21
+ path, _query = request_path.split("?")
22
+ path.match(/\/(?<full_version_string>v\d+.\d+)\/(?<primary_collection_name>\w+)(\/(?<primary_collection_id>[^\/]+)(\/(?<subcollection_name>\w+))?)?/)&.named_captures || {}
23
+ end
24
+ end
25
+
26
+ def subcollection?
27
+ !!(request_path_parts["subcollection_name"] && request_path_parts["primary_collection_id"] && request_path_parts["primary_collection_name"])
28
+ end
29
+
30
+ private
31
+
32
+ def id_regexp
33
+ self.class.send(:id_regexp, request_path_parts["primary_collection_name"])
34
+ end
35
+
36
+ def validate_primary_collection_id
37
+ id = request_path_parts["primary_collection_id"]
38
+ return if id.blank?
39
+
40
+ raise RequestPathError, "ID is invalid" unless id.match(id_regexp)
41
+ end
42
+
43
+ module ClassMethods
44
+ private
45
+
46
+ def id_regexp(primary_collection_name)
47
+ @id_regexp ||= begin
48
+ id_parameter = id_parameter_from_api_doc(primary_collection_name)
49
+ id_parameter ? id_parameter.fetch_path("schema", "pattern") : /^\d+$/
50
+ end
51
+ end
52
+
53
+ def id_parameter_from_api_doc(primary_collection_name)
54
+ # Find the id parameter in the documented route
55
+ id_parameter = api_doc.paths.fetch_path("/#{primary_collection_name}/{id}", "get", "parameters", 0)
56
+ # The route isn't documented, return nil
57
+ return unless id_parameter
58
+
59
+ # Return the id parameter or resolve the reference to it and return that
60
+ reference = id_parameter["$ref"]
61
+ return id_parameter unless reference
62
+
63
+ api_doc.parameters[reference.split("parameters/").last]
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end