marty 2.3.15 → 2.4.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/app/components/marty/api_auth_view.rb +27 -4
  4. data/app/components/marty/api_config_view.rb +27 -4
  5. data/app/components/marty/extras/layout.rb +4 -7
  6. data/app/components/marty/form.rb +8 -0
  7. data/app/components/marty/grid.rb +45 -19
  8. data/app/components/marty/main_auth_app.rb +12 -11
  9. data/app/controllers/marty/rpc_controller.rb +63 -150
  10. data/app/models/marty/api_auth.rb +86 -14
  11. data/app/models/marty/api_config.rb +11 -6
  12. data/app/models/marty/delorean_rule.rb +3 -2
  13. data/config/routes.rb +1 -1
  14. data/db/migrate/501_add_api_class_to_marty_api_config.rb +6 -0
  15. data/db/migrate/502_add_parameters_to_marty_api_auth.rb +5 -0
  16. data/lib/marty/util.rb +0 -15
  17. data/lib/marty/version.rb +1 -1
  18. data/other/marty/api/base.rb +207 -0
  19. data/other/marty/diagnostic/aws/ec2_instance.rb +12 -87
  20. data/other/marty/diagnostic/environment_variables.rb +1 -1
  21. data/spec/controllers/job_controller_spec.rb +1 -1
  22. data/spec/dummy/app/components/gemini/xyz_rule_view.rb +1 -0
  23. data/spec/dummy/config/application.rb +1 -0
  24. data/spec/features/enum_spec.rb +35 -100
  25. data/spec/features/log_view_spec.rb +5 -5
  26. data/spec/features/rule_spec.rb +30 -30
  27. data/spec/features/user_view_spec.rb +0 -2
  28. data/spec/lib/logger_spec.rb +1 -1
  29. data/spec/models/api_auth_spec.rb +33 -12
  30. data/spec/models/event_spec.rb +1 -1
  31. data/spec/models/promise_spec.rb +1 -1
  32. data/spec/models/user_spec.rb +6 -6
  33. data/spec/spec_helper.rb +69 -9
  34. data/spec/support/{shared_connection_db_helpers.rb → clean_db_helpers.rb} +2 -2
  35. data/spec/support/delayed_job_helpers.rb +1 -1
  36. data/spec/support/{users.rb → integration_helpers.rb} +9 -11
  37. data/spec/support/{setup.rb → spec_setup.rb} +6 -19
  38. data/spec/support/user_helpers.rb +12 -0
  39. metadata +10 -21
  40. data/spec/dummy/app/assets/client/application.css +0 -13
  41. data/spec/dummy/app/assets/client/application.js +0 -15
  42. data/spec/support/chromedriver.rb +0 -41
  43. data/spec/support/components/netzke_combobox.rb +0 -57
  44. data/spec/support/components/netzke_grid.rb +0 -356
  45. data/spec/support/custom_matchers.rb +0 -18
  46. data/spec/support/custom_selectors.rb +0 -49
  47. data/spec/support/download_helper.rb +0 -52
  48. data/spec/support/helper.rb +0 -20
  49. data/spec/support/netzke.rb +0 -306
  50. data/spec/support/performance_helper.rb +0 -26
  51. data/spec/support/post_run_logger.rb +0 -32
  52. data/spec/support/shared_connection.rb +0 -31
  53. data/spec/support/structure_compare.rb +0 -62
  54. data/spec/support/suite.rb +0 -27
@@ -1,11 +1,11 @@
1
1
  class Marty::ApiAuth < Marty::Base
2
2
  has_mcfly
3
3
 
4
+ belongs_to :entity, polymorphic: true, optional: true
5
+
4
6
  KEY_SIZE = 19
5
7
 
6
- def self.generate_key
7
- SecureRandom.hex(KEY_SIZE)
8
- end
8
+ validates_presence_of :app_name, :api_key, :script_name
9
9
 
10
10
  class ApiAuthValidator < ActiveModel::Validator
11
11
  def validate(api)
@@ -17,24 +17,96 @@ class Marty::ApiAuth < Marty::Base
17
17
  end
