etna 0.1.16 → 0.1.18

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/bin/etna +63 -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 +368 -0
  7. data/lib/etna.rb +6 -0
  8. data/lib/etna/application.rb +38 -20
  9. data/lib/etna/client.rb +60 -29
  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 +2 -0
  16. data/lib/etna/clients/magma/client.rb +24 -9
  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 +323 -9
  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 +1 -0
  34. data/lib/etna/clients/metis/client.rb +207 -5
  35. data/lib/etna/clients/metis/models.rb +174 -3
  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 +235 -5
  45. data/lib/etna/controller.rb +4 -0
  46. data/lib/etna/environment_scoped.rb +19 -0
  47. data/lib/etna/generate_autocompletion_script.rb +130 -0
  48. data/lib/etna/json_serializable_struct.rb +6 -3
  49. data/lib/etna/logger.rb +0 -3
  50. data/lib/etna/multipart_serializable_nested_hash.rb +6 -1
  51. data/lib/etna/route.rb +1 -1
  52. data/lib/etna/spec/vcr.rb +98 -0
  53. data/lib/etna/templates/attribute_actions_template.json +43 -0
  54. data/lib/etna/test_auth.rb +3 -1
  55. data/lib/etna/user.rb +4 -0
  56. data/lib/helpers.rb +81 -0
  57. metadata +47 -7
@@ -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,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,130 @@
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
+ generate_flag_handling
76
+
77
+ scope.subcommands.each do |name, command|
78
+ write %Q(elif [[ "$1" == "#{name}" ]]; then)
79
+ write 'shift'
80
+ if command.class.included_modules.include?(CommandExecutor)
81
+ generate_for_scope(command)
82
+ else
83
+ generate_for_command(command)
84
+ end
85
+ end
86
+
87
+ write "else"
88
+ write "return"
89
+ write "fi"
90
+ write 'done'
91
+ end
92
+
93
+ def program_name
94
+ $PROGRAM_NAME
95
+ end
96
+
97
+ def execute
98
+ name = File.basename(program_name)
99
+
100
+ write <<-EOF
101
+ #!/usr/bin/env bash
102
+
103
+ function _#{name}_completions() {
104
+ _#{name}_inner_completions "${COMP_WORDS[@]:1:COMP_CWORD}"
105
+ }
106
+
107
+ function _#{name}_inner_completions() {
108
+ local all_flag_completion_names=''
109
+ local string_flag_completion_names=''
110
+ local all_completion_names=''
111
+ local i=''
112
+ local a=''
113
+ EOF
114
+ generate_for_scope(parent)
115
+ write <<-EOF
116
+ }
117
+
118
+ complete -o default -F _#{name}_completions #{name}
119
+ EOF
120
+
121
+ File.open("#{name}.completion", 'w') { |f| f.write(@script) }
122
+ end
123
+
124
+ def write(string)
125
+ @script ||= ""
126
+ @script << string
127
+ @script << "\n"
128
+ end
129
+ end
130
+ end
@@ -20,11 +20,14 @@ module Etna
20
20
  end
21
21
  end
22
22
 
23
- def as_json
24
- members.map do |k|
23
+ def as_json(keep_nils: false)
24
+ inner_json = members.map do |k|
25
25
  v = self.class.as_json(send(k))
26
26
  [k, v]
27
- end.to_h.delete_if { |k, v| v.nil? }
27
+ end.to_h
28
+
29
+ return inner_json if keep_nils
30
+ inner_json.delete_if { |k, v| v.nil? }
28
31
  end
29
32
 
30
33
  def to_json
@@ -16,17 +16,14 @@ module Etna
16
16
 
17
17
  def warn(msg, &block)
18
18
  super
19
- Rollbar.warn(msg)
20
19
  end
21
20
 
22
21
  def error(msg, &block)
23
22
  super
24
- Rollbar.error(msg)
25
23
  end
26
24
 
27
25
  def fatal(msg, &block)
28
26
  super
29
- Rollbar.error(msg)
30
27
  end
31
28
 
32
29
  def log_error(e)
@@ -20,7 +20,12 @@ module Etna
20
20
  end
21
21
  else
22
22
  raise "base_key cannot be empty for a scalar value!" if base_key.length == 0
23
- yield [base_key, value.respond_to?(:read) ? UploadIO.new(value, 'application/octet-stream') : value.to_s]
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
24
29
  end
25
30
  end
26
31
 
