etna 0.1.32 → 0.1.37

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.
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