etna 0.1.13 → 0.1.19

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/bin/etna +18 -0
  3. data/etna.completion +926 -0
  4. data/etna_app.completion +133 -0
  5. data/ext/completions/extconf.rb +20 -0
  6. data/lib/commands.rb +360 -0
  7. data/lib/etna.rb +6 -0
  8. data/lib/etna/application.rb +46 -22
  9. data/lib/etna/client.rb +82 -48
  10. data/lib/etna/clients.rb +4 -0
  11. data/lib/etna/clients/enum.rb +9 -0
  12. data/lib/etna/clients/janus.rb +2 -0
  13. data/lib/etna/clients/janus/client.rb +73 -0
  14. data/lib/etna/clients/janus/models.rb +78 -0
  15. data/lib/etna/clients/magma.rb +4 -0
  16. data/lib/etna/clients/magma/client.rb +80 -0
  17. data/lib/etna/clients/magma/formatting.rb +1 -0
  18. data/lib/etna/clients/magma/formatting/models_csv.rb +345 -0
  19. data/lib/etna/clients/magma/models.rb +579 -0
  20. data/lib/etna/clients/magma/workflows.rb +10 -0
  21. data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +78 -0
  22. data/lib/etna/clients/magma/workflows/attribute_actions_from_json_workflow.rb +62 -0
  23. data/lib/etna/clients/magma/workflows/create_project_workflow.rb +117 -0
  24. data/lib/etna/clients/magma/workflows/crud_workflow.rb +85 -0
  25. data/lib/etna/clients/magma/workflows/ensure_containing_record_workflow.rb +44 -0
  26. data/lib/etna/clients/magma/workflows/file_attributes_blank_workflow.rb +68 -0
  27. data/lib/etna/clients/magma/workflows/file_linking_workflow.rb +115 -0
  28. data/lib/etna/clients/magma/workflows/json_converters.rb +81 -0
  29. data/lib/etna/clients/magma/workflows/json_validators.rb +447 -0
  30. data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +306 -0
  31. data/lib/etna/clients/magma/workflows/record_synchronization_workflow.rb +63 -0
  32. data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +178 -0
  33. data/lib/etna/clients/metis.rb +3 -0
  34. data/lib/etna/clients/metis/client.rb +239 -0
  35. data/lib/etna/clients/metis/models.rb +313 -0
  36. data/lib/etna/clients/metis/workflows.rb +2 -0
  37. data/lib/etna/clients/metis/workflows/metis_download_workflow.rb +37 -0
  38. data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +137 -0
  39. data/lib/etna/clients/polyphemus.rb +3 -0
  40. data/lib/etna/clients/polyphemus/client.rb +33 -0
  41. data/lib/etna/clients/polyphemus/models.rb +68 -0
  42. data/lib/etna/clients/polyphemus/workflows.rb +1 -0
  43. data/lib/etna/clients/polyphemus/workflows/set_configuration_workflow.rb +47 -0
  44. data/lib/etna/command.rb +243 -5
  45. data/lib/etna/controller.rb +4 -0
  46. data/lib/etna/directed_graph.rb +56 -0
  47. data/lib/etna/environment_scoped.rb +19 -0
  48. data/lib/etna/generate_autocompletion_script.rb +131 -0
  49. data/lib/etna/hmac.rb +1 -0
  50. data/lib/etna/json_serializable_struct.rb +37 -0
  51. data/lib/etna/logger.rb +15 -1
  52. data/lib/etna/multipart_serializable_nested_hash.rb +50 -0
  53. data/lib/etna/parse_body.rb +1 -1
  54. data/lib/etna/route.rb +1 -1
  55. data/lib/etna/server.rb +3 -0
  56. data/lib/etna/spec/vcr.rb +98 -0
  57. data/lib/etna/templates/attribute_actions_template.json +43 -0
  58. data/lib/etna/test_auth.rb +4 -2
  59. data/lib/etna/user.rb +11 -1
  60. data/lib/helpers.rb +87 -0
  61. metadata +69 -5
@@ -12,6 +12,7 @@ module Etna
12
12
  @logger = @request.env['etna.logger']
13
13
  @user = @request.env['etna.user']
