3scale_toolbox 0.14.0 → 0.15.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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/3scale_toolbox.gemspec +2 -1
  3. data/README.md +21 -3
  4. data/lib/3scale_toolbox.rb +5 -1
  5. data/lib/3scale_toolbox/attribute_filters.rb +2 -0
  6. data/lib/3scale_toolbox/attribute_filters/attribute_filter.rb +9 -0
  7. data/lib/3scale_toolbox/attribute_filters/service_id_from_ref_filter.rb +30 -0
  8. data/lib/3scale_toolbox/commands/activedocs_command/apply_command.rb +1 -1
  9. data/lib/3scale_toolbox/commands/activedocs_command/list_command.rb +18 -1
  10. data/lib/3scale_toolbox/commands/import_command/openapi.rb +26 -5
  11. data/lib/3scale_toolbox/commands/import_command/openapi/create_activedocs_step.rb +4 -17
  12. data/lib/3scale_toolbox/commands/import_command/openapi/create_service_step.rb +1 -5
  13. data/lib/3scale_toolbox/commands/import_command/openapi/mapping_rule.rb +3 -2
  14. data/lib/3scale_toolbox/commands/import_command/openapi/step.rb +43 -5
  15. data/lib/3scale_toolbox/commands/import_command/openapi/update_policies_step.rb +7 -11
  16. data/lib/3scale_toolbox/commands/import_command/openapi/update_service_oidc_conf_step.rb +2 -17
  17. data/lib/3scale_toolbox/commands/import_command/openapi/update_service_proxy_step.rb +10 -10
  18. data/lib/3scale_toolbox/openapi.rb +2 -0
  19. data/lib/3scale_toolbox/openapi/oas3.rb +232 -0
  20. data/lib/3scale_toolbox/openapi/swagger.rb +192 -0
  21. data/lib/3scale_toolbox/tasks/copy_service_proxy_task.rb +1 -0
  22. data/lib/3scale_toolbox/version.rb +1 -1
  23. data/licenses.xml +161 -1
  24. data/resources/oas3_meta_schema.json +1654 -0
  25. metadata +24 -6
  26. data/lib/3scale_toolbox/commands/import_command/openapi/threescale_api_spec.rb +0 -80
  27. data/lib/3scale_toolbox/swagger.rb +0 -1
  28. data/lib/3scale_toolbox/swagger/swagger.rb +0 -123
@@ -27,31 +27,16 @@ module ThreeScaleToolbox
27
27
 
28
28
  def add_flow_settings(settings)
29
29
  # only applies to oauth2 sec type
30
- return if security.nil? || security.type != 'oauth2'
30
+ return if api_spec.security.nil? || api_spec.security[:type] != 'oauth2'
31
31
 
32
32
  oidc_configuration = {
33
33
  standard_flow_enabled: false,
34
34
  implicit_flow_enabled: false,
35
35
  service_accounts_enabled: false,
36
36
  direct_access_grants_enabled: false
37
- }.merge(flow => true)
37
+ }.merge(api_spec.security[:flow] => true)
38
38
  settings.merge!(oidc_configuration)
39
39
  end
40
-
41
- def flow
42
- case (flow_f = security.flow)
43
- when 'implicit'
44
- :implicit_flow_enabled
45
- when 'password'
46
- :direct_access_grants_enabled
47
- when 'application'
48
- :service_accounts_enabled
49
- when 'accessCode'
50
- :standard_flow_enabled
51
- else
52
- raise ThreeScaleToolbox::Error, "Unexpected security flow field #{flow_f}"
53
- end
54
- end
55
40
  end
56
41
  end
57
42
  end
@@ -40,29 +40,29 @@ module ThreeScaleToolbox
40
40
  end
41
41
 
42
42
  def add_api_backend_settings(settings)
43
- return if private_base_url.nil?
44
-
45
- settings[:api_backend] = private_base_url
43
+ settings[:api_backend] = private_base_url unless private_base_url.nil?
44
+ settings[:secret_token] = backend_api_secret_token unless backend_api_secret_token.nil?
45
+ settings[:hostname_rewrite] = backend_api_host_header unless backend_api_host_header.nil?
46
46
  end
