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