18
18
  end
19
19
 
20
+ validates_with ApiAuthValidator
21
+
22
+ mcfly_validates_uniqueness_of :api_key, scope: [:script_name]
23
+ validates_uniqueness_of :app_name, scope: [:script_name,
24
+ :obsoleted_dt]
25
+
26
+
20
27
  before_validation do
21
28
  self.api_key = Marty::ApiAuth.generate_key if
22
29
  self.api_key.nil? || self.api_key.length == 0
23
30
  end
24
31
 
25
- validates_presence_of :app_name, :api_key, :script_name
32
+ before_save do
33
+ return unless changed.include?(:entity_id) && !parameters['aws_api_key']
26
34
 
27
- validates_with ApiAuthValidator
35
+ msg = 'API Auth must be associated with an AWS API KEY before '\
36
+ 'it can be associated with an entity'
28
37
 
29
- mcfly_validates_uniqueness_of :api_key, scope: [:script_name]
30
- validates_uniqueness_of :app_name, scope: [:script_name,
31
- :obsoleted_dt]
38
+ errors.add(:base, msg)
39
+ end
40
+
41
+ before_destroy do
42
+ next unless aws = parameters['aws_api_key']
43
+ begin
44
+ client = Marty::Aws::Apigateway.new
45
+ resp = client.delete_usage_plan_key(aws['api_usage_plan_id'],
46
+ aws['aid'])
47
+ client.delete_api_key(aws['aid']) if resp
48
+ rescue => e
49
+ Marty::Logger.log('api_test', 'error', e.message)
50
+ throw :abort unless e.message.include?('Invalid API Key')
51
+ end
52
+ end
53
+
54
+ def self.generate_key
55
+ SecureRandom.hex(KEY_SIZE)
56
+ end
57
+
58
+ def create_aws_api_key api_id, api_usage_plan_id
59
+ client = Marty::Aws::Apigateway.new
60
+ app_id = Marty::Config['AWS_APP_IDENTIFIER'] || 'marty'
61
+ name = "#{app_id}-#{api_id}-#{api_key[0..3]}"
62
+
63
+ key = nil
64
+ begin
65
+ key = client.create_api_key(name, 'marty_api_key', api_key)
66
+ rescue => e
67
+ #Marty::Logger.log('api_test', 'error', e.message)
68
+ end
69
+
70
+ upkey = nil
71
+ begin
72
+ upkey = key &&
73
+ client.create_usage_plan_key(api_usage_plan_id, key.id)
74
+ rescue => e
75
+ #Marty::Logger.log('api_test', 'error', e.message)
76
+ # remove api key we created
77
+ client.delete_api_key(key.id)
78
+ end
79
+
80
+ raise "Unable to create AWS API Key" unless key && upkey
81
+
82
+ parameters['aws_api_key'] = {
83
+ 'aid' => key.id,
84
+ 'api_usage_plan_id' => api_usage_plan_id,
85
+ 'api_id' => api_id,
86
+ }
87
+
88
+ save!
89
+ end
90
+
91
+ def move_aws_key usage_plan_id
92
+ return unless aws = parameters['aws_api_key']
93
+ return if aws['api_usage_plan_id'] == usage_plan_id
94
+
95
+ begin
96
+ client = Marty::Aws::Apigateway.new
97
+ resp = client.delete_usage_plan_key(aws['api_usage_plan_id'],
98
+ aws['aid'])
99
+ rescue => e
100
+ # on fail recreate usage plan key
101
+ Marty::Logger.log('api', 'api_test', aws)
102
+ client.create_usage_plan_key(aws['api_usage_plan_id'], aws['aid']) if
103
+ client
104
+ return
105
+ else
106
+ client.create_usage_plan_key(usage_plan_id, aws['aid']) if resp
107
+ end
32
108
 
33
- def self.authorized?(script_name, api_key)
34
- is_secured = where(script_name: script_name,
35
- obsoleted_dt: 'infinity').exists?
36
- !is_secured || where(api_key: api_key,
37
- script_name: script_name,
38
- obsoleted_dt: 'infinity').pluck(:app_name).first
109
+ parameters['aws_api_key'] += {'api_usage_plan_id' => usage_plan_id}
110
+ save!
39
111
  end
