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,20 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Insights::API::Common
6
+
7
+ config.autoload_paths << root.join("lib").to_s
8
+
9
+ initializer :load_inflections do
10
+ Insights::API::Common::Inflections.load_inflections
11
+ end
12
+
13
+ initializer :patch_option_redirect_routing do
14
+ require 'action_dispatch/routing/redirection'
15
+ ActionDispatch::Routing::OptionRedirect.prepend(Insights::API::Common::OptionRedirectEnhancements)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ class EntitlementError < StandardError; end
5
+
6
+ class Entitlement
7
+ def initialize(identity)
8
+ @identity = identity
9
+ end
10
+
11
+ %w[
12
+ ansible
13
+ hybrid_cloud
14
+ insights
15
+ migrations
16
+ openshift
17
+ smart_management
18
+ ].each do |m|
19
+ define_method("#{m}?") do
20
+ find_entitlement_key(m)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :identity
27
+
28
+ def find_entitlement_key(key)
29
+ result = identity.dig('entitlements', key.to_s)
30
+ # TODO: Always force entitlements key
31
+ return true unless result
32
+ result['is_entitled']
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,29 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ class ErrorDocument
5
+ def add(status = 400, message)
6
+ @status = status
7
+ errors << {"status" => status, "detail" => message}
8
+ self
9
+ end
10
+
11
+ def errors
12
+ @errors ||= []
13
+ end
14
+
15
+ def status
16
+ @status
17
+ end
18
+
19
+ def blank?
20
+ errors.blank?
21
+ end
22
+
23
+ def to_h
24
+ {"errors" => errors}
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,175 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ class Filter
5
+ INTEGER_COMPARISON_KEYWORDS = ["eq", "gt", "gte", "lt", "lte", "nil", "not_nil"].freeze
6
+ STRING_COMPARISON_KEYWORDS = ["contains", "contains_i", "eq", "eq_i", "starts_with", "starts_with_i", "ends_with", "ends_with_i", "nil", "not_nil"].freeze
7
+
8
+ attr_reader :apply, :arel_table, :api_doc_definition
9
+
10
+ def initialize(model, raw_filter, api_doc_definition)
11
+ self.query = model
12
+ @arel_table = model.arel_table
13
+ @raw_filter = raw_filter
14
+ @api_doc_definition = api_doc_definition
15
+ end
16
+
17
+ def apply
18
+ return query if @raw_filter.blank?
19
+ @raw_filter.each do |k, v|
20
+ next unless attribute = attribute_for_key(k)
21
+
22
+ if attribute["type"] == "string"
23
+ type = determine_string_attribute_type(attribute)
24
+ send(type, k, v)
25
+ else
26
+ errors << "unsupported attribute type for: #{k}"
27
+ end
28
+ end
29
+
30
+ raise(Insights::API::Common::Filter::Error, errors.join(", ")) unless errors.blank?
31
+ query
32
+ end
33
+
34
+ private
35
+
36
+ attr_accessor :query
37
+
38
+ class Error < ArgumentError; end
39
+
40
+ def attribute_for_key(key)
41
+ attribute = api_doc_definition.properties[key.to_s]
42
+ return attribute if attribute
43
+ errors << "found unpermitted parameter: #{key}"
44
+ nil
45
+ end
46
+
47
+ def determine_string_attribute_type(attribute)
48
+ return :timestamp if attribute["format"] == "date-time"
49
+ return :integer if attribute["pattern"] == /^\d+$/
50
+ :string
51
+ end
52
+
53
+ def errors
54
+ @errors ||= []
55
+ end
56
+
57
+ def string(k, val)
58
+ if val.kind_of?(ActionController::Parameters)
59
+ val.each do |comparator, value|
60
+ add_filter(comparator, STRING_COMPARISON_KEYWORDS, k, value)
61
+ end
62
+ else
63
+ add_filter("eq", STRING_COMPARISON_KEYWORDS, k, val)
64
+ end
65
+ end
66
+
67
+ def add_filter(requested_comparator, allowed_comparators, key, value)
68
+ return unless attribute = attribute_for_key(key)
69
+ type = determine_string_attribute_type(attribute)
70
+
71
+ if requested_comparator.in?(["nil", "not_nil"])
72
+ send("comparator_#{requested_comparator}", key, value)
73
+ elsif requested_comparator.in?(allowed_comparators)
74
+ value = parse_datetime(value) if type == :datetime
75
+ return if value.nil?
76
+ send("comparator_#{requested_comparator}", key, value)
77
+ else
78
+ errors << "unsupported #{type} comparator: #{requested_comparator}"
79
+ end
80
+ end
81
+
82
+ def timestamp(k, val)
83
+ if val.kind_of?(ActionController::Parameters)
84
+ val.each do |comparator, value|
85
+ add_filter(comparator, INTEGER_COMPARISON_KEYWORDS, k, value)
86
+ end
87
+ else
88
+ add_filter("eq", INTEGER_COMPARISON_KEYWORDS, k, val)
89
+ end
90
+ end
91
+
92
+ def parse_datetime(value)
93
+ return value.collect { |i| parse_datetime(i, ) } if value.kind_of?(Array)
94
+
95
+ DateTime.parse(value)
96
+ rescue ArgumentError
97
+ errors << "invalid timestamp: #{value}"
98
+ return nil
99
+ end
100
+
101
+ def integer(k, val)
102
+ if val.kind_of?(ActionController::Parameters)
103
+ val.each do |comparator, value|
104
+ add_filter(comparator, INTEGER_COMPARISON_KEYWORDS, k, value)
105
+ end
106
+ else
107
+ add_filter("eq", INTEGER_COMPARISON_KEYWORDS, k, val)
108
+ end
109
+ end
110
+
111
+ def comparator_contains(key, value)
112
+ return value.each { |v| comparator_contains(key, v) } if value.kind_of?(Array)
113
+
114
+ self.query = query.where(arel_table[key].matches("%#{query.sanitize_sql_like(value)}%", nil, true))
115
+ end
116
+
117
+ def comparator_contains_i(key, value)
118
+ return value.each { |v| comparator_contains_i(key, v) } if value.kind_of?(Array)
119
+
120
+ self.query = query.where(arel_table[key].lower.matches("%#{query.sanitize_sql_like(value.downcase)}%", nil, true))
121
+ end
122
+
123
+ def comparator_starts_with(key, value)
124
+ self.query = query.where(arel_table[key].matches("#{query.sanitize_sql_like(value)}%", nil, true))
125
+ end
126
+
127
+ def comparator_starts_with_i(key, value)
128
+ self.query = query.where(arel_table[key].lower.matches("#{query.sanitize_sql_like(value.downcase)}%", nil, true))
129
+ end
130
+
131
+ def comparator_ends_with(key, value)
132
+ self.query = query.where(arel_table[key].matches("%#{query.sanitize_sql_like(value)}", nil, true))
133
+ end
134
+
135
+ def comparator_ends_with_i(key, value)
136
+ self.query = query.where(arel_table[key].lower.matches("%#{query.sanitize_sql_like(value.downcase)}", nil, true))
137
+ end
138
+
139
+ def comparator_eq(key, value)
140
+ self.query = query.where(key => value)
141
+ end
142
+
143
+ def comparator_eq_i(key, value)
144
+ values = Array(value).map { |v| query.sanitize_sql_like(v.downcase) }
145
+
146
+ self.query = query.where(arel_table[key].lower.matches_any(values))
147
+ end
148
+
149
+ def comparator_gt(key, value)
150
+ self.query = query.where(arel_table[key].gt(value))
151
+ end
152
+
153
+ def comparator_gte(key, value)
154
+ self.query = query.where(arel_table[key].gteq(value))
155
+ end
156
+
157
+ def comparator_lt(key, value)
158
+ self.query = query.where(arel_table[key].lt(value))
159
+ end
160
+
161
+ def comparator_lte(key, value)
162
+ self.query = query.where(arel_table[key].lteq(value))
163
+ end
164
+
165
+ def comparator_nil(key, _value = nil)
166
+ self.query = query.where(key => nil)
167
+ end
168
+
169
+ def comparator_not_nil(key, _value = nil)
170
+ self.query = query.where.not(key => nil)
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,127 @@
1
+ require "graphql"
2
+ require "graphql/batch"
3
+ require "graphql/preload"
4
+
5
+ require "insights/api/common/graphql/association_loader"
6
+ require "insights/api/common/graphql/associated_records"
7
+ require "insights/api/common/graphql/generator"
8
+ require "insights/api/common/graphql/types/big_int"
9
+ require "insights/api/common/graphql/types/date_time"
10
+ require "insights/api/common/graphql/types/query_filter"
11
+ require "insights/api/common/graphql/types/query_sort_by"
12
+
13
+ module Insights
14
+ module API
15
+ module Common
16
+ module GraphQL
17
+ module Api
18
+ end
19
+
20
+ def self.version(request)
21
+ /\/?\w+\/v(?<major>\d+)[x\.]?(?<minor>\d+)?\// =~ request.original_url
22
+ [major, minor].compact.join(".")
23
+ end
24
+
25
+ def self.openapi_graphql_description
26
+ {
27
+ "summary" => "Perform a GraphQL Query",
28
+ "operationId" => "postGraphQL",
29
+ "description" => "Performs a GraphQL Query",
30
+ "requestBody" => {
31
+ "content" => {
32
+ "application/json" => {
33
+ "schema" => {
34
+ "$ref" => "#/components/schemas/GraphQLRequest"
35
+ }
36
+ }
37
+ },
38
+ "description" => "GraphQL Query Request",
39
+ "required" => true
40
+ },
41
+ "responses" => {
42
+ "200" => {
43
+ "description" => "GraphQL Query Response",
44
+ "content" => {
45
+ "application/json" => {
46
+ "schema" => {
47
+ "$ref" => "#/components/schemas/GraphQLResponse"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ end
55
+
56
+ def self.openapi_graphql_request
57
+ {
58
+ "type" => "object",
59
+ "properties" => {
60
+ "query" => {
61
+ "type" => "string",
62
+ "description" => "The GraphQL query",
63
+ "default" => "{}"
64
+ },
65
+ "operationName" => {
66
+ "type" => "string",
67
+ "description" => "If the Query contains several named operations, the operationName controls which one should be executed",
68
+ "default" => ""
69
+ },
70
+ "variables" => {
71
+ "type" => "object",
72
+ "description" => "Optional Query variables",
73
+ "nullable" => true
74
+ }
75
+ },
76
+ "required" => [
77
+ "query"
78
+ ]
79
+ }
80
+ end
81
+
82
+ def self.openapi_graphql_response
83
+ {
84
+ "type" => "object",
85
+ "properties" => {
86
+ "data" => {
87
+ "type" => "object",
88
+ "description" => "Results from the GraphQL query"
89
+ },
90
+ "errors" => {
91
+ "type" => "array",
92
+ "description" => "Errors resulting from the GraphQL query",
93
+ "items" => {
94
+ "type" => "object"
95
+ }
96
+ }
97
+ }
98
+ }
99
+ end
100
+
101
+ def self.search_options(scope, args)
102
+ args[:id] ? scope.where(:id => args[:id]) : scope
103
+ end
104
+
105
+ # Following code is auto-generated via rails generate graphql:install
106
+ #
107
+ # Handle form data, JSON body, or a blank value
108
+ def self.ensure_hash(ambiguous_param)
109
+ case ambiguous_param
110
+ when String
111
+ if ambiguous_param.present?
112
+ ensure_hash(JSON.parse(ambiguous_param))
113
+ else
114
+ {}
115
+ end
116
+ when Hash, ActionController::Parameters
117
+ ambiguous_param
118
+ when nil
119
+ {}
120
+ else
121
+ raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,44 @@
1
+ require "query_relation"
2
+
3
+ module Insights
4
+ module API
5
+ module Common
6
+ module GraphQL
7
+ class AssociatedRecords
8
+ include Enumerable
9
+ include QueryRelation::Queryable
10
+
11
+ def initialize(collection)
12
+ @collection = collection
13
+ end
14
+
15
+ private
16
+
17
+ def search(mode, options)
18
+ res = filter_collection_by_options(options)
19
+
20
+ case mode
21
+ when :first then res.first
22
+ when :last then res.last
23
+ when :all then res
24
+ end
25
+ end
26
+
27
+ def filter_collection_by_options(options)
28
+ res = @collection
29
+ if options[:where].present?
30
+ options[:where].each_pair { |k, v| res = res.select { |rec| rec[k].to_s == v.to_s } }
31
+ end
32
+ if options[:order].present?
33
+ order_by = options[:order].first
34
+ res = res.sort_by { |rec| rec[order_by] }
35
+ end
36
+ res = res.drop(options[:offset]) if options[:offset]
37
+ res = res.take(options[:limit]) if options[:limit]
38
+ res
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module Insights
2
+ module API
3
+ module Common
4
+ module GraphQL
5
+ class AssociationLoader < ::GraphQL::Batch::Loader
6
+ attr_reader :args, :association_name, :model
7
+
8
+ def initialize(model, association_name, args = {})
9
+ @model = model
10
+ @association_name = association_name
11
+ @args = args
12
+ end
13
+
14
+ def cache_key(record)
15
+ record.object_id
16
+ end
17
+
18
+ def perform(records)
19
+ records.each { |record| fulfill(record, read_association(record)) }
20
+ end
21
+
22
+ private
23
+
24
+ def read_association(record)
25
+ recs = GraphQL::AssociatedRecords.new(record.public_send(association_name))
26
+ recs = GraphQL.search_options(recs, args)
27
+ PaginatedResponse.new(
28
+ :base_query => recs, :request => nil, :limit => args[:limit], :offset => args[:offset]
29
+ ).records
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end