etna 0.1.32 → 0.1.37

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb19650be05ef97433ec00556981388436eb54ee48844055218f3ec290b10f3f
4
- data.tar.gz: fbbee21325ddd316b4a742bc0cd143cba33c4bce635ad767720ed2d330da5446
3
+ metadata.gz: 484c950fffbafcf5132df923bc0ef88a79faa6c285ccde653805aac526e4a97e
4
+ data.tar.gz: f513b75b048e816acc494c09cb0eccbc2c38cda02c2723266fe7bd98c8595f3a
5
5
  SHA512:
6
- metadata.gz: ee5548756441715e1944ced74613ae21f328910c3dcd982d638918f0df432d0ca8ee24b4ac62d7f86b17b3325483635e7e2b2ae750f93b82ef9a5629d925de01
7
- data.tar.gz: ce866396a93210565bfc6bd9d5ff1acd69833c3bb9ff55a845e4fbdaad3769b65b0402ee3dc4c0ea937aacb7eac1432b9460ff5612c4344317bd4fcffaec4771
6
+ metadata.gz: 6ad0e99925dc6110c8f80f4a1e1417d610b28838497f09afe8be1c69a65fec95a7d549af987aaccde2b42480347bf336838be5398ef580d3024476206b4d611d
7
+ data.tar.gz: 44eb05596c635e7ab96df7971fed6b24bfee7bd545238e173e3d1b57bdcfe7b30db5deea355d2711b192d026d8fb72afc32cbcbcf4a83ef92848184433b7a813
data/etna.completion CHANGED
@@ -32,7 +32,7 @@ arg_flag_completion_names="$arg_flag_completion_names "
32
32
  multi_flags="$multi_flags "
33
33
  while [[ "$#" != "0" ]]; do
34
34
  if [[ "$#" == "1" ]]; then
35
- all_completion_names="help models project"
35
+ all_completion_names="help models project token"
36
36
  all_completion_names="$all_completion_names $all_flag_completion_names"
37
37
  if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
38
38
  return
@@ -722,6 +722,120 @@ else
722
722
  return
723
723
  fi
724
724
  done
725
+ elif [[ "$1" == "token" ]]; then
726
+ shift
727
+ all_flag_completion_names="$all_flag_completion_names "
728
+ arg_flag_completion_names="$arg_flag_completion_names "
729
+ multi_flags="$multi_flags "
730
+ while [[ "$#" != "0" ]]; do
731
+ if [[ "$#" == "1" ]]; then
732
+ all_completion_names="generate help"
733
+ all_completion_names="$all_completion_names $all_flag_completion_names"
734
+ if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
735
+ return
736
+ fi
737
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
738
+ return
739
+ elif [[ "$1" == "generate" ]]; then
740
+ shift
741
+ all_flag_completion_names="$all_flag_completion_names --task --project-name "
742
+ arg_flag_completion_names="$arg_flag_completion_names --project-name "
743
+ multi_flags="$multi_flags "
744
+ declare _completions_for_project_name="__project_name__"
745
+ while [[ "$#" != "0" ]]; do
746
+ if [[ "$#" == "1" ]]; then
747
+ all_completion_names=""
748
+ all_completion_names="$all_completion_names $all_flag_completion_names"
749
+ if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
750
+ return
751
+ fi
752
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
753
+ return
754
+ elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
755
+ return
756
+ elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
757
+ if ! [[ "$multi_flags" =~ $1\ ]]; then
758
+ all_flag_completion_names="${all_flag_completion_names//$1\ /}"
759
+ fi
760
+ a=$1
761
+ shift
762
+ if [[ "$arg_flag_completion_names" =~ $a\ ]]; then
763
+ if [[ "$#" == "1" ]]; then
764
+ a="${a//--/}"
765
+ a="${a//-/_}"
766
+ i="_completions_for_$a"
767
+ all_completion_names="${!i}"
768
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
769
+ return
770
+ fi
771
+ shift
772
+ fi
773
+ else
774
+ return
775
+ fi
776
+ done
777
+ return
778
+ elif [[ "$1" == "help" ]]; then
779
+ shift
780
+ all_flag_completion_names="$all_flag_completion_names "
781
+ arg_flag_completion_names="$arg_flag_completion_names "
782
+ multi_flags="$multi_flags "
783
+ while [[ "$#" != "0" ]]; do
784
+ if [[ "$#" == "1" ]]; then
785
+ all_completion_names=""
786
+ all_completion_names="$all_completion_names $all_flag_completion_names"
787
+ if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
788
+ return
789
+ fi
790
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
791
+ return
792
+ elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
793
+ return
794
+ elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
795
+ if ! [[ "$multi_flags" =~ $1\ ]]; then
796
+ all_flag_completion_names="${all_flag_completion_names//$1\ /}"
797
+ fi
798
+ a=$1
799
+ shift
800
+ if [[ "$arg_flag_completion_names" =~ $a\ ]]; then
801
+ if [[ "$#" == "1" ]]; then
802
+ a="${a//--/}"
803
+ a="${a//-/_}"
804
+ i="_completions_for_$a"
805
+ all_completion_names="${!i}"
806
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
807
+ return
808
+ fi
809
+ shift
810
+ fi
811
+ else
812
+ return
813
+ fi
814
+ done
815
+ return
816
+ elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
817
+ return
818
+ elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
819
+ if ! [[ "$multi_flags" =~ $1\ ]]; then
820
+ all_flag_completion_names="${all_flag_completion_names//$1\ /}"
821
+ fi
822
+ a=$1
823
+ shift
824
+ if [[ "$arg_flag_completion_names" =~ $a\ ]]; then
825
+ if [[ "$#" == "1" ]]; then
826
+ a="${a//--/}"
827
+ a="${a//-/_}"
828
+ i="_completions_for_$a"
829
+ all_completion_names="${!i}"
830
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
831
+ return
832
+ fi
833
+ shift
834
+ fi
835
+ else
836
+ return
837
+ fi
838
+ done
725
839
  elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