40
112
  end
@@ -3,13 +3,18 @@ class Marty::ApiConfig < Marty::Base
3
3
 
4
4
  def self.lookup(script, node, attr)
5
5
  res = where(["script = ? AND (node IS NULL OR node = ?) "\
6
- "AND (attr IS NULL OR attr = ?)",
7
- script, node, attr]).
8
- order('node nulls last, attr nulls last').
9
- pluck(:logged, :input_validated, :output_validated, :strict_validate,
10
- :id)
11
- res.first
6
+ "AND (attr IS NULL OR attr = ?)",
7
+ script, node, attr]).
8
+ order('node nulls last, attr nulls last').first
9
+
10
+ res && res.as_json.except('id',
11
+ 'created_at',
12
+ 'updated_at',
13
+ 'script',
14
+ 'node',
15
+ 'attr').symbolize_keys
12
16
  end
17
+
13
18
  def self.multi_lookup(script, node, attrs)
14
19
  (attrs.nil? ? [nil] : attrs).
15
20
  map { |attr| lookup(script, node, attr).try{|x| x.unshift(attr) }}
@@ -152,9 +152,10 @@ class Marty::DeloreanRule < Marty::BaseRule
152
152
  estack_full = resh.delete(:err_stack)
153
153
  estack = estack_full && {
154
154
  err_stack: estack_full.select{ |l| l.starts_with?('DELOREAN')}} || {}
155
- detail = { input: params, dgparams: dgparams} + resh + estack
156
155
  Marty::Logger.info("Rule Log #{ruleh['name']}",
157
- Marty::Util.scrub_obj(detail))
156
+ { input: params,
157
+ dgparams: dgparams } + resh + estack
158
+ )
158
159
  end
159
160
  end
160
161
  end
@@ -6,5 +6,5 @@ Marty::Engine.routes.draw do
6
6
  match via: [:get, :post], "rpc/evaluate(.:format)" => "rpc", as: :rpc
7
7
  match via: [:get, :post], "report(.:format)" => "report#index", as: :report
8
8
  get 'job/download' => 'job', as: :job
9
- get 'diag', to: 'diagnostic/#op'
9
+ get 'diag', to: 'diagnostic/#op'
10
10
  end
@@ -0,0 +1,6 @@
1
+ class AddApiClassToMartyApiConfig < ActiveRecord::Migration[5.1]
2
+ def change
3
+ table = :marty_api_configs
4
+ add_column table, :api_class, :string, default: 'Marty::Api::Base'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ class AddParametersToMartyApiAuth < ActiveRecord::Migration[5.1]
2
+ def change
3
+ add_column :marty_api_auths, :parameters, :jsonb, default: {}
4
+ end
5
+ end
@@ -132,19 +132,4 @@ module Marty::Util
132
132
  URI.encode("#{Marty::Util.marty_path}/report?data=#{data}"\
133
133
  "&reptitle=#{title}&format=#{format}")
134
134
  end
135
-
136
- def self.scrub_obj(obj)
137
- trav = lambda {|o|
138
- if o.is_a?(Hash)
139
- return o.each_with_object({}) {|(k, v), h| h[k] = trav.call(v)}
140
- elsif o.is_a?(Array)
141
- return o.map {|v| trav.call(v)}
142
- elsif o.to_s.length > 10000
143
- o.class.to_s
144
- else
145
- o
146
- end
147
- }
148
- trav.call(obj)
149
- end
150
135
  end
@@ -1,3 +1,3 @@
1
1
  module Marty
2
- VERSION = "2.3.15"
2
+ VERSION = "2.4.0"
3
3
  end