14
14
  @request_id = @request.env['etna.request_id']
15
+ @hmac = @request.env['etna.hmac']
15
16
  end
16
17
 
17
18
  def log(line)
@@ -23,11 +24,14 @@ module Etna
23
24
 
24
25
  return send(@action) if @action
25
26
 
27
+
26
28
  [501, {}, ['This controller is not implemented.']]
27
29
  rescue Etna::Error => e
30
+ Rollbar.error(e)
28
31
  @logger.error(request_msg("Exiting with #{e.status}, #{e.message}"))
29
32
  return failure(e.status, error: e.message)
30
33
  rescue Exception => e
34
+ Rollbar.error(e)
31
35
  @logger.error(request_msg('Caught unspecified error'))
32
36
  @logger.error(request_msg(e.message))
33
37
  e.backtrace.each do |trace|
@@ -0,0 +1,56 @@
1
+ class DirectedGraph
2
+ def initialize
3
+ @children = {}
4
+ @parents = {}
5
+ end
6
+
7
+ attr_reader :children
8
+ attr_reader :parents
9
+
10
+ def add_connection(parent, child)
11
+ children = @children[parent] ||= {}
12
+ child_children = @children[child] ||= {}
13
+
14
+ children[child] = child_children
15
+
16
+ parents = @parents[child] ||= {}
17
+ parent_parents = @parents[parent] ||= {}
18
+ parents[parent] = parent_parents
19
+ end
20
+
21
+ def descendants(parent)
22
+ seen = Set.new
23
+
24
+ seen.add(parent)
25
+ queue = @children[parent].keys.dup
26
+ parent_queue = @parents[parent].keys.dup
27
+
28
+ # Because this is not an acyclical graph, the definition of descendants needs to be stronger;
29
+ # here we believe that any path that would move through --any-- parent to this child would not be considered
30
+ # descendant, so we first find all those parents and mark them as 'seen' so that they are not traveled.
31
+ while next_parent = parent_queue.pop
32
+ next if seen.include?(next_parent)
33
+ seen.add(next_parent)
34
+ parent_queue.push(*@parents[next_parent].keys)
35
+ end
36
+
37
+ queue = queue.nil? ? [] : queue.dup
38
+ paths = {}
39
+
40
+ while child = queue.pop
41
+ next if seen.include? child
42
+ seen.add(child)
43
+ path = (paths[child] ||= [parent])
44
+
45
+ @children[child].keys.each do |child_child|
46
+ queue.push child_child
47
+
48
+ unless paths.include? child_child
49
+ paths[child_child] = path + [child]
50
+ end
51
+ end
52
+ end
53
+
54
+ paths
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ class EnvironmentScoped < Module
2
+ def initialize(&block)
3
+ environment_class = Class.new do
4
+ class_eval(&block)
5
+
6
+ attr_reader :environment
7
+ def initialize(environment)
8
+ @environment = environment
9
+ end
10
+ end
11
+
12
+ super() do
13
+ define_method :environment do |env|
14
+ env = env.to_sym
15
+ (@envs ||= {})[env] ||= environment_class.new(env)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,131 @@
1
+ require_relative 'command'
2
+
3
+ # application.rb instantiates this for the project scoping.
4
+ # This generates a file, project-name.completion, which is sourced
5
+ # in build.sh to provide autocompletion in that environment.
6
+ module Etna
7
+ class GenerateCompletionScript < Etna::Command
8
+ def generate_for_command(command)
9
+ completions = command.completions
10
+ completions.each do |c|
11
+ generate_start_match(c, false)
12
+ write "fi"
13
+ write "shift"
14
+ end
15
+
16
+ enable_flags(command.class)
17
+ write 'while [[ "$#" != "0" ]]; do'
18
+ generate_start_match([])
19
+ generate_flag_handling
20
+
21
+ write "else"
22
+ write "return"
23
+ write 'fi'
24
+ write 'done'
25
+ write "return"
26
+ end
27
+
28
+ def generate_flag_handling
29
+ write %Q(elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then)
30
+ write "return"
31
+ write %Q(elif [[ "$all_flag_completion_names" =~ $1\\ ]]; then)
32
+ write %Q(all_flag_completion_names="${all_flag_completion_names//$1\\ /}")
33
+ write 'a=$1'
34
+ write 'shift'
35
+ write %Q(if [[ "$string_flag_completion_names" =~ $a\\ ]]; then)
36
+ write 'if [[ "$#" == "1" ]]; then'
37
+ write %Q(a="${a//--/}")
38
+ write %Q(a="${a//-/_}")
39
+ write %Q(i="_completions_for_$a")
40
+ write %Q(all_completion_names="${!i}")
41
+ write 'COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))'
42
+ write 'return'
43
+ write 'fi'
44
+ write 'shift'
45
+ write 'fi'
46
+ end
47
+
48
+ def generate_start_match(completions, include_flags=true)
49
+ write 'if [[ "$#" == "1" ]]; then'
50
+ write %Q(all_completion_names="#{completions.join(' ')}")
51
+ write %Q(all_completion_names="$all_completion_names $all_flag_completion_names") if include_flags
52
+ write %Q(if [[ -z "$(echo $all_completion_names | xargs)" ]]; then)
53
+ write 'return'
54
+ write 'fi'
55
+ write 'COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))'
56
+ write 'return'
57
+ end
58
+
59
+ def enable_flags(flags_container)
60
+ boolean_flags = flags_container.boolean_flags
61
+ string_flags = flags_container.string_flags
62
+ flags = boolean_flags + string_flags
63
+ write %Q(all_flag_completion_names="$all_flag_completion_names #{flags.join(' ')} ")
64
+ write %Q(string_flag_completion_names="$string_flag_completion_names #{string_flags.join(' ')} ")
65
+
66
+ string_flags.each do |flag|
67
+ write %Q(declare _completions_for_#{flag_as_parameter(flag)}="#{completions_for(flag_as_parameter(flag)).join(' ')}")
68
+ end
69
+ end
70
+
71
+ def generate_for_scope(scope)
72
+ enable_flags(scope.class)
73
+ write 'while [[ "$#" != "0" ]]; do'
74
+ generate_start_match(scope.subcommands.keys)
75
+
76
+ scope.subcommands.each do |name, command|
77
+ write %Q(elif [[ "$1" == "#{name}" ]]; then)
78
+ write 'shift'
79
+ if command.class.included_modules.include?(CommandExecutor)
80
+ generate_for_scope(command)
81
+ else
82
+ generate_for_command(command)
83
+ end
84
+ end
85
+
86
+ generate_flag_handling
87
+
88
+ write "else"
89
+ write "return"
90
+ write "fi"
91
+ write 'done'
92
+ end
93
+
94
+ def program_name
95
+ $PROGRAM_NAME
96
+ end
97
+
98
+ def execute
99
+ name = File.basename(program_name)
100
+
101
+ write <<-EOF
102
+ #!/usr/bin/env bash
103
+
104
+ function _#{name}_completions() {
105
+ _#{name}_inner_completions "${COMP_WORDS[@]:1:COMP_CWORD}"
106
+ }
107
+
108
+ function _#{name}_inner_completions() {
109
+ local all_flag_completion_names=''
110
+ local string_flag_completion_names=''
111
+ local all_completion_names=''
112
+ local i=''
113
+ local a=''
114
+ EOF
115
+ generate_for_scope(parent)
116
+ write <<-EOF
117
+ }
118
+
119
+ complete -o default -F _#{name}_completions #{name}
120
+ EOF
121
+
122
+ File.open("#{name}.completion", 'w') { |f| f.write(@script) }
123
+ end
124
+
125
+ def write(string)
126
+ @script ||= ""
127
+ @script << string
128
+ @script << "\n"
129
+ end
130
+ end
131
+ end
@@ -60,6 +60,7 @@ module Etna
60
60
  end