726
840
  return
727
841
  elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
data/lib/commands.rb CHANGED
@@ -91,6 +91,36 @@ class EtnaApp
91
91
  class Administrate
92
92
  include Etna::CommandExecutor
93
93
 
94
+ class Token
95
+ include Etna::CommandExecutor
96
+
97
+ class Generate < Etna::Command
98
+ include WithLogger
99
+
100
+ boolean_flags << "--task"
101
+ string_flags << "--project-name"
102
+ string_flags << "--email"
103
+
104
+ def execute(email:, task: false, project_name: nil)
105
+ # the token is not required, but can be used if available
106
+ # to generate a task token, so we pass it in here
107
+ janus_client = Etna::Clients::Janus.new(
108
+ token: ENV['TOKEN'],
109
+ ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
110
+ **EtnaApp.instance.config(:janus, EtnaApp.instance.environment))
111
+
112
+ generate_token_workflow = Etna::Clients::Janus::GenerateTokenWorkflow.new(
113
+ janus_client: janus_client,
114
+ token_type: task ? 'task' : 'login',
115
+ email: email,
116
+ project_name: project_name,
117
+ private_key_file: EtnaApp.instance.config(:private_key, EtnaApp.instance.environment)
118
+ )
119
+ generate_token_workflow.generate!
120
+ end
121
+ end
122
+ end
123
+
94
124
  class Project
95
125
  include Etna::CommandExecutor
96
126
 
@@ -309,7 +339,7 @@ class EtnaApp
309
339
  request = Etna::Clients::Magma::RetrievalRequest.new(project_name: project_name)
310
340
  request.model_name = model_name
311
341
  request.attribute_names = 'all'
312
- request.record_names = 'all'
342
+ request.record_names = []
313
343
  model = magma_client.retrieve(request).models.model(model_name)
314
344
  model_parent_name = model.template.attributes.all.select do |attribute|
315
345
  attribute.attribute_type == Etna::Clients::Magma::AttributeType::PARENT
data/lib/etna.rb CHANGED
@@ -21,6 +21,7 @@ require_relative './etna/environment_scoped'
21
21
  require_relative './etna/filesystem'
22
22
  require_relative './etna/formatting'
23
23
  require_relative './etna/cwl'
24
+ require_relative './etna/metrics'
24
25
 
25
26
  class EtnaApp
26
27
  include Etna::Application
@@ -8,6 +8,7 @@ require_relative './command'
8
8
  require_relative './generate_autocompletion_script'
9
9
  require 'singleton'
10
10
  require 'rollbar'
11
+ require 'fileutils'
11
12
 
12
13
  module Etna::Application
13
14
  def self.included(other)
@@ -31,6 +32,12 @@ module Etna::Application
31
32
  raise "Could not find application instance from #{namespace}, and not subclass of Application found."
32
33
  end
33
34
 
35
+ # Used to find the application in development recorded vcr tests.
36
+ # see spec/vcr.rb
37
+ def dev_route
38
+ "#{self.class.name.split('::').first.downcase}.development.local"
39
+ end
40
+
34
41
  def self.register(app)
35
42
  @instance = app
36
43
  end
@@ -53,6 +60,23 @@ module Etna::Application
53
60
  end
54
61
  end
55
62
 
63
+ def setup_yabeda
64
+ Yabeda.configure!
65
+ end
66
+
67
+ def write_job_metrics(name)
68
+ node_metrics_dir = config(:node_metrics_dir) || "/tmp/metrics.prom"
69
+ ::FileUtils.mkdir_p(node_metrics_dir)
70
+
71
+ tmp_file = ::File.join(node_metrics_dir, "#{name}.prom.$$")
72
+ ::File.open(tmp_file, "w") do |f|
73
+ f.write(Prometheus::Client::Formats::Text.marshal(Prometheus::Client.registry))
74
+ end
75
+
76
+ require 'fileutils'
77
+ ::FileUtils.mv(tmp_file, ::File.join(node_metrics_dir, "#{name}.prom"))
78
+ end
79
+
56
80
  def setup_logger
