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,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