insights-api-common 3.0.0

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