57
81
  @logger = Etna::Logger.new(
58
82
  # The name of the log_file, required.
data/lib/etna/auth.rb CHANGED
@@ -70,6 +70,29 @@ module Etna
70
70
  return route && route.noauth?
71
71
  end
72
72
 
73
+ def janus_approved?(payload, token, request)
74
+ route = server.find_route(request)
75
+
76
+ # some routes don't need janus approval
77
+ return true if route && route.ignore_janus?
78
+
79
+ # only process task tokens right now
80
+ return true unless payload['task']
81
+
82
+ return false unless application.config(:janus) && application.config(:janus)[:host]
83
+
84
+ janus_client = Etna::Clients::Janus.new(
85
+ token: token,
86
+ host: application.config(:janus)[:host]
87
+ )
88
+
89
+ response = janus_client.validate_task_token
90
+
91
+ return false unless response.code == '200'
92
+
93
+ return true
94
+ end
95
+
73
96
  def approve_user(request)
74
97
  token = request.cookies[application.config(:token_name)] || auth(request, :etna)
75
98
 
@@ -77,6 +100,8 @@ module Etna
77
100
 
78
101
  begin
79
102
  payload, header = application.sign.jwt_decode(token)
103
+
104
+ return false unless janus_approved?(payload, token, request)
80
105
  return request.env['etna.user'] = Etna::User.new(payload.map{|k,v| [k.to_sym, v]}.to_h, token)
81
106
  rescue
82
107
  # bail out if anything goes wrong
data/lib/etna/client.rb CHANGED
@@ -17,6 +17,13 @@ module Etna
17
17
 
18
18
  attr_reader :routes
19
19
 
20
+ def with_headers(headers, &block)
21
+ @request_headers = headers.compact
22
+ result = instance_eval(&block)
23
+ @request_headers = nil
24
+ return result
25
+ end
26
+
20
27
  def signed_route_path(route, params)
21
28
  path = route_path(route,params)
22
29
 
@@ -111,7 +118,7 @@ module Etna
111
118
 
112
119
  def body_request(type, endpoint, params = {}, &block)
113
120
  uri = request_uri(endpoint)
114
- req = type.new(uri.request_uri, request_params)
121
+ req = type.new(uri.request_uri, request_headers)
115
122
  req.body = params.to_json
116
123
  request(uri, req, &block)
117
124
  end
@@ -124,7 +131,7 @@ module Etna
124
131
  else
125
132
  uri.query = URI.encode_www_form(params)
126
133
  end
127
- req = type.new(uri.request_uri, request_params)
134
+ req = type.new(uri.request_uri, request_headers)
128
135
  request(uri, req, &block)
129
136
  end
130
137
 
@@ -132,12 +139,14 @@ module Etna
132
139
  URI("#{@host}#{endpoint}")
133
140
  end
134
141
 
135
- def request_params
142
+ def request_headers
136
143
  {
137
144
  'Content-Type' => 'application/json',
138
145
  'Accept' => 'application/json, text/*',
139
146
  'Authorization' => "Etna #{@token}"
140
- }
147
+ }.update(
148
+ @request_headers || {}
149
+ )
141
150
  end
142
151
 
143
152
  def status_check!(response)
@@ -164,7 +173,7 @@ module Etna
164
173
  verify_mode = @ignore_ssl ?
165
174
  OpenSSL::SSL::VERIFY_NONE :
166
175
  OpenSSL::SSL::VERIFY_PEER
167
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode) do |http|
176
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
168
177
  http.request(data) do |response|
169
178
  status_check!(response)
170
179
  yield response
@@ -174,7 +183,7 @@ module Etna
174
183
  verify_mode = @ignore_ssl ?
175
184
  OpenSSL::SSL::VERIFY_NONE :
176
185
  OpenSSL::SSL::VERIFY_PEER
177
- Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode) do |http|
186
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, verify_mode: verify_mode, read_timeout: 300) do |http|
178
187
  response = http.request(data)
179
188
  status_check!(response)
180
189
  return response
@@ -8,10 +8,9 @@ module Etna
8
8
  attr_reader :host, :token, :ignore_ssl
9
9
  def initialize(host:, token:, ignore_ssl: false)
10
10
  raise "#{self.class.name} client configuration is missing host." unless host
11
- raise "#{self.class.name} client configuration is missing token." unless token
12
11
 
13
12
  @token = token
14
- raise "Your token is expired." if token_expired?
13
+ raise "Your token is expired." if token && token_expired?
15
14
 