@@ -0,0 +1,207 @@
1
+ class Marty::Api::Base
2
+ mattr_accessor :class_list
3
+ @@class_list ||= [name].to_set
4
+
5
+ def self.inherited(klass)
6
+ @@class_list << klass.to_s
7
+ super
8
+ end
9
+
10
+ def self.respond_to controller
11
+ result = yield
12
+ controller.respond_to do |format|
13
+ format.json { controller.send_data result.to_json }
14
+ format.csv {
15
+ # SEMI-HACKY: strip outer list if there's only one element.
16
+ result = result[0] if result.is_a?(Array) && result.length==1
17
+ controller.send_data Marty::DataExporter.to_csv(result)
18
+ }
19
+ end
20
+ end
21
+
22
+ # api handles
23
+ def self.process_params params
24
+ params
25
+ end
26
+
27
+ def self.before_evaluate api_params
28
+ end
29
+
30
+ def self.after_evaluate api_params, result
31
+ end
32
+
33
+ def self.is_authorized? params
34
+ is_secured = Marty::ApiAuth.where(
35
+ script_name: params[:script],
36
+ obsoleted_dt: 'infinity'
37
+ ).exists?
38
+
39
+ !is_secured || Marty::ApiAuth.where(
40
+ api_key: params[:api_key],
41
+ script_name: params[:script],
42
+ obsoleted_dt: 'infinity'
43
+ ).pluck(:app_name).first
44
+ end
45
+
46
+ def self.evaluate params, request, config
47
+ # validate input schema
48
+ if config[:input_validated]
49
+ begin
50
+ schema = SchemaValidator::get_schema(params)
51
+ rescue => e
52
+ return {error: e.message}
53
+ end
54
+
55
+ begin
56
+ res = SchemaValidator::validate_schema(schema, params[:params])
57
+ rescue NameError
58
+ return {error: "Unrecognized PgEnum for attribute #{params[:attr]}"}
59
+ rescue => e
60
+ return {error: "#{params[:attr]}: #{e.message}"}
61
+ end
62
+
63
+ schema_errors = SchemaValidator::get_errors(res) unless res.empty?
64
+ return {error: "Error(s) validating: #{schema_errors}"} if
65
+ schema_errors
66
+ end
67
+
68
+ # get script engine
69
+ begin
70
+ engine = Marty::ScriptSet.new(params[:tag]).get_engine(params[:script])
71
+ rescue => e
72
+ error = "Can't get engine: #{params[:script] || 'nil'} with tag: " +
73
+ "#{params[:tag] || 'nil'}; message: #{e.message}"
74
+ Marty::Logger.info error
75
+ return {error: error}
76
+ end
77
+
78
+ retval = nil
79
+
80
+ # evaluate script
81
+ begin
82
+ if params[:background]
83
+ res = engine.background_eval(params[:node],
84
+ params[:params],
85
+ params[:attr])
86
+
87
+ return retval = {"job_id" => res.__promise__.id}
88
+ end
89
+
90
+ res = engine.evaluate(params[:node],
91
+ params[:attr],
92
+ params[:params])
93
+
94
+ # validate output schema
95
+ if config[:output_validated] && !(res.is_a?(Hash) && res['error'])
96
+ begin
97
+ output_schema_params = params + {attr: params[:attr] + '_'}
98
+ schema = SchemaValidator::get_schema(output_schema_params)
99
+ rescue => e
100
+ return {error: e.message}
101
+ end
102
+
103
+ begin
104
+ schema_errors = SchemaValidator::validate_schema(schema, res)
105
+ rescue NameError
106
+ return {error: "Unrecognized PgEnum for attribute #{attr}"}
107
+ rescue => e
108
+ return {error: "#{attr}: #{e.message}"}
109
+ end
110
+
111
+ if schema_errors.present?
112
+ errors = schema_errors.map{|e| e[:message]}
113
+
114
+ Marty::Logger.error(
115
+ "API #{params[:script]}:#{params[:node]}.#{params[:attr]}",
116
+ {error: errors, data: res}
117
+ )
118
+
119
+ msg = "Error(s) validating: #{errors}"
120
+ res = config[:strict_validate] ? {error: msg ,data: res} : res
121
+ end
122
+ end
123
+
124
+ # if attr is an array, return result as an array
125
+ return retval = params[:return_array] ? [res] : res
126
+
127
+ rescue => e
128
+ msg = Delorean::Engine.grok_runtime_exception(e).symbolize_keys
129
+ Marty::Logger.info "Evaluation error: #{msg}"
130
+ return retval = msg
131
+ ensure
132
+ error = Hash === retval ? retval[:error] : nil
133
+ end
134
+ end
135
+
136
+ def self.log result, params, request
137
+ ret_arr = params[:return_array]
138
+ Marty::Log.write_log('api',
139
+ params.values_at(:script, :node, :attr).join(' - '),
140
+ {script: params[:script],
141
+ node: params[:node],
142
+ attrs: ret_arr ? [params[:attr]] : params[:attr],
143
+ input: params[:params],
144
+ output: (result.is_a?(Hash) &&
145
+ result.include?('error')) ? nil : result,
146
+ start_time: params[:start_time],
147
+ end_time: Time.zone.now,
148
+ error: (result.is_a?(Hash) &&
149
+ result.include?('error')) ? result : nil,
150
+ remote_ip: request.remote_ip,
151
+ auth_name: params[:auth]
152
+ })
153
+ end
154
+
155
+ class SchemaValidator
156
+ def self.get_schema params
157
+ begin
158
+ Marty::ScriptSet.new(params[:tag]).get_engine(params[:script]+'Schemas').
159
+ evaluate(params[:node], params[:attr], {})
160
+ rescue => e
161
+ msg = e.message == 'No such script' ? 'Schema not defined' :
162
+ 'Problem with schema: ' + e.message
163
+
164
+ raise "Schema error for #{params[:script]}/#{params[:node]} "\
165
+ "attrs=#{params[:attr]}: #{msg}"
166
+ end
167
+ end
168
+
169
+ def self.validate_schema schema, hash
170
+ JSON::Validator.fully_validate(
171
+ schema.merge({"\$schema" => Marty::JsonSchema::RAW_URI}),
172
+ hash,
173
+ validate_schema: true,
174
+ errors_as_objects: true,
175
+ version: Marty::JsonSchema::RAW_URI,
176
+ )
177
+ end
178
+
179
+ def self.massage_message(msg)
180
+ m = %r|'#/([^']+)' of type ([^ ]+) matched the disallowed schema|.
181
+ match(msg)
182
+
183
+ return msg unless m
184
+ "disallowed parameter '#{m[1]}' of type #{m[2]} was received"
185
+ end
186
+
187
+ def self._get_errors(errs)
188
+ if errs.is_a?(Array)
189
+ errs.map { |err| _get_errors(err) }
190
+ elsif errs.is_a?(Hash)
191
+ if !errs.include?(:failed_attribute)
192
+ errs.map { |k, v| _get_errors(v) }
193
+ else
194
+ fa, fragment, message, errors = errs.values_at(:failed_attribute,
195
+ :fragment,
196
+ :message, :errors)
197
+ ((['AllOf','AnyOf','Not'].include?(fa) && fragment =='#/') ?
198
+ [] : [massage_message(message)]) + _get_errors(errors || {})
199
+ end
200
+ end
201
+ end
202
+
203
+ def self.get_errors(errs)
204
+ _get_errors(errs).flatten
205
+ end
206
+ end
207
+ end
@@ -1,13 +1,8 @@
1
- class Marty::Diagnostic::Aws::Ec2Instance
1
+ class Marty::Diagnostic::Aws::Ec2Instance < Marty::Aws::Base
2
2
  # aws reserved host used to get instance meta-data