61
61
 
62
62
  def valid_id?
63
+ return false if !@application.config(:hmac_keys)
63
64
  @application.config(:hmac_keys).key?(@id)
64
65
  end
65
66
 
@@ -0,0 +1,37 @@
1
+ module Etna
2
+ module JsonSerializableStruct
3
+ def self.included(cls)
4
+ cls.instance_eval do
5
+ def self.as_json(v)
6
+ if v.respond_to? :as_json
7
+ return v.as_json
8
+ end
9
+
10
+ if v.is_a? Hash
11
+ return v.map { |k, v| [k, as_json(v)] }.to_h
12
+ end
13
+
14
+ if v.class.include? Enumerable
15
+ return v.map { |v| as_json(v) }
16
+ end
17
+
18
+ v
19
+ end
20
+ end
21
+ end
22
+
23
+ def as_json(keep_nils: false)
24
+ inner_json = members.map do |k|
25
+ v = self.class.as_json(send(k))
26
+ [k, v]
27
+ end.to_h
28
+
29
+ return inner_json if keep_nils
30
+ inner_json.delete_if { |k, v| v.nil? }
31
+ end
32
+
33
+ def to_json
34
+ as_json.to_json
35
+ end
36
+ end
37
+ end
@@ -1,10 +1,10 @@
1
1
  require 'logger'