16
15
  @etna_client = ::Etna::Client.new(
17
16
  host,
@@ -30,10 +29,11 @@ module Etna
30
29
  def token_will_expire?(offset=3000)
31
30
  # offset in seconds
32
31
  # Will the user's token expire in the given amount of time?
33
- epoch_seconds = JSON.parse(Base64.urlsafe_decode64(token.split('.')[1]))["exp"]
32
+ payload = JSON.parse(Base64.urlsafe_decode64(token.split('.')[1]))
33
+ epoch_seconds = payload["exp"]
34
34
  expiration = DateTime.strptime(epoch_seconds.to_s, "%s")
35
35
  expiration <= DateTime.now.new_offset + offset
36
36
  end
37
37
  end
38
38
  end
39
- end
39
+ end
@@ -1,2 +1,3 @@
1
1
  require_relative './janus/client'
2
2
  require_relative './janus/models'
3
+ require_relative './janus/workflows'
@@ -57,6 +57,24 @@ module Etna
57
57
 
58
58
  TokenResponse.new(token)
59
59
  end
60
+
61
+ def validate_task_token
62
+ @etna_client.post('/api/tokens/validate_task')
63
+ end
64
+
65
+ def get_nonce
66
+ @etna_client.get('/api/tokens/nonce').body
67
+ end
68
+
69
+ def generate_token(token_type, signed_nonce: nil, project_name: nil, read_only: false)
70
+ response = @etna_client.with_headers(
71
+ 'Authorization' => signed_nonce ? "Signed-Nonce #{signed_nonce}" : nil
72
+ ) do
73
+ post('/api/tokens/generate', token_type: token_type, project_name: project_name, read_only: read_only)
74
+ end
75
+
76
+ response.body
77
+ end
60
78
  end
61
79
  end
62
80
  end
@@ -51,6 +51,12 @@ module Etna
51
51
  end
52
52
  end
53
53
 
54
+ class ValidateTaskTokenRequest
55
+ def map
56
+ []
57
+ end
58
+ end
59
+
54
60
  class HtmlResponse
55
61
  attr_reader :raw
56
62
 
@@ -76,4 +82,4 @@ module Etna
76
82
  end
77
83
  end
78
84
  end
79
- end
85
+ end
@@ -0,0 +1 @@
1
+ require_relative './workflows/generate_token_workflow'
@@ -0,0 +1,77 @@
1
+ # Base workflow for setting up a project by a super user.
2
+ # 1) Creates the project in janus
3
+ # 2) Adds administrator(s) to Janus
4
+ # 3) Refreshes the user's token with the new privileges.
5
+ # 4) Creates the project in .
6
+
7
+ require 'base64'
8
+ require 'json'
9
+ require 'ostruct'
10
+ require_relative '../models'
11
+
12
+ module Etna
13
+ module Clients
14
+ class Janus
15
+ class GenerateTokenWorkflow < Struct.new(:janus_client, :email, :project_name, :token_type, :private_key_file, keyword_init: true)
16
+ def generate!
17
+ nonce = janus_client.get_nonce
18
+
19
+ unless email
20
+ puts "Email address for #{janus_client.host} account?"
21
+ email = STDIN.gets.chomp
22
+ end
23
+
24
+ if use_nonce?
25
+ until private_key_file
26
+ puts "Location of private key file?"
27
+ private_key_file = ::File.expand_path(STDIN.gets.chomp)
28
+ unless File.exists?(private_key_file)
29
+ puts "No such file."
30
+ private_key_file = nil
31
+ end
32
+ end
33
+ end
34
+
35
+ if needs_project_name?
36
+ puts "Project name?"
37
+ project_name = STDIN.gets.chomp
38
+ end
39
+
40
+ token = janus_client.generate_token(token_type, signed_nonce: use_nonce? ? signed_nonce(nonce) : nil, project_name: project_name)
41
+
42
+ puts token
43
+ end
44
+
45
+ private
46
+
47
+ def signed_nonce(nonce)
48
+ private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file))
49
+
50
+ txt_to_sign = "#{nonce}.#{Base64.strict_encode64(email)}"
51
+
52
+ sig = Base64.strict_encode64(
53
+ private_key.sign(OpenSSL::Digest::SHA256.new,txt_to_sign)
54
+ )
55
+
56
+ "#{txt_to_sign}.#{sig}"
57
+ end
58
+
59
+ def use_nonce?
60
+ !task_token? || !janus_client.token
61
+ end
62
+
63
+ def needs_project_name?
64
+ task_token? && !project_name
65
+ end
66
+
67
+ def task_token?
68
+ token_type == 'task'
69
+ end
70
+
71
+ def user
72
+ @user ||= JSON.parse(Base64.urlsafe_decode64(magma_client.token.split('.')[1]))
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -138,7 +138,7 @@ module Etna
138
138
  attribute_type: attribute.attribute_type,
139
139
  link_model_name: attribute.link_model_name,
140
140
  reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type,
141
- description: attribute.desc,
141
+ description: attribute.description,
142
142
  display_name: attribute.display_name,
143
143
  match: attribute.match,
144
144
  format_hint: attribute.format_hint,
@@ -175,7 +175,7 @@ module Etna
175
175
  # This should line up with the attribute names _on the model itself_.
176
176
  ATTRIBUTE_ROW_ENTRIES = [
177
177
  :attribute_type,
178
- :link_model_name, :desc,
178
+ :link_model_name, :description,
179
179
  :display_name, :format_hint,
180
180
  :restricted, :read_only,
181
181
  :validation, :attribute_group,
@@ -188,7 +188,6 @@ module Etna
188
188
 
189
189
  def format_row(row)
190
190
  replace_row_column(row, :attribute_type) { |s| AttributeType.new(s) }
191
- replace_row_column(row, :desc) { row.delete(:description) }
192
191
  replace_row_column(row, :restricted, &COLUMN_AS_BOOLEAN)
193
192
  replace_row_column(row, :read_only, &COLUMN_AS_BOOLEAN)
194
193
  replace_row_column(row, :options) { |s| {"type" => "Array", "value" => s.split(',').map(&:strip)} }
@@ -250,7 +249,7 @@ module Etna
250
249
  parent_att.name = parent_att.attribute_name = parent_model_name
251
250
  parent_att.attribute_type = Etna::Clients::Magma::AttributeType::PARENT
252
251
  parent_att.link_model_name = parent_model_name
253
- parent_att.desc = prettify(parent_model_name)
252
+ parent_att.description = prettify(parent_model_name)
254
253
  parent_att.display_name = prettify(parent_model_name)
255
254
  end
256
255
  end
@@ -275,7 +274,7 @@ module Etna
275
274
  attr.attribute_name = attr.name = template.name
276
275
  attr.attribute_type = parent_link_type
277
276
  attr.link_model_name = template.name
278
- attr.desc = prettify(template.name)
277
+ attr.description = prettify(template.name)
279
278
  attr.display_name = prettify(template.name)
280
279
  end
281
280
  end
@@ -338,7 +337,7 @@ module Etna
338
337
  models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
339
338
  rec_att.attribute_name = rec_att.name = template.name
340
339
  rec_att.display_name = prettify(template.name)
341
- rec_att.desc = prettify(template.name)
340
+ rec_att.description = prettify(template.name)
342
341
  rec_att.attribute_type = Etna::Clients::Magma::AttributeType::COLLECTION
343
342
  rec_att.link_model_name = template.name
344
343
  end
@@ -246,7 +246,7 @@ module Etna
246
246
  }
