manageiq-api-common 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +62 -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/manageiq-api-common.rb +1 -0
  13. data/lib/manageiq/api/common.rb +13 -0
  14. data/lib/manageiq/api/common/api_error.rb +21 -0
  15. data/lib/manageiq/api/common/application_controller_mixins/api_doc.rb +39 -0
  16. data/lib/manageiq/api/common/application_controller_mixins/common.rb +27 -0
  17. data/lib/manageiq/api/common/application_controller_mixins/openapi_enabled.rb +13 -0
  18. data/lib/manageiq/api/common/application_controller_mixins/parameters.rb +113 -0
  19. data/lib/manageiq/api/common/application_controller_mixins/request_body_validation.rb +61 -0
  20. data/lib/manageiq/api/common/application_controller_mixins/request_path.rb +75 -0
  21. data/lib/manageiq/api/common/engine.rb +20 -0
  22. data/lib/manageiq/api/common/entitlement.rb +35 -0
  23. data/lib/manageiq/api/common/error_document.rb +29 -0
  24. data/lib/manageiq/api/common/filter.rb +160 -0
  25. data/lib/manageiq/api/common/graphql.rb +117 -0
  26. data/lib/manageiq/api/common/graphql/associated_records.rb +44 -0
  27. data/lib/manageiq/api/common/graphql/association_loader.rb +35 -0
  28. data/lib/manageiq/api/common/graphql/generator.rb +149 -0
  29. data/lib/manageiq/api/common/graphql/templates/model_type.erb +35 -0
  30. data/lib/manageiq/api/common/graphql/templates/query_type.erb +47 -0
  31. data/lib/manageiq/api/common/graphql/templates/schema.erb +6 -0
  32. data/lib/manageiq/api/common/graphql/types/big_int.rb +23 -0
  33. data/lib/manageiq/api/common/graphql/types/date_time.rb +16 -0
  34. data/lib/manageiq/api/common/graphql/types/query_filter.rb +16 -0
  35. data/lib/manageiq/api/common/inflections.rb +28 -0
  36. data/lib/manageiq/api/common/logging.rb +17 -0
  37. data/lib/manageiq/api/common/metrics.rb +39 -0
  38. data/lib/manageiq/api/common/middleware/web_server_metrics.rb +62 -0
  39. data/lib/manageiq/api/common/open_api.rb +2 -0
  40. data/lib/manageiq/api/common/open_api/docs.rb +54 -0
  41. data/lib/manageiq/api/common/open_api/docs/component_collection.rb +67 -0
  42. data/lib/manageiq/api/common/open_api/docs/doc_v3.rb +92 -0
  43. data/lib/manageiq/api/common/open_api/docs/object_definition.rb +27 -0
  44. data/lib/manageiq/api/common/open_api/generator.rb +441 -0
  45. data/lib/manageiq/api/common/open_api/serializer.rb +31 -0
  46. data/lib/manageiq/api/common/option_redirect_enhancements.rb +23 -0
  47. data/lib/manageiq/api/common/paginated_response.rb +92 -0
  48. data/lib/manageiq/api/common/request.rb +107 -0
  49. data/lib/manageiq/api/common/routing.rb +26 -0
  50. data/lib/manageiq/api/common/user.rb +48 -0
  51. data/lib/manageiq/api/common/version.rb +7 -0
  52. data/lib/tasks/manageiq/api/common_tasks.rake +4 -0
  53. data/spec/support/default_as_json.rb +17 -0
  54. data/spec/support/requests_spec_helper.rb +7 -0
  55. data/spec/support/user_header_spec_helper.rb +62 -0
  56. 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