47
47
 
48
48
  def add_security_proxy_settings(settings)
49
49
  # nothing to add on proxy settings when no security required in openapi
50
- return if security.nil?
50
+ return if api_spec.security.nil?
51
51
 
52
- case security.type
52
+ case (type = api_spec.security[:type])
53
53
  when 'oauth2'
54
54
  settings[:credentials_location] = 'headers'
55
55
  settings[:oidc_issuer_endpoint] = oidc_issuer_endpoint unless oidc_issuer_endpoint.nil?
56
56
  when 'apiKey'
57
57
  settings[:credentials_location] = credentials_location
58
- settings[:auth_user_key] = security.name
58
+ settings[:auth_user_key] = api_spec.security[:name]
59
59
  else
60
- raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{security.type}"
60
+ raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{type}"
61
61
  end
62
62
  end
63
63
 
64
64
  def credentials_location
65
- case (in_f = security.in_f)
65
+ case (in_f = api_spec.security[:in_f])
66
66
  when 'query'
67
67
  'query'
68
68
  when 'header'
@@ -73,13 +73,13 @@ module ThreeScaleToolbox
73
73
  end
74
74
 
75
75
  def private_base_url
76
- override_private_base_url || private_base_url_from_openapi
76
+ override_private_base_url || private_base_url_from_openapi
77
77
  end
78
78
 
79
79
  def private_base_url_from_openapi
80
80
  return if api_spec.host.nil?
81
81
 
82
- "#{api_spec.schemes.first || 'https'}://#{api_spec.host}"
82
+ "#{api_spec.scheme || 'https'}://#{api_spec.host}"
83
83
  end
84
84
  end
85
85
  end