247
247
 
248
248
  params['redcap:TextValidationType'] = redcap_text_validation_map[attribute_type] if redcap_text_validation_map[attribute_type]
249
- params['redcap:FieldNote'] = attribute.desc if attribute.desc
249
+ params['redcap:FieldNote'] = attribute.description if attribute.description
250
250
  xml.ItemDef(params) do
251
251
  xml.Question do
252
252
  xml.send('TranslatedText', attribute_name.capitalize)
@@ -10,7 +10,7 @@ require_relative '../base_client'
10
10
  module Etna
11
11
  module Clients
12
12
  class Magma < Etna::Clients::BaseClient
13
- class RetrievalRequest < Struct.new(:model_name, :attribute_names, :record_names, :project_name, :page, :page_size, :order, :filter, keyword_init: true)
13
+ class RetrievalRequest < Struct.new(:model_name, :attribute_names, :record_names, :project_name, :page, :page_size, :order, :filter, :hide_templates, keyword_init: true)
14
14
  include JsonSerializableStruct
15
15
 
16
16
  def initialize(**params)
@@ -18,7 +18,7 @@ module Etna
18
18
  end
19
19
  end
20
20
 
21
- class QueryRequest < Struct.new(:query, :project_name, keyword_init: true)
21
+ class QueryRequest < Struct.new(:query, :project_name, :order, :page, :page_size, keyword_init: true)
22
22
  include JsonSerializableStruct
23
23
  end
24
24
 
@@ -117,14 +117,6 @@ module Etna
117
117
  super({action_name: 'update_attribute'}.update(args))
118
118
  end
119
119
 
120
- def desc=(val)
121
- self.description = val
122
- end
123
-
124
- def desc
125
- self.description
126
- end
127
-
128
120
  def as_json
129
121
  super(keep_nils: true)
130
122
  end
@@ -454,6 +446,16 @@ module Etna
454
446
  @raw = raw
455
447
  end
456
448
 
449
+ def is_edited?(other)
450
+ # Don't just override == in case need to do a full comparison.
451
+ editable_attribute_names = Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s)
452
+
453
+ self_editable = raw.slice(*editable_attribute_names)
454
+ other_editable = other.raw.slice(*editable_attribute_names)
455
+
456
+ self_editable != other_editable
457
+ end
458
+
457
459
  # Sets certain attribute fields which are implicit, even when not set, to match server behavior.
458
460
  def set_field_defaults!
