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.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +102 -0
- data/Rakefile +18 -0
- data/app/models/authentication.rb +19 -0
- data/app/models/concerns/encryption_concern.rb +52 -0
- data/app/models/encryption.rb +13 -0
- data/lib/generators/shared_utilities/migration_generator.rb +79 -0
- data/lib/generators/shared_utilities/orm_helper.rb +25 -0
- data/lib/generators/shared_utilities/templates/migration.rb +27 -0
- data/lib/generators/shared_utilities/templates/migration_existing.rb +28 -0
- data/lib/insights.rb +1 -0
- data/lib/insights/api/common.rb +12 -0
- data/lib/insights/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/insights/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/insights/api/common/application_controller_mixins/exception_handling.rb +41 -0
- data/lib/insights/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/insights/api/common/application_controller_mixins/parameters.rb +134 -0
- data/lib/insights/api/common/application_controller_mixins/request_body_validation.rb +48 -0
- data/lib/insights/api/common/application_controller_mixins/request_parameter_validation.rb +29 -0
- data/lib/insights/api/common/application_controller_mixins/request_path.rb +70 -0
- data/lib/insights/api/common/engine.rb +20 -0
- data/lib/insights/api/common/entitlement.rb +37 -0
- data/lib/insights/api/common/error_document.rb +29 -0
- data/lib/insights/api/common/filter.rb +175 -0
- data/lib/insights/api/common/graphql.rb +127 -0
- data/lib/insights/api/common/graphql/associated_records.rb +44 -0
- data/lib/insights/api/common/graphql/association_loader.rb +35 -0
- data/lib/insights/api/common/graphql/generator.rb +148 -0
- data/lib/insights/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/insights/api/common/graphql/templates/query_type.erb +49 -0
- data/lib/insights/api/common/graphql/templates/schema.erb +6 -0
- data/lib/insights/api/common/graphql/types/big_int.rb +23 -0
- data/lib/insights/api/common/graphql/types/date_time.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/insights/api/common/graphql/types/query_sort_by.rb +16 -0
- data/lib/insights/api/common/inflections.rb +28 -0
- data/lib/insights/api/common/logging.rb +17 -0
- data/lib/insights/api/common/metrics.rb +39 -0
- data/lib/insights/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/insights/api/common/open_api.rb +2 -0
- data/lib/insights/api/common/open_api/docs.rb +54 -0
- data/lib/insights/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/insights/api/common/open_api/docs/doc_v3.rb +102 -0
- data/lib/insights/api/common/open_api/docs/object_definition.rb +39 -0
- data/lib/insights/api/common/open_api/generator.rb +520 -0
- data/lib/insights/api/common/open_api/serializer.rb +31 -0
- data/lib/insights/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/insights/api/common/paginated_response.rb +108 -0
- data/lib/insights/api/common/rbac/access.rb +66 -0
- data/lib/insights/api/common/rbac/acl.rb +74 -0
- data/lib/insights/api/common/rbac/policies.rb +33 -0
- data/lib/insights/api/common/rbac/query_shared_resource.rb +45 -0
- data/lib/insights/api/common/rbac/roles.rb +77 -0
- data/lib/insights/api/common/rbac/seed.rb +140 -0
- data/lib/insights/api/common/rbac/service.rb +67 -0
- data/lib/insights/api/common/rbac/share_resource.rb +60 -0
- data/lib/insights/api/common/rbac/unshare_resource.rb +32 -0
- data/lib/insights/api/common/rbac/utilities.rb +30 -0
- data/lib/insights/api/common/request.rb +111 -0
- data/lib/insights/api/common/routing.rb +26 -0
- data/lib/insights/api/common/user.rb +48 -0
- data/lib/insights/api/common/version.rb +7 -0
- data/lib/tasks/insights/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/rbac_shared_contexts.rb +44 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/service_spec_helper.rb +26 -0
- data/spec/support/user_header_spec_helper.rb +68 -0
- 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
|