manageiq-api-common 0.1.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 +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
|