459
461
  @raw.replace({
@@ -511,24 +513,12 @@ module Etna
511
513
  raw['unique'] = val
512
514
  end
513
515
 
514
- def desc
515
- raw['desc']
516
- end
517
-
518
- def desc=(val)
519
- @raw['desc'] = val
520
- end
521
-
522
- # description and description= are needed
523
- # to make UpdateAttribute actions
524
- # work in the model_synchronization_workflow for
525
- # desc.
526
516
  def description
527
- raw['desc']
517
+ raw['description']
528
518
  end
529
519
 
530
520
  def description=(val)
531
- @raw['desc'] = val
521
+ @raw['description'] = val
532
522
  end
533
523
 
534
524
  def display_name
@@ -595,11 +585,8 @@ module Etna
595
585
  raw['options']
596
586
  end
597
587
 
598
- # NOTE! The Attribute class returns description as desc, where as actions take it in as description.
599
- # There are shortcut methods that try to handle this on the action class side of things. Ideally we would
600
- # make this more consistent in the near future.
601
588
  COPYABLE_ATTRIBUTE_ATTRIBUTES = [
602
- :attribute_name, :attribute_type, :desc, :display_name, :format_hint,
589
+ :attribute_name, :attribute_type, :display_name, :format_hint,
603
590
  :hidden, :link_model_name, :read_only, :attribute_group, :unique, :validation,
604
591
  :restricted, :description
605
592
  ]
@@ -95,7 +95,9 @@ module Etna
95
95
  file_path = ::File.dirname(file_path)
96
96
  {attribute_name => "https://metis.ucsf.edu/#{project_name}/browse/#{bucket_name}/#{file_path}"}
97
97
  else
98
- {attribute_name => {path: "metis://#{project_name}/#{bucket_name}/#{file_path}"}}
98
+ {attribute_name => {
99
+ path: "metis://#{project_name}/#{bucket_name}/#{file_path}",
100
+ original_filename: File.basename(file_path)}}
99
101
  end
100
102
  end
101
103
 
@@ -190,10 +190,7 @@ module Etna
190
190
  source_attribute = Attribute.new(source_attribute.raw)
191
191
  source_attribute.set_field_defaults!
192
192
 
193
- source_editable = source_attribute.raw.slice(*Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s))
194
- target_editable = target_attribute.raw.slice(*Attribute::EDITABLE_ATTRIBUTE_ATTRIBUTES.map(&:to_s))
195
-
196
- if source_editable == target_editable
193
+ if !source_attribute.is_edited?(target_attribute)
197
194
  return
198
195
  end
199
196
 
@@ -12,14 +12,37 @@ module Etna
12
12
  @template_for = {}
13
13
  end
14
14
 
15
+ def all_attributes(template:)
16
+ template.attributes.attribute_keys
17
+ end
18
+
19
+ def safe_group_attributes(template:, attributes_mask:)
20
+ result = []
21
+
22
+ template.attributes.attribute_keys.each do |attribute_name|
23
+ next if template.attributes.attribute(attribute_name).attribute_type == Etna::Clients::Magma::AttributeType::TABLE
24
+ result << attribute_name if attribute_included?(attributes_mask, attribute_name)
25
+ end
26
+
27
+ result
28
+ end
29
+
15
30
  def masked_attributes(template:, model_attributes_mask:, model_name:)
16
31
  attributes_mask = model_attributes_mask[model_name]
17
- return ["all", "all"] if attributes_mask.nil?
18
- [(attributes_mask + [template.identifier, 'parent']).uniq, attributes_mask]
32
+
33
+ if attributes_mask.nil?
34
+ return [
35
+ safe_group_attributes(template: template, attributes_mask: attributes_mask),
36
+ all_attributes(template: template)
37
+ ]
38
+ end
39
+
40
+ [(safe_group_attributes(template: template, attributes_mask: attributes_mask) + [template.identifier, 'parent']).uniq,
41
+ attributes_mask]
19
42
  end
20
43
 
21
44
  def attribute_included?(mask, attribute_name)
22
- return true if mask == "all"
45
+ return true if mask.nil?
23
46
  mask.include?(attribute_name)
24
47
  end
25
48
 
@@ -48,7 +71,11 @@ module Etna
48
71
  seen.add([path[:from], model_name])
49
72
 
50
73
  template = template_for(model_name)
51
- query_attributes, walk_attributes = masked_attributes(template: template, model_attributes_mask: model_attributes_mask, model_name: model_name)
74
+ query_attributes, walk_attributes = masked_attributes(
75
+ template: template,
76
+ model_attributes_mask: model_attributes_mask,
77
+ model_name: model_name
78
+ )
52
79
 
53
80
  request = RetrievalRequest.new(
54
81
  project_name: magma_crud.project_name,
@@ -62,6 +89,7 @@ module Etna
62
89
  related_models = {}
63
90
 
64
91
  magma_crud.page_records(model_name, request) do |response|
92
+ logger&.info("Fetched page of #{model_name}")
65
93
  tables = []
66
94
  collections = []
67
95
  links = []
@@ -70,13 +98,15 @@ module Etna
70
98
  model = response.models.model(model_name)
71
99
 
72
100
  template.attributes.attribute_keys.each do |attr_name|
101
+ attr = template.attributes.attribute(attr_name)
102
+ if attr.attribute_type == AttributeType::TABLE && attribute_included?(walk_attributes, attr_name)
103
+ tables << attr_name
104
+ end
105
+
73
106
  next unless attribute_included?(query_attributes, attr_name)
74
107
  attributes << attr_name
75
108
 
76
- attr = template.attributes.attribute(attr_name)
77
- if attr.attribute_type == AttributeType::TABLE
78
- tables << attr_name
79
- elsif attr.attribute_type == AttributeType::COLLECTION
109
+ if attr.attribute_type == AttributeType::COLLECTION
80
110
  related_models[attr.link_model_name] ||= Set.new
81
111
  collections << attr_name
82
112
  elsif attr.attribute_type == AttributeType::LINK
@@ -91,14 +121,32 @@ module Etna
91
121
  end
92
122
  end
93
123
 
124
+ table_data = {}
125
+ # Request tables in an inner chunk.
126
+ tables.each do |table_attr|
127
+ request = RetrievalRequest.new(
128
+ project_name: magma_crud.project_name,
129
+ model_name: model_name,
130
+ record_names: model.documents.document_keys,
131
+ attribute_names: [table_attr],
132
+ )
133
+
134
+ logger&.info("Fetching inner table #{table_attr}...")
135
+
136
+ table_response = magma_crud.magma_client.retrieve(request)
137
+ d = table_response.models.model(model_name).documents
138
+ table_d = table_response.models.model(table_attr).documents
139
+ table_data[table_attr] = d.document_keys.map do |id|
140
+ [id, d.document(id)[table_attr].map { |tid| table_d.document(tid) }]
141
+ end.to_h
142
+ end
143
+
94
144
  model.documents.document_keys.each do |key|
95
145
  record = model.documents.document(key).slice(*attributes)
96
146
 
97
147
  # Inline tables inside the record
98
148
  tables.each do |table_attr|
99
- record[table_attr] = record[table_attr].map do |id|
100
- response.models.model(template.attributes.attribute(table_attr).link_model_name).documents.document(id)
101
- end unless record[table_attr].nil?
149
+ record[table_attr] = table_data[table_attr][key] unless table_data[table_attr].nil?
102
150
  end
103
151
 
104
152
  collections.each do |collection_attr|
data/lib/etna/command.rb CHANGED
@@ -203,6 +203,7 @@ module Etna
203
203
  @subcommands ||= self.class.constants.sort.reduce({}) do |acc, n|
204
204
  acc.tap do
205
205
  c = self.class.const_get(n)
206
+ next unless c.respond_to?(:instance_methods)
206
207
  next unless c.instance_methods.include?(:find_command)
207
208
  v = c.new(self)
208
209
  acc[v.command_name] = v
@@ -7,6 +7,64 @@ class DirectedGraph
7
7
  attr_reader :children
8
8
  attr_reader :parents
9
9
 
10
+ def full_parentage(n)
11
+ [].tap do |result|
12
+ q = @parents[n].keys.dup
13
+ seen = Set.new
14
+
15
+ until q.empty?
16
+ n = q.shift
17
+ next if seen.include?(n)
18
+ seen.add(n)
19
+
20
+ result << n
21
+ q.push(*@parents[n].keys)
22
+ end
23
+
24
+ result.uniq!
25
+ end
26
+ end
27
+
28
+ def as_normalized_hash(root, include_root = true)
29
+ q = [root]
30
+ {}.tap do |result|
31
+ if include_root
32
+ result[root] = []
33
+ end
34
+
35
+ seen = Set.new
36
+
37
+ until q.empty?
38
+ n = q.shift
39
+ next if seen.include?(n)
40
+ seen.add(n)
41
+
42
+ parentage = full_parentage(n)
43
+
44
+ @children[n].keys.each do |child_node|
45
+ q << child_node
46
+
47
+ if result.include?(n)
48
+ result[n] << child_node
49
+ end
50
+
51
+ parentage.each do |grandparent|
52
+ result[grandparent] << child_node if result.include?(grandparent)
53
+ end
54
+
55
+ # Depending on the graph shape, diamonds could lead to
56
+ # resetting of previously calculated dependencies.
57
+ # Here we avoid resetting existing entries in `result`
58
+ # and instead concatenate them if they already exist.
59
+ result[child_node] = [] unless result.include?(child_node)
60
+ result[n].concat(result[child_node]) if result.include?(child_node) && result.include?(n)
61
+ end
62
+ end
63
+
64
+ result.values.each(&:uniq!)
65
+ end
66
+ end
67
+
10
68
  def add_connection(parent, child)
11
69
  children = @children[parent] ||= {}
12
70
  child_children = @children[child] ||= {}
@@ -18,12 +76,13 @@ class DirectedGraph
18
76
  parents[parent] = parent_parents
19
77
  end
20
78
 
21
- def serialized_path_from(root)
79
+ def serialized_path_from(root, include_root = true)
22
80
  seen = Set.new
23
81
  [].tap do |result|
24
- result << root
82
+ result << root if include_root
25
83
  seen.add(root)
26
- path_q = paths_from(root)
84
+ path_q = paths_from(root, include_root)
85
+ traversables = path_q.flatten
27
86
 
28
87
  until path_q.empty?
29
88
  next_path = path_q.shift
@@ -34,7 +93,7 @@ class DirectedGraph
34
93
  next if next_n.nil?
35
94
  next if seen.include?(next_n)
36
95
 
37
- if @parents[next_n].keys.any? { |p| !seen.include?(p) }
96
+ if @parents[next_n].keys.any? { |p| !seen.include?(p) && traversables.include?(p) }
38
97
  next_path.unshift(next_n)
39
98
  path_q.push(next_path)
40
99
  break
@@ -47,9 +106,9 @@ class DirectedGraph
47
106
  end
48
107
  end
49
108
 
50
- def paths_from(root)
109
+ def paths_from(root, include_root = true)
51
110
  [].tap do |result|
52
- parents_of_map = descendants(root)
111
+ parents_of_map = descendants(root, include_root)
53
112
  seen = Set.new
54
113
 
55
114
  parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k.inspect] }.each do |k, parents|