3
3
  META_DATA_HOST = '169.254.169.254'
4
4
 
5
- attr_reader :id,
6
- :doc,
7
- :role,
8
- :creds,
9
- :version,
10
- :host,
5
+ attr_reader :host,
11
6
  :tag,
12
7
  :nodes,
13
8
  :instances
@@ -33,76 +28,18 @@ class Marty::Diagnostic::Aws::Ec2Instance
33
28
  end
34
29
  end
35
30
 
36
- def self.is_aws?
37
- response = get("http://#{META_DATA_HOST}") rescue nil
38
- response.present?
39
- end
40
-
41
31
  def initialize
42
- @id = get_instance_id
43
- @doc = get_document
44
- @role = get_role
45
- @creds = get_credentials
46
- @host = "ec2.#{@doc['region']}.amazonaws.com"
47
- @version = '2016-11-15'
48
- @tag = get_tag
49
- @instances = InstancesSet.new(get_instances)
50
- @nodes = get_private_ips
51
- end
52
-
53
- def self.get url
54
- uri = URI.parse(url)
55
- request = Net::HTTP.new(uri.host, uri.port)
56
- request.read_timeout = request.open_timeout = ENV['DIAG_TIMEOUT'] || 0.25
57
- request.start {|http|
58
- http.get(uri.to_s)
59
- }.body
60
- end
61
-
62
- def query_meta_data query
63
- self.class.get("http://#{META_DATA_HOST}/latest/meta-data/#{query}/")
64
- end
65
-
66
- def query_dynamic query
67
- self.class.get("http://#{META_DATA_HOST}/latest/dynamic/#{query}/")
32
+ super
33
+ @service = 'ec2'
34
+ @tag = get_tag
35
+ @instances = InstancesSet.new(get_instances)
36
+ @nodes = get_private_ips
68
37
  end