@@ -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)
@@ -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
@@ -0,0 +1,43 @@
1
+ // Make sure to remove all comments before using -- JSON doesn't allow comments!
2
+ [
3
+ // Attribute actions can be "add_attribute", "update_attribute", "rename_attribute", or "add_link".
4
+ // These are all executed at the same time -- so you can't "update" an attribute that you "add"
5
+ // in the same file -- everything must already exist on the server."
6
+ {
7
+ "action_name": "add_attribute",
8
+ "model_name": "assay_name", // Each action must include a model_name that it applies to.
9
+ "attribute_type": "string", // When adding, you must include attribute_type, attribute_name, desc, and display_name.
10
+ "attribute_name": "notes",
11
+ "display_name": "Notes",
12
+ "desc": "for notes that you have."
13
+ },
14
+ {
15
+ "action_name": "update_attribute",
16
+ "model_name": "document",
17
+ "attribute_name": "version", // When updating, you must include model_name and attribute_name, but any additional values are optional.
18
+ "read_only": true
19
+ },
20
+ {
21
+ "action_name": "rename_attribute",
22
+ "model_name": "assay_name",
23
+ "attribute_name": "vendor",
24
+ "new_attribute_name": "e_vendor" // When renaming an attribute, you need to include a "new_attribute_name" value.
25
+ },
26
+ {
27
+ // Links require two hashes inside of a "links" attribute.
28
+ "action_name": "add_link",
29
+ "links": [
30
+ {
31
+ "model_name": "assay_name",
32
+ "attribute_name": "document",
33
+ "attribute_type": "link" // One link must be of type "link". This has a one-to-one relationship with the other model.
34
+ // In this case, each assay_name has one document record, but document records can point to zero-or-more assay_name records.
35
+ },
36
+ {
37
+ "model_name": "document",
38
+ "attribute_name": "assay_name",
39
+ "attribute_type": "collection" // The other link must be of type "collection". This is a one-to-many relationship with the other model.
40
+ }
41
+ ]
42
+ }
43
+ ]
@@ -43,7 +43,9 @@ module Etna
43
43
  end
44
44
 
45
45
  def approve_hmac(request)
46
- hmac_signature = etna_param(request, :signature) || 'invalid'
46
+ hmac_signature = etna_param(request, :signature)
47
+
48
+ return false unless hmac_signature
47
49
 
48
50
  headers = (etna_param(request, :headers)&.split(/,/) || []).map do |header|
49
51
  [ header.to_sym, etna_param(request, header) ]
@@ -81,5 +81,9 @@ module Etna
81
81
  def is_admin? project
82
82
  is_superuser? || has_roles(project, :admin)
83
83
  end
84
+
85
+ def active? project=nil
86
+ permissions.keys.length > 0
87
+ end
84
88
  end
85
89
  end
@@ -0,0 +1,81 @@
1
+ require_relative './etna/clients'
2
+ require_relative './etna/environment_scoped'
3
+
4
+ module WithEtnaClients
5
+ def environment
6
+ EtnaApp.instance.environment
7
+ end
8
+
9
+ def token
10
+ env_token = ENV['TOKEN']
11
+ if !env_token
12
+ puts "No environment variable TOKEN is set. You should set your token with `export TOKEN=<your.janus.token>` before running."
13
+ redirect = EtnaApp.instance.config(:auth_redirect)
14
+
15
+ if redirect.nil? && EtnaApp.instance.environment == :production
16
+ redirect = 'https://janus.ucsf.edu/'
17
+ end
18
+
19
+ unless redirect.nil?
20
+ puts "Open your browser to #{redirect} to complete login and copy your token."
21
+ end
22
+
23
+ exit
24
+ end
25
+
26
+ env_token
27
+ end
28
+
29
+ def magma_client
30
+ @magma_client ||= Etna::Clients::Magma.new(
31
+ token: token,
32
+ ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
33
+ # Persistent connections cause problem with magma restarts, until we can fix that we should force them
34
+ # to close + reopen each request.
35
+ persistent: false,
36
+ **EtnaApp.instance.config(:magma, environment) || {})
37
+ end
38
+
39
+ def metis_client
40
+ @metis_client ||= Etna::Clients::Metis.new(
41
+ token: token,
42
+ ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
43
+ **EtnaApp.instance.config(:metis, environment) || {})
44
+ end
45
+
46
+ def janus_client
47
+ @janus_client ||= Etna::Clients::Janus.new(
48
+ token: token,
49
+ ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
50
+ **EtnaApp.instance.config(:janus, environment) || {})
51
+ end
52
+
53
+ def polyphemus_client
54
+ @polyphemus_client ||= Etna::Clients::Polyphemus.new(
55
+ token: token,
56
+ ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
57
+ **EtnaApp.instance.config(:polyphemus, environment) || {})
58
+ end
59
+ end
60
+
61
+ module WithLogger
62
+ def logger
63
+ EtnaApp.instance.logger
64
+ end
65
+ end
66
+
67
+ module StrongConfirmation
68
+ def confirm
69
+ puts "Confirm Y/n:"
70
+ input = STDIN.gets.chomp
71
+ if input != "Y"
72
+ return false
73
+ end
74
+
75
+ true
76
+ end
77
+ end
78
+
79
+ WithEtnaClientsByEnvironment = EnvironmentScoped.new do
80
+ include WithEtnaClients
81
+ end