@@ -68,7 +127,7 @@ class DirectedGraph
68
127
  end
69
128
  end
70
129
 
71
- def descendants(parent)
130
+ def descendants(parent, include_root = true)
72
131
  seen = Set.new
73
132
 
74
133
  seen.add(parent)
@@ -90,7 +149,7 @@ class DirectedGraph
90
149
  while child = queue.pop
91
150
  next if seen.include? child
92
151
  seen.add(child)
93
- path = (paths[child] ||= [parent])
152
+ path = (paths[child] ||= (include_root ? [parent] : []))
94
153
 
95
154
  @children[child].keys.each do |child_child|
96
155
  queue.push child_child
@@ -0,0 +1,26 @@
1
+
2
+ module Etna
3
+ class MetricsExporter
4
+ def initialize(app, path: '/metrics')
5
+ @app = app
6
+ @path = path
7
+ end
8
+
9
+ def exporter
10
+ @exporter ||= begin
11
+ exporter = Yabeda::Prometheus::Exporter.new(@app, path: @path)
12
+ Rack::Auth::Basic.new(exporter) do |user, pw|
13
+ user == 'prometheus' && pw == ENV['METRICS_PW']
14
+ end
15
+ end
16
+ end
17
+
18
+ def call(env)
19
+ if env['PATH_INFO'] == @path
20
+ exporter.call(env)
21
+ else
22
+ @app.call(env)
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/etna/route.rb CHANGED
@@ -91,6 +91,10 @@ module Etna
91
91
  @auth && @auth[:noauth]
92
92
  end
93
93
 
94
+ def ignore_janus?
95
+ @auth && @auth[:ignore_janus]
96
+ end
97
+
94
98
  private
95
99
 
96
100
  def application
@@ -144,7 +148,10 @@ module Etna
144
148
  params = request.env['rack.request.params']
145
149
 
146
150
  @auth[:user].all? do |constraint, param_name|
147
- user.respond_to?(constraint) && user.send(constraint, params[param_name])
151
+ user.respond_to?(constraint) && (
152
+ param_name.is_a?(Symbol) ?
153
+ user.send(constraint, params[param_name]) :
154
+ user.send(constraint, param_name))
148
155
  end
149
156
  end
150
157
 
data/lib/etna/server.rb CHANGED
@@ -74,6 +74,11 @@ module Etna
74
74
  def initialize
75
75
  # Setup logging.
76
76
  application.setup_logger
77
+
78
+ # This needs to be required before yabeda invocation, but cannot belong at the top of any module since clients
79
+ # do not install yabeda.
80
+ require 'yabeda'
81
+ application.setup_yabeda
77
82
  end