2
+ require 'rollbar'
2
3
 
3
4
  module Etna
4
5
  class Logger < ::Logger
5
6
  def initialize(log_dev, age, size)
6
7
  super
7
-
8
8
  self.formatter = proc do |severity, datetime, progname, msg|
9
9
  format(severity, datetime, progname, msg)
10
10
  end
@@ -14,11 +14,25 @@ module Etna
14
14
  "#{severity}:#{datetime.iso8601} #{msg}\n"
15
15
  end
16
16
 
17
+ def warn(msg, &block)
18
+ super
19
+ end
20
+
21
+ def error(msg, &block)
22
+ super
23
+ end
24
+
25
+ def fatal(msg, &block)
26
+ super
27
+ end
28
+
17
29
  def log_error(e)
18
30
  error(e.message)
19
31
  e.backtrace.each do |trace|
20
32
  error(trace)
21
33
  end
34
+
35
+ Rollbar.error(e)
22
36
  end
23
37
 
24
38
  def log_request(request)
@@ -0,0 +1,50 @@
1
+ module Etna
2
+ module MultipartSerializableNestedHash
3
+ def self.included(cls)
4
+ cls.instance_eval do
5
+ def self.encode_multipart_pairs(value, base_key, is_root, &block)
6
+ if value.is_a? Hash
7
+ value.each do |k, v|
8
+ encode_multipart_pairs(v, is_root ? k : "#{base_key}[#{k}]", false, &block)
9
+ end
10
+ elsif value.is_a? Array
11
+ value.each_with_index do |v, i|
12
+ # This is necessary to ensure that arrays of hashes that have hetergenous keys still get parsed correctly
13
+ # Since the only way to indicate a new entry in the array of hashes is by re-using a key that existed in
14
+ # the previous hash.
15
+ if v.is_a? Hash
16
+ encode_multipart_pairs(i, "#{base_key}[][_idx]", false, &block)
17
+ end
18
+
19
+ encode_multipart_pairs(v, "#{base_key}[]", false, &block)
20
+ end
21
+ else
22
+ raise "base_key cannot be empty for a scalar value!" if base_key.length == 0
23
+
24
+ if value.respond_to?(:read)
25
+ yield [base_key, UploadIO.new(value, 'application/octet-stream'), {filename: 'blob'}]
26
+ else
27
+ yield [base_key, value.to_s]
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ def self.encode_multipart_content(value, base_key = '', is_root = true)
34
+ result = []
35
+ self.encode_multipart_pairs(value, base_key, is_root) { |pair| result << pair }
36
+ result
37
+ end
38
+ end
39
+ end
40
+
41
+ def encode_multipart_content(base_key = '')
42
+ value = self
43
+ if value.respond_to? :as_json
44
+ value = value.as_json
45
+ end
46
+
47
+ self.class.encode_multipart_content(value, base_key)
48
+ end
49
+ end
50
+ end
@@ -23,7 +23,7 @@ module Etna
23
23
  )
24
24
  when %r{multipart/form-data}i
25
25
  params.update(
26
- Rack::Multipart.parse_multipart(env)
26
+ Rack::Multipart.parse_multipart(env) || {}
27
27
  )
28
28
  end
29
29
  # Always parse the params that are url-encoded.
@@ -118,7 +118,7 @@ module Etna
118
118
 