@@ -0,0 +1,2 @@
1
+ require '3scale_toolbox/openapi/swagger'
2
+ require '3scale_toolbox/openapi/oas3'
@@ -0,0 +1,232 @@
1
+ module ThreeScaleToolbox
2
+ module OpenAPI
3
+ ##
4
+ #
5
+ # OAS3 object
6
+ # * OAS3.title -> string
7
+ # * OAS3.description -> string
8
+ # * OAS3.version -> string
9
+ # * OAS3.base_path -> string
10
+ # * OAS3.host -> string
11
+ # * OAS3.scheme -> string
12
+ # * OAS3.operation -> array of operation hash
13
+ # * operation hash properties
14
+ # * :verb
15
+ # * :path
16
+ # * :description
17
+ # * :operation_id
18
+ # * OAS3.security -> security hash
19
+ # * security hash properties
20
+ # * :id -> string
21
+ # * :type -> string
22
+ # * :name -> string
23
+ # * :in_f -> string
24
+ # * :flow -> symbol (:implicit_flow_enabled, :direct_access_grants_enabled, :service_accounts_enabled, :standard_flow_enabled)
25
+ # * :scopes -> array of string
26
+ # * OAS3.service_backend_version -> string ('1','2','oidc')
27
+ # * OAS3.set_server_url -> def(spec, url)
28
+ # * OAS3.set_oauth2_urls-> def(spec, scheme_id, authorization_url, token_url)
29
+ class OAS3
30
+ META_SCHEMA_PATH = File.expand_path('../../../resources/oas3_meta_schema.json', __dir__)
31
+
32
+ def self.validate(raw)
33
+ meta_schema = JSON.parse(File.read(META_SCHEMA_PATH))
34
+ JSON::Validator.validate!(meta_schema, raw)
35
+ end
36
+
37
+ def self.build(path, raw, validate: true)
38
+ self.validate(raw) if validate
39
+
40
+ new(path, raw)
41
+ end
42
+
43
+ attr_reader :definition
44
+
45
+ def title
46
+ definition.info['title']
47
+ end
48
+
49
+ def description
50
+ definition.info['description']
51
+ end
52
+
53
+ def version
54
+ definition.info['version']
55
+ end
56
+
57
+ def base_path
58
+ # If there are many? take first
59
+ # From https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object
60
+ # If the servers property is not provided, or is an empty array,
61
+ # the default value would be a Server Object with a url value of /
62
+ server_objects(&:path).first || '/'
63
+ end
64
+
65
+ def host
66
+ # If there are many? take first
67
+ server_objects(&:host).first
68
+ end
69
+
70
+ def scheme
71
+ # If there are many? take first
72
+ server_objects(&:scheme).first
73
+ end
74
+
75
+ def operations
76
+ @operations ||= parse_operations
77
+ end
78
+
79
+ def security
80
+ @security ||= parse_security
81
+ end
82
+
83
+ def service_backend_version
84
+ # default authentication mode if no security requirement
85
+ return '1' if security.nil?
86
+
87
+ case security[:type]
88
+ when 'oauth2'
89
+ 'oidc'
90
+ when 'apiKey'
91
+ '1'
92
+ else
93
+ raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{security[:type]}"
94
+ end
95
+ end
96
+
97
+ ##
98
+ # Update given spec with urls
99
+ # It is expected identified security scheme to be oauth2 type
100
+ def set_oauth2_urls(spec, sec_scheme_id, authorization_url, token_url)
101
+ sec_scheme_obj = spec.dig('components', 'securitySchemes', sec_scheme_id)
102
+ if sec_scheme_obj.nil? || sec_scheme_obj['type'] != 'oauth2'
103
+ raise ThreeScaleToolbox::Error, "Expected security scheme {#{sec_scheme_id}} not found or not oauth2"
104
+ end
105
+
106
+ flow_key, flow_obj = sec_scheme_obj['flows'].first
107
+ flow_obj['authorizationUrl'] = authorization_url if %w[implicit authorizationCode].include?(flow_key)
108
+ flow_obj['tokenUrl'] = token_url if %w[password clientCredentials authorizationCode].include?(flow_key)
109
+ end
110
+
111
+ def set_server_url(spec, url)
112
+ spec['servers'] = [{ 'url' => url }]
113
+ end
114
+
115
+ private
116
+
117
+ def initialize(path, raw)
118
+ parser = OasParser::Parser.new(path, raw).resolve
119
+ @definition = OasParser::Definition.new(parser, path)
120
+ end
121
+
122
+ def server_objects
123
+ servers.map do |s|
124
+ yield Helper.parse_uri rendered_url(s)
125
+ end
126
+ end
127
+
128
+ # OAS3 server object variable substitution
129
+ def rendered_url(server_object)
130
+ template = erbfying_template(server_object.fetch('url'))
131
+ vars = server_object_variables(server_object['variables'])
132
+ ERB.new(template).result(OpenStruct.new(vars).instance_eval { binding })
133
+ end
134
+
135
+ def server_object_variables(variables)
136
+ vars = (variables || {}).each_with_object({}) do |(key, value), a|
137
+ a[key] = value['default']
138
+ end
139
+ JSON.parse(vars.to_json, symbolize_names: true)
140
+ end
141
+
142
+ def erbfying_template(template)
143
+ # A URL is composed from a limited set of characters belonging to the US-ASCII character set.
144
+ # These characters include digits (0-9), letters(A-Z, a-z), and a few special characters ("-", ".", "_", "~").
145
+ # https://www.urlencoder.io/learn/
146
+ tmp = template.gsub '{', '<%='
147
+ tmp.gsub '}', '%>'
148
+ end
149
+
150
+ def servers
151
+ definition.servers || []
152
+ end
153
+
154
+ def parse_operations
155
+ definition.paths.flat_map do |path_obj|
156
+ path_obj.endpoints.flat_map do |endpoint|
157
+ {
158
+ verb: endpoint.method,
159
+ path: endpoint.path.path,
160
+ description: endpoint.description,
161
+ operation_id: endpoint.operationId
162
+ }
163
+ end
164
+ end
165
+ end
166
+
167
+ def parse_security
168
+ raise ThreeScaleToolbox::Error, 'Invalid OAS: multiple security requirements' \
169
+ if global_security_requirements.size > 1
170
+
171
+ global_security_requirements.first
172
+ end
173
+
174
+ def global_security_requirements
175
+ @global_security_requirements ||= parse_global_security_reqs
176
+ end
177
+
178
+ def parse_global_security_reqs
179
+ security_requirements.flat_map do |sec_req|
180
+ sec_req.map do |sec_item_name, sec_item|
181
+ sec_def = fetch_security_scheme(sec_item_name)
182
+ {
183
+ id: sec_item_name,
184
+ type: sec_def['type'],
185
+ name: sec_def['name'],
186
+ in_f: sec_def['in'],
187
+ flow: parse_flows(sec_def['flows']),
188
+ scopes: sec_item
189
+ }
190
+ end
191
+ end
192
+ end
193
+
194
+ def fetch_security_scheme(name)
195
+ security_schemes.fetch(name) do |el|
196
+ raise ThreeScaleToolbox::Error, "OAS3 parsing error: #{el} not found in security schemes"
197
+ end
198
+ end
199
+
200
+ def security_requirements
201
+ definition.security || []
202
+ end
203
+
204
+ def security_schemes
205
+ (definition.components || {})['securitySchemes'] || {}
206
+ end
207
+
208
+ def parse_flows(flows_object)
209
+ return nil if flows_object.nil?
210
+
211
+ raise ThreeScaleToolbox::Error, 'Invalid OAS: multiple flows' if flows_object.size > 1
212
+
213
+ convert_flow(flows_object.keys.first)
214
+ end
215
+
216
+ def convert_flow(flow_name)
217
+ case flow_name
218
+ when 'implicit'
219
+ :implicit_flow_enabled
220
+ when 'password'
221
+ :direct_access_grants_enabled
222
+ when 'clientCredentials'
223
+ :service_accounts_enabled
224
+ when 'authorizationCode'
225
+ :standard_flow_enabled
226
+ else
227
+ raise ThreeScaleToolbox::Error, "Unexpected security flow field #{flow_name}"
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,192 @@
1
+ module ThreeScaleToolbox
2
+ module OpenAPI
3
+ ##
4
+ #
5
+ # Swagger object
6
+ # * Swagger.title -> string
7
+ # * Swagger.description -> string
8
+ # * Swagger.version -> string
9
+ # * Swagger.basePath -> string
10
+ # * Swagger.host -> string
11
+ # * Swagger.scheme -> string
12
+ # * Swagger.operation -> array of operation hash
13
+ # * operation hash properties
14
+ # * :verb
15
+ # * :path
16
+ # * :description
17
+ # * :operation_id
18
+ # * Swagger.security -> security hash
19
+ # * security hash properties
20
+ # * :id -> string
21
+ # * :type -> string
22
+ # * :name -> string
23
+ # * :in_f -> string
24
+ # * :flow -> symbol (:implicit_flow_enabled, :direct_access_grants_enabled, :service_accounts_enabled, :standard_flow_enabled)
25
+ # * :scopes -> array of string
26
+ # * Swagger.service_backend_version -> string ('1','2','oidc')
27
+ # * Swagger.set_server_url -> def(spec, url)
28
+ # * Swagger.set_oauth2_urls-> def(spec, scheme_id, authorization_url, token_url)
29
+ class Swagger
30
+ META_SCHEMA_PATH = File.expand_path('../../../resources/swagger_meta_schema.json', __dir__)
31
+
32
+ def self.validate(raw)
33
+ meta_schema = JSON.parse(File.read(META_SCHEMA_PATH))
34
+ JSON::Validator.validate!(meta_schema, raw)
35
+ end
36
+
37
+ def self.build(raw, validate: true)
38
+ self.validate(raw) if validate
39
+
40
+ new(raw)
41
+ end
42
+
43
+ attr_reader :raw
44
+
45
+ def title
46
+ raw.dig('info', 'title')
47
+ end
48
+
49
+ def description
50
+ raw.dig('info', 'description')
51
+ end
52
+
53
+ def version
54
+ raw.dig('info', 'version')
55
+ end
56
+
57
+ def base_path
58
+ raw['basePath']
59
+ end
60
+
61
+ def host
62
+ raw['host']
63
+ end
64
+
65
+ def scheme
66
+ Array(raw['schemes']).first
67
+ end
68
+
69
+ def operations
70
+ @operations ||= parse_operations
71
+ end
72
+
73
+ def security
74
+ @security ||= parse_security
75
+ end
76
+
77
+ def service_backend_version
78
+ # default authentication mode if no security requirement
79
+ return '1' if security.nil?
80
+
81
+ case security[:type]
82
+ when 'oauth2'
83
+ 'oidc'
84
+ when 'apiKey'
85
+ '1'
86
+ else
87
+ raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{security[:type]}"
88
+ end
89
+ end
90
+
91
+ def set_server_url(spec, url)
92
+ URI(url).tap do |uri|
93
+ spec['host'] = "#{uri.host}:#{uri.port}"
94
+ spec['schemes'] = [uri.scheme]
95
+ spec['basePath'] = uri.path
96
+ end
97
+ end
98
+
99
+ ##
100
+ # Update given spec with urls
101
+ # It is expected identified security scheme to be oauth2 type
102
+ def set_oauth2_urls(spec, sec_scheme_id, authorization_url, token_url)
103
+ sec_scheme_obj = spec.dig('securityDefinitions', sec_scheme_id)
104
+ if sec_scheme_obj.nil? || sec_scheme_obj['type'] != 'oauth2'
105
+ raise ThreeScaleToolbox::Error, "Expected security scheme {#{sec_scheme_id}} not found or not oauth2"
106
+ end
107
+
108
+ sec_scheme_obj['authorizationUrl'] = authorization_url if %w[implicit accessCode].include?(sec_scheme_obj['flow'])
109
+ sec_scheme_obj['tokenUrl'] = token_url if %w[password application accessCode].include?(sec_scheme_obj['flow'])
110
+ end
111
+
112
+ private
113
+
114
+ def initialize(raw)
115
+ @raw = raw
116
+ end
117
+
118
+ def parse_operations
119
+ raw['paths'].flat_map do |path, path_obj|
120
+ path_obj.flat_map do |method, operation|
121
+ next unless %w[get head post put patch delete trace options].include? method
122
+
123
+ {
124
+ verb: method,
125
+ path: path,
126
+ description: operation['description'],
127
+ operation_id: operation['operationId']
128
+ }
129
+ end.compact
130
+ end
131
+ end
132
+
133
+ def parse_security
134
+ raise ThreeScaleToolbox::Error, 'Invalid OAS: multiple security requirements' \
135
+ if global_security_requirements.size > 1
136
+
137
+ global_security_requirements.first
138
+ end
139
+
140
+ def global_security_requirements
141
+ @global_security_requirements ||= parse_global_security_reqs
142
+ end
143
+
144
+ def parse_global_security_reqs
145
+ security_requirements.flat_map do |sec_req|
146
+ sec_req.map do |sec_item_name, sec_item|
147
+ sec_def = fetch_security_definition(sec_item_name)
148
+ {
149
+ id: sec_item_name,
150
+ type: sec_def['type'],
151
+ name: sec_def['name'],
152
+ in_f: sec_def['in'],
153
+ flow: convert_flow(sec_def['flow']),
154
+ scopes: sec_item
155
+ }
156
+ end
157
+ end
158
+ end
159
+
160
+ def fetch_security_definition(name)
161
+ security_definitions.fetch(name) do |el|
162
+ raise ThreeScaleToolbox::Error, "Swagger parsing error: #{el} not found in security definitions"
163
+ end
164
+ end
165
+
166
+ def security_requirements
167
+ raw['security'] || []
168
+ end
169
+
170
+ def security_definitions
171
+ raw['securityDefinitions'] || {}
172
+ end
173
+
174
+ def convert_flow(flow_name)
175
+ return nil if flow_name.nil?
176
+
177
+ case flow_name
178
+ when 'implicit'
179
+ :implicit_flow_enabled
180
+ when 'password'
181
+ :direct_access_grants_enabled
182
+ when 'application'
183
+ :service_accounts_enabled
184
+ when 'accessCode'
185
+ :standard_flow_enabled
186
+ else
187
+ raise ThreeScaleToolbox::Error, "Unexpected security flow field #{flow_name}"
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end