manageiq-api-common 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +62 -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/manageiq-api-common.rb +1 -0
- data/lib/manageiq/api/common.rb +13 -0
- data/lib/manageiq/api/common/api_error.rb +21 -0
- data/lib/manageiq/api/common/application_controller_mixins/api_doc.rb +39 -0
- data/lib/manageiq/api/common/application_controller_mixins/common.rb +27 -0
- data/lib/manageiq/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
- data/lib/manageiq/api/common/application_controller_mixins/parameters.rb +113 -0
- data/lib/manageiq/api/common/application_controller_mixins/request_body_validation.rb +61 -0
- data/lib/manageiq/api/common/application_controller_mixins/request_path.rb +75 -0
- data/lib/manageiq/api/common/engine.rb +20 -0
- data/lib/manageiq/api/common/entitlement.rb +35 -0
- data/lib/manageiq/api/common/error_document.rb +29 -0
- data/lib/manageiq/api/common/filter.rb +160 -0
- data/lib/manageiq/api/common/graphql.rb +117 -0
- data/lib/manageiq/api/common/graphql/associated_records.rb +44 -0
- data/lib/manageiq/api/common/graphql/association_loader.rb +35 -0
- data/lib/manageiq/api/common/graphql/generator.rb +149 -0
- data/lib/manageiq/api/common/graphql/templates/model_type.erb +35 -0
- data/lib/manageiq/api/common/graphql/templates/query_type.erb +47 -0
- data/lib/manageiq/api/common/graphql/templates/schema.erb +6 -0
- data/lib/manageiq/api/common/graphql/types/big_int.rb +23 -0
- data/lib/manageiq/api/common/graphql/types/date_time.rb +16 -0
- data/lib/manageiq/api/common/graphql/types/query_filter.rb +16 -0
- data/lib/manageiq/api/common/inflections.rb +28 -0
- data/lib/manageiq/api/common/logging.rb +17 -0
- data/lib/manageiq/api/common/metrics.rb +39 -0
- data/lib/manageiq/api/common/middleware/web_server_metrics.rb +62 -0
- data/lib/manageiq/api/common/open_api.rb +2 -0
- data/lib/manageiq/api/common/open_api/docs.rb +54 -0
- data/lib/manageiq/api/common/open_api/docs/component_collection.rb +67 -0
- data/lib/manageiq/api/common/open_api/docs/doc_v3.rb +92 -0
- data/lib/manageiq/api/common/open_api/docs/object_definition.rb +27 -0
- data/lib/manageiq/api/common/open_api/generator.rb +441 -0
- data/lib/manageiq/api/common/open_api/serializer.rb +31 -0
- data/lib/manageiq/api/common/option_redirect_enhancements.rb +23 -0
- data/lib/manageiq/api/common/paginated_response.rb +92 -0
- data/lib/manageiq/api/common/request.rb +107 -0
- data/lib/manageiq/api/common/routing.rb +26 -0
- data/lib/manageiq/api/common/user.rb +48 -0
- data/lib/manageiq/api/common/version.rb +7 -0
- data/lib/tasks/manageiq/api/common_tasks.rake +4 -0
- data/spec/support/default_as_json.rb +17 -0
- data/spec/support/requests_spec_helper.rb +7 -0
- data/spec/support/user_header_spec_helper.rb +62 -0
- metadata +375 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
module ManageIQ
|
2
|
+
module API
|
3
|
+
module Common
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace ManageIQ::API::Common
|
6
|
+
|
7
|
+
config.autoload_paths << root.join("lib").to_s
|
8
|
+
|
9
|
+
initializer :load_inflections do
|
10
|
+
ManageIQ::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(ManageIQ::API::Common::OptionRedirectEnhancements)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ManageIQ
|
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
|
+
hybrid_cloud
|
13
|
+
insights
|
14
|
+
openshift
|
15
|
+
smart_management
|
16
|
+
].each do |m|
|
17
|
+
define_method("#{m}?") do
|
18
|
+
find_entitlement_key(m)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :identity
|
25
|
+
|
26
|
+
def find_entitlement_key(key)
|
27
|
+
result = identity.dig('entitlements', key.to_s)
|
28
|
+
# TODO: Always force entitlements key
|
29
|
+
return true unless result
|
30
|
+
result['is_entitled']
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ManageIQ
|
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,160 @@
|
|
1
|
+
module ManageIQ
|
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", "eq", "starts_with", "ends_with", "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.add("unsupported attribute type for: #{k}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
raise ManageIQ::API::Common::Filter::Error.new(:error_document => errors) unless errors.blank?
|
31
|
+
query
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_accessor :query
|
37
|
+
|
38
|
+
class Error < ArgumentError
|
39
|
+
attr_reader :error_document
|
40
|
+
|
41
|
+
def initialize(attrs)
|
42
|
+
@error_document = attrs[:error_document]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def attribute_for_key(key)
|
47
|
+
attribute = api_doc_definition.properties[key.to_s]
|
48
|
+
return attribute if attribute
|
49
|
+
errors.add("found unpermitted parameter: #{key}")
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def determine_string_attribute_type(attribute)
|
54
|
+
return :timestamp if attribute["format"] == "date-time"
|
55
|
+
return :integer if attribute["pattern"] == /^\d+$/
|
56
|
+
:string
|
57
|
+
end
|
58
|
+
|
59
|
+
def errors
|
60
|
+
@errors ||= ManageIQ::API::Common::ErrorDocument.new
|
61
|
+
end
|
62
|
+
|
63
|
+
def string(k, val)
|
64
|
+
if val.kind_of?(ActionController::Parameters)
|
65
|
+
val.each do |comparator, value|
|
66
|
+
add_filter(comparator, STRING_COMPARISON_KEYWORDS, k, value)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
add_filter("eq", STRING_COMPARISON_KEYWORDS, k, val)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_filter(requested_comparator, allowed_comparators, key, value)
|
74
|
+
return unless attribute = attribute_for_key(key)
|
75
|
+
type = determine_string_attribute_type(attribute)
|
76
|
+
|
77
|
+
if requested_comparator.in?(["nil", "not_nil"])
|
78
|
+
send("comparator_#{requested_comparator}", key, value)
|
79
|
+
elsif requested_comparator.in?(allowed_comparators)
|
80
|
+
value = parse_datetime(value) if type == :datetime
|
81
|
+
return if value.nil?
|
82
|
+
send("comparator_#{requested_comparator}", key, value)
|
83
|
+
else
|
84
|
+
errors.add("unsupported #{type} comparator: #{requested_comparator}")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def timestamp(k, val)
|
89
|
+
if val.kind_of?(ActionController::Parameters)
|
90
|
+
val.each do |comparator, value|
|
91
|
+
add_filter(comparator, INTEGER_COMPARISON_KEYWORDS, k, value)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
add_filter("eq", INTEGER_COMPARISON_KEYWORDS, k, val)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse_datetime(value)
|
99
|
+
return value.collect { |i| parse_datetime(i, ) } if value.kind_of?(Array)
|
100
|
+
|
101
|
+
DateTime.parse(value)
|
102
|
+
rescue ArgumentError
|
103
|
+
errors.add("invalid timestamp: #{value}")
|
104
|
+
return nil
|
105
|
+
end
|
106
|
+
|
107
|
+
def integer(k, val)
|
108
|
+
if val.kind_of?(ActionController::Parameters)
|
109
|
+
val.each do |comparator, value|
|
110
|
+
add_filter(comparator, INTEGER_COMPARISON_KEYWORDS, k, value)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
add_filter("eq", INTEGER_COMPARISON_KEYWORDS, k, val)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def comparator_contains(key, value)
|
118
|
+
return value.each { |v| comparator_contains(key, v) } if value.kind_of?(Array)
|
119
|
+
self.query = query.where(arel_table[key].matches("%#{query.sanitize_sql_like(value)}%", nil, true))
|
120
|
+
end
|
121
|
+
|
122
|
+
def comparator_starts_with(key, value)
|
123
|
+
self.query = query.where(arel_table[key].matches("#{query.sanitize_sql_like(value)}%", nil, true))
|
124
|
+
end
|
125
|
+
|
126
|
+
def comparator_ends_with(key, value)
|
127
|
+
self.query = query.where(arel_table[key].matches("%#{query.sanitize_sql_like(value)}", nil, true))
|
128
|
+
end
|
129
|
+
|
130
|
+
def comparator_eq(key, value)
|
131
|
+
self.query = query.where(key => value)
|
132
|
+
end
|
133
|
+
|
134
|
+
def comparator_gt(key, value)
|
135
|
+
self.query = query.where(arel_table[key].gt(value))
|
136
|
+
end
|
137
|
+
|
138
|
+
def comparator_gte(key, value)
|
139
|
+
self.query = query.where(arel_table[key].gteq(value))
|
140
|
+
end
|
141
|
+
|
142
|
+
def comparator_lt(key, value)
|
143
|
+
self.query = query.where(arel_table[key].lt(value))
|
144
|
+
end
|
145
|
+
|
146
|
+
def comparator_lte(key, value)
|
147
|
+
self.query = query.where(arel_table[key].lteq(value))
|
148
|
+
end
|
149
|
+
|
150
|
+
def comparator_nil(key, _value = nil)
|
151
|
+
self.query = query.where(key => nil)
|
152
|
+
end
|
153
|
+
|
154
|
+
def comparator_not_nil(key, _value = nil)
|
155
|
+
self.query = query.where.not(key => nil)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require "graphql"
|
2
|
+
require "graphql/batch"
|
3
|
+
require "graphql/preload"
|
4
|
+
|
5
|
+
require "manageiq/api/common/graphql/association_loader"
|
6
|
+
require "manageiq/api/common/graphql/associated_records"
|
7
|
+
require "manageiq/api/common/graphql/generator"
|
8
|
+
require "manageiq/api/common/graphql/types/big_int"
|
9
|
+
require "manageiq/api/common/graphql/types/date_time"
|
10
|
+
require "manageiq/api/common/graphql/types/query_filter"
|
11
|
+
|
12
|
+
module ManageIQ
|
13
|
+
module API
|
14
|
+
module Common
|
15
|
+
module GraphQL
|
16
|
+
def self.version(request)
|
17
|
+
/\/?\w+\/v(?<major>\d+)[x\.]?(?<minor>\d+)?\// =~ request.original_url
|
18
|
+
[major, minor].compact.join(".")
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.openapi_graphql_description
|
22
|
+
{
|
23
|
+
"summary" => "Perform a GraphQL Query",
|
24
|
+
"operationId" => "postGraphQL",
|
25
|
+
"description" => "Performs a GraphQL Query",
|
26
|
+
"requestBody" => {
|
27
|
+
"content" => {
|
28
|
+
"application/json" => {
|
29
|
+
"schema" => {
|
30
|
+
"type" => "object",
|
31
|
+
"properties" => {
|
32
|
+
"query" => {
|
33
|
+
"type" => "string",
|
34
|
+
"description" => "The GraphQL query",
|
35
|
+
"default" => "{}"
|
36
|
+
},
|
37
|
+
"operationName" => {
|
38
|
+
"type" => "string",
|
39
|
+
"description" => "If the Query contains several named operations, the operationName controls which one should be executed",
|
40
|
+
"default" => ""
|
41
|
+
},
|
42
|
+
"variables" => {
|
43
|
+
"type" => "object",
|
44
|
+
"description" => "Optional Query variables",
|
45
|
+
"nullable" => true
|
46
|
+
}
|
47
|
+
},
|
48
|
+
"required" => [
|
49
|
+
"query"
|
50
|
+
]
|
51
|
+
}
|
52
|
+
}
|
53
|
+
},
|
54
|
+
"description" => "GraphQL Query Request",
|
55
|
+
"required" => true
|
56
|
+
},
|
57
|
+
"responses" => {
|
58
|
+
"200" => {
|
59
|
+
"description" => "GraphQL Query Response",
|
60
|
+
"content" => {
|
61
|
+
"application/json" => {
|
62
|
+
"schema" => {
|
63
|
+
"$ref" => "#/components/schemas/GraphQLResponse"
|
64
|
+
}
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.openapi_graphql_response
|
73
|
+
{
|
74
|
+
"type" => "object",
|
75
|
+
"properties" => {
|
76
|
+
"data" => {
|
77
|
+
"type" => "object",
|
78
|
+
"description" => "Results from the GraphQL query"
|
79
|
+
},
|
80
|
+
"errors" => {
|
81
|
+
"type" => "array",
|
82
|
+
"description" => "Errors resulting from the GraphQL query",
|
83
|
+
"items" => {
|
84
|
+
"type" => "object"
|
85
|
+
}
|
86
|
+
}
|
87
|
+
}
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.search_options(scope, args)
|
92
|
+
args[:id] ? scope.where(:id => args[:id]) : scope
|
93
|
+
end
|
94
|
+
|
95
|
+
# Following code is auto-generated via rails generate graphql:install
|
96
|
+
#
|
97
|
+
# Handle form data, JSON body, or a blank value
|
98
|
+
def self.ensure_hash(ambiguous_param)
|
99
|
+
case ambiguous_param
|
100
|
+
when String
|
101
|
+
if ambiguous_param.present?
|
102
|
+
ensure_hash(JSON.parse(ambiguous_param))
|
103
|
+
else
|
104
|
+
{}
|
105
|
+
end
|
106
|
+
when Hash, ActionController::Parameters
|
107
|
+
ambiguous_param
|
108
|
+
when nil
|
109
|
+
{}
|
110
|
+
else
|
111
|
+
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require "query_relation"
|
2
|
+
|
3
|
+
module ManageIQ
|
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 ManageIQ
|
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
|