78
83
 
79
84
  private
data/lib/etna/spec/vcr.rb CHANGED
@@ -4,13 +4,32 @@ require 'openssl'
4
4
  require 'digest/sha2'
5
5
  require 'base64'
6
6
 
7
- def setup_base_vcr(spec_helper_dir)
7
+ def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
8
8
  VCR.configure do |c|
9
9
  c.hook_into :webmock
10
10
  c.cassette_serializers
11
11
  c.cassette_library_dir = ::File.join(spec_helper_dir, 'fixtures', 'cassettes')
12
12
  c.allow_http_connections_when_no_cassette = true
13
13
 
14
+ c.register_request_matcher :verify_uri_route do |request_1, request_2|
15
+ next true if server.nil? || application.nil?
16
+
17
+ route_match = request_1.uri =~ /https:\/\/#{application.dev_route}(.*)/
18
+ if route_match && route_match[1]
19
+ def request_1.request_method
20
+ method.to_s.upcase
21
+ end
22
+
23
+ def request_1.path
24
+ URI(uri).path
25
+ end
26
+
27
+ !!server.find_route(request_1)
28
+ else
29
+ true
30
+ end
31
+ end
32
+
14
33
  c.register_request_matcher :try_body do |request_1, request_2|
15
34
  if request_1.headers['Content-Type'].first =~ /application\/json/
16
35
  if request_2.headers['Content-Type'].first =~ /application\/json/
@@ -39,7 +58,7 @@ def setup_base_vcr(spec_helper_dir)
39
58
  else
40
59
  ENV['RERECORD'] ? :all : :once
41
60
  end,
42
- match_requests_on: [:method, :uri, :try_body]
61
+ match_requests_on: [:method, :uri, :try_body, :verify_uri_route]
43
62
  }
44
63
 
45
64
  # Filter the authorization headers of any request by replacing any occurrence of that request's
@@ -42,7 +42,6 @@ module Etna
42
42
  # We do this to support Metis client tests, we pass in tokens with multiple "."-separated parts, so
43
43
  # have to account for that.
44
44
  payload = JSON.parse(Base64.decode64(token.split('.')[1]))
45
-
46
45
  request.env['etna.user'] = Etna::User.new(payload.map{|k,v| [k.to_sym, v]}.to_h, token)
47
46
  end
48
47
 
data/lib/etna/user.rb CHANGED
@@ -7,7 +7,7 @@ module Etna
7
7
  }
8
8
 
9
9
  def initialize params, token=nil
10
- @name, @email, @encoded_permissions, encoded_flags = params.values_at(:name, :email, :perm, :flags)
10
+ @name, @email, @encoded_permissions, encoded_flags, @task = params.values_at(:name, :email, :perm, :flags, :task)
11
11
 
12
12
  @flags = encoded_flags&.split(/;/) || []
13
13
  @token = token unless !token
@@ -16,6 +16,10 @@ module Etna
16
16
 
17
17
  attr_reader :name, :email, :token
18
18
 
19
+ def task?
20
+ !!@task
21
+ end
22
+
19
23
  def permissions
20
24
  @permissions ||= @encoded_permissions.split(/\;/).map do |roles|
21
25
  role, projects = roles.split(/:/)
data/lib/helpers.rb CHANGED
@@ -57,9 +57,9 @@ module WithEtnaClients
57
57
  **EtnaApp.instance.config(:metis, environment) || {})
58
58
  end
59
59
 
60
- def janus_client
60
+ def janus_client(opts={})
61
61
  @janus_client ||= Etna::Clients::Janus.new(
62
- token: token,
62
+ token: opts.has_key?(:token) ? opts[:token] : token,
63
63
  ignore_ssl: EtnaApp.instance.config(:ignore_ssl),
64
64
  **EtnaApp.instance.config(:janus, environment) || {})
65
65
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etna
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.32
4
+ version: 0.1.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saurabh Asthana
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-05 00:00:00.000000000 Z
11
+ date: 2021-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -94,20 +94,6 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: concurrent-ruby-ext
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :runtime
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
97
  description: See summary
112
98
  email: Saurabh.Asthana@ucsf.edu
113
99
  executables:
@@ -131,6 +117,8 @@ files:
131
117
  - lib/etna/clients/janus.rb
132
118
  - lib/etna/clients/janus/client.rb
133
119
  - lib/etna/clients/janus/models.rb
120
+ - lib/etna/clients/janus/workflows.rb
121
+ - lib/etna/clients/janus/workflows/generate_token_workflow.rb
134
122
  - lib/etna/clients/magma.rb
135
123
  - lib/etna/clients/magma/client.rb
136
124
  - lib/etna/clients/magma/formatting.rb
@@ -180,6 +168,7 @@ files:
180
168
  - lib/etna/hmac.rb
181
169
  - lib/etna/json_serializable_struct.rb
182
170
  - lib/etna/logger.rb
171
+ - lib/etna/metrics.rb
183
172
  - lib/etna/multipart_serializable_nested_hash.rb
184
173
  - lib/etna/parse_body.rb
185
174
  - lib/etna/route.rb
@@ -213,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
202
  version: '0'
214
203
  requirements: []
215
204
  rubyforge_project:
216
- rubygems_version: 2.7.6.2
205
+ rubygems_version: 2.7.6.3
217
206
  signing_key:
218
207
  specification_version: 4
219
208
  summary: Base classes for Mount Etna applications