69
38
 
70
39
  private
71
- def get_instance_id
72
- query_meta_data('instance-id').to_s
73
- end
74
-
75
- def get_role
76
- query_meta_data('iam/security-credentials').to_s
77
- end
78
-
79
- def get_credentials
80
- JSON.parse(query_meta_data("iam/security-credentials/#{@role}"))
81
- end
82
-
83
- def get_document
84
- JSON.parse(query_dynamic('instance-identity/document'))
85
- end
86
-
87
40
  def ec2_request action, params = {}
88
- default = {
89
- 'Action' => action,
90
- 'Version' => @version
91
- }
92
-
93
- url = "https://#{@host}/?" +
94
- (default + params).map{|a, v| "#{a}=#{v}"}.join('&')
95
-
96
- sig = Aws::Sigv4::Signer.new(service: 'ec2',
97
- region: @doc['region'],
98
- access_key_id: @creds['AccessKeyId'],
99
- secret_access_key: @creds['SecretAccessKey'],
100
- session_token: @creds['Token'])
101
- signed_url = sig.presign_url(http_method:'GET', url: url)
102
-
103
- http = Net::HTTP.new(@host, 443)
104
- http.use_ssl = true
105
- Hash.from_xml(Net::HTTP.get(signed_url))["#{action}Response"]
41
+ resp = request({action: action}, params)
42
+ Hash.from_xml(resp)["#{action}Response"]
106
43
  end
107
44
 
108
45
  def get_tag
@@ -117,9 +54,11 @@ class Marty::Diagnostic::Aws::Ec2Instance
117
54
  params = {'Filter.1.Name' => 'tag-value',
118
55
  'Filter.1.Value.1' => @tag}
119
56
 
57
+ resp = ec2_request('DescribeInstances', params)
58
+
120
59
  instances = ensure_resp(
121
60
  ['reservationSet', 'item', 'instancesSet', 'item'],
122
- ec2_request('DescribeInstances', params)
61
+ resp
123
62
  ).map do |i|
124
63
  {
125
64
  'id' => i['instanceId'],
@@ -132,18 +71,4 @@ class Marty::Diagnostic::Aws::Ec2Instance
132
71
  def get_private_ips
133
72
  @instances.running.map{|i| i['ip']}.compact
134
73
  end
135
-
136
- def ensure_resp path, obj
137
- if path == []
138
- obj.is_a?(Array) ? obj : [obj]
139
- elsif obj.is_a?(Hash)
140
- key = path.shift
141
- raise "Unexpected AWS Response: #{key} missing" unless
142
- (obj.is_a?(Hash) && obj[key])
143
-
144
- ensure_resp(path, obj[key])
145
- else
146
- obj.map{|s| ensure_resp(path.clone, s)}.flatten(1)
147
- end
148
- end
149
74
  end