119
119
  def hmac_authorized?(request)
120
120
  # either there is no hmac requirement, or we have a valid hmac
121
- !@auth[:hmac] || request.env['etna.hmac'].valid?
121
+ !@auth[:hmac] || request.env['etna.hmac']&.valid?
122
122
  end
123
123
 
124
124
  def route_name(options)
@@ -66,6 +66,9 @@ module Etna
66
66
  end
67
67
 
68
68
  [404, {}, ["There is no such path '#{request.path}'"]]
69
+ rescue => e
70
+ application.logger.log_error(e)
71
+ raise
69
72
  end
70
73
 
71
74
  def initialize
@@ -0,0 +1,98 @@
1
+ require 'webmock/rspec'
2
+ require 'vcr'
3
+ require 'openssl'
4
+ require 'digest/sha2'
5
+ require 'base64'
6
+
7
+ def setup_base_vcr(spec_helper_dir)
8
+ VCR.configure do |c|
9
+ c.hook_into :webmock
10
+ c.cassette_serializers
11
+ c.cassette_library_dir = ::File.join(spec_helper_dir, 'fixtures', 'cassettes')
12
+ c.allow_http_connections_when_no_cassette = true
13
+
14
+ c.register_request_matcher :try_body do |request_1, request_2|
15
+ if request_1.headers['Content-Type'].first =~ /application\/json/
16
+ if request_2.headers['Content-Type'].first =~ /application\/json/
17
+ request_1_json = begin
18
+ JSON.parse(request_1.body) rescue 'not-json'
19
+ end
20
+ request_2_json = begin
21
+ JSON.parse(request_2.body) rescue 'not-json'
22
+ end
23
+ request_1_json == request_2_json
24
+ else
25
+ false
26
+ end
27
+ else
28
+ request_1.body == request_2.body
29
+ end
30
+ end
31
+
32
+ c.default_cassette_options = {
33
+ serialize_with: :compressed,
34
+ record: if ENV['IS_CI'] == '1'
35
+ :none
36
+ else
37
+ ENV['RERECORD'] ? :all : :once
38
+ end,
39
+ match_requests_on: [:method, :uri, :try_body]
40
+ }
41
+
42
+ # Filter the authorization headers of any request by replacing any occurrence of that request's
43
+ # Authorization value with <AUTHORIZATION>
44
+ c.filter_sensitive_data('<AUTHORIZATION>') do |interaction|
45
+ interaction.request.headers['Authorization'].first
46
+ end
47
+
48
+ c.before_record do |interaction|
49
+ key = prepare_vcr_secret
50
+
51
+ if interaction.response.body && !interaction.response.body.empty?
52
+ cipher = OpenSSL::Cipher.new("AES-256-CBC")
53
+ iv = cipher.random_iv
54
+
55
+ cipher.encrypt
56
+ cipher.key = key
57
+ cipher.iv = iv
58
+
59
+ encrypted = cipher.update(interaction.response.body)
60
+ encrypted << cipher.final
61
+
62
+ interaction.response.body = [iv, encrypted].pack('mm')
63
+ end
64
+ end
65
+
66
+ c.before_playback do |interaction|
67
+ key = prepare_vcr_secret
68
+
69
+ if interaction.response.body && !interaction.response.body.empty?
70
+ iv, encrypted = interaction.response.body.unpack('mm')
71
+
72
+ cipher = OpenSSL::Cipher.new("AES-256-CBC")
73
+ cipher.decrypt
74
+ cipher.key = key
75
+ cipher.iv = iv
76
+
77
+ plain = cipher.update(encrypted)
78
+ plain << cipher.final
79
+
80
+ interaction.response.body = plain
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ def prepare_vcr_secret
87
+ secret = ENV["CI_SECRET"]
88
+
89
+ if (secret.nil? || secret.empty?) && ENV['IS_CI'] != '1'
90
+ current_example = RSpec.current_example
91
+ RSpec::Core::Pending.mark_pending! current_example, 'CI_SECRET must be set to run this test'
92
+ raise "CI_SECRET must be set to run this test"
93
+ end
94
+
95
+ digest = Digest::SHA256.new
96
+ digest.update(secret)
97
+ digest.digest
98
+ end