etna 0.1.30 → 0.1.35

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: 9118a99396cf2b041083ff982fb680a5d60096802699789962d19bcf3fecf41d
4
- data.tar.gz: 3145994427f0fa57bf796e68dfe4e69156eb407f144a6afe048bd17454201949
3
+ metadata.gz: bf1f86e15c74113a4591afd3d8a43064b6ed3b448bdfad3739baa27061df7913
4
+ data.tar.gz: ed543c8775c89679b4755ca3119899639fc125df2d7e65c3515998f06ae45de8
5
5
  SHA512:
6
- metadata.gz: ec59fd21bcf8bb88848e0c8c628a0185226d7dd7ee1af79525a790ac2117fe2b7d44b9d3e514717e9c23233bd531c9d3c8404981892174adbeee221e62118c33
7
- data.tar.gz: bf0cc8cea804bb218e7f487ea014c84e394738760dc400de1c716b780da6a668ca5f853a72acfff7a77cd979f4a90004a866cdd16cb26a008a3498c83161fd71
6
+ metadata.gz: 8523211434696c74c03ca5319a089c74dba9bc3c3cc086f723dcadd1789c91f2e6c7fe43701cd3ac467460bc5a8ee1f4c82c032a5d0ac16e666a565212fba209
7
+ data.tar.gz: 60e3338c3021ea2e1a54dcfb7f2a15c9ffce71aed5a12db7739992649cfd9518f19edc49262a0095ba09c8783e0ef499bcb3015aebca0d2b7751e7096cbf88eb
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/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)
@@ -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,
@@ -36,4 +35,4 @@ module Etna
36
35
  end
37
36
  end
38
37
  end
39
- end
38
+ 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,25 @@ module Etna
57
57
 
58
58
  TokenResponse.new(token)
59
59
  end
60
+
61
+ def validate_task_token(validate_task_token_request = ValidateTaskTokenRequest.new)
62
+ token = nil
63
+ @etna_client.post('/api/tokens/task/validate', validate_task_token_request)
64
+ end
65
+
66
+ def get_nonce
67
+ @etna_client.get('/api/tokens/nonce').body
68
+ end
69
+
70
+ def generate_token(token_type, signed_nonce: nil, project_name: nil)
71
+ response = @etna_client.with_headers(
72
+ 'Authorization' => signed_nonce ? "Signed-Nonce #{signed_nonce}" : nil
73
+ ) do
74
+ post('/api/tokens/generate', token_type: token_type, project_name: project_name)
75
+ end
76
+
77
+ response.body
78
+ end
60
79
  end
61
80
  end
62
81
  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,12 +513,12 @@ module Etna
511
513
  raw['unique'] = val
512
514
  end
513
515
 
514
- def desc
515
- raw['desc']
516
+ def description
517
+ raw['description']
516
518
  end
517
519
 
518
- def desc=(val)
519
- @raw['desc'] = val
520
+ def description=(val)
521
+ @raw['description'] = val
520
522
  end
521
523
 
522
524
  def display_name
@@ -583,13 +585,10 @@ module Etna
583
585
  raw['options']
584
586
  end
585
587
 
586
- # NOTE! The Attribute class returns description as desc, where as actions take it in as description.
587
- # There are shortcut methods that try to handle this on the action class side of things. Ideally we would
588
- # make this more consistent in the near future.
589
588
  COPYABLE_ATTRIBUTE_ATTRIBUTES = [
590
- :attribute_name, :attribute_type, :desc, :display_name, :format_hint,
589
+ :attribute_name, :attribute_type, :display_name, :format_hint,
591
590
  :hidden, :link_model_name, :read_only, :attribute_group, :unique, :validation,
592
- :restricted
591
+ :restricted, :description
593
592
  ]
594
593
 
595
594
  EDITABLE_ATTRIBUTE_ATTRIBUTES = UpdateAttributeAction.members & COPYABLE_ATTRIBUTE_ATTRIBUTES
@@ -48,8 +48,16 @@ module Etna
48
48
 
49
49
  retry
50
50
  rescue Etna::Error => e
51
- raise e unless e.message.include?('not found')
52
- break
51
+ if e.status === 502
52
+ if attempts > 5
53
+ raise e
54
+ end
55
+
56
+ retry
57
+ else
58
+ raise e unless e.message.include?('not found')
59
+ break
60
+ end
53
61
  end
54
62
 
55
63
  documents += last_page.models.model(model_name).documents unless block_given?
@@ -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
 
@@ -34,7 +34,7 @@ module Etna
34
34
  record_names,
35
35
  model_attributes_mask: model_attributes_mask,
36
36
  model_filters: model_filters,
37
- page_size: 500,
37
+ page_size: 20,
38
38
  ) do |template, document|
39
39
  logger&.info("Materializing #{template.name}##{document[template.identifier]}")
40
40
  templates[template.name] = template
@@ -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
 
@@ -226,7 +223,7 @@ module Etna
226
223
  if renames && (attribute_renames = renames[model_name]) && (new_name = attribute_renames[attribute_name])
227
224
  new_name = target_attribute_of_source(model_name, new_name)
228
225
 
229
- unless target_model.template.attributes.include?(new_name)
226
+ unless target_model.template.attributes.attribute_keys.include?(new_name)
230
227
  if target_original_attribute
231
228
  rename = RenameAttributeAction.new(model_name: target_model_name, attribute_name: target_attribute_name, new_attribute_name: new_name)
232
229
  queue_update(rename)
@@ -241,7 +241,13 @@ module Etna
241
241
  attribute_name_clean = attribute_name.strip
242
242
  raise "Invalid attribute \"#{attribute_name_clean}\" for model #{@model_name}." unless attribute = @workflow.find_attribute(@model_name, attribute_name_clean)
243
243
 
244
- attributes[attribute_name_clean] = stripped_value(attribute, @raw[attribute_name])
244
+ stripped = stripped_value(attribute, @raw[attribute_name])
245
+
246
+ unless @workflow.hole_value.nil?
247
+ next if stripped == @workflow.hole_value
248
+ end
249
+
250
+ attributes[attribute_name_clean] = stripped
245
251
  end
246
252
  end
247
253
  end
@@ -9,6 +9,50 @@ module Etna
9
9
  class WalkModelTreeWorkflow < Struct.new(:magma_crud, :logger, keyword_init: true)
10
10
  def initialize(**args)
11
11
  super(**({}.update(args)))
12
+ @template_for = {}
13
+ end
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
+
30
+ def masked_attributes(template:, model_attributes_mask:, model_name:)
31
+ attributes_mask = model_attributes_mask[model_name]
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]
42
+ end
43
+
44
+ def attribute_included?(mask, attribute_name)
45
+ return true if mask.nil?
46
+ mask.include?(attribute_name)
47
+ end
48
+
49
+ def template_for(model_name)
50
+ @template_for[model_name] ||= magma_crud.magma_client.retrieve(RetrievalRequest.new(
51
+ project_name: magma_crud.project_name,
52
+ model_name: model_name,
53
+ record_names: [],
54
+ attribute_names: [],
55
+ )).models.model(model_name).template
12
56
  end
13
57
 
14
58
  def walk_from(
@@ -26,35 +70,43 @@ module Etna
26
70
  next if seen.include?([path[:from], model_name])
27
71
  seen.add([path[:from], model_name])
28
72
 
73
+ template = template_for(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
+ )
79
+
29
80
  request = RetrievalRequest.new(
30
81
  project_name: magma_crud.project_name,
31
82
  model_name: model_name,
32
83
  record_names: path[:record_names],
33
84
  filter: model_filters[model_name],
85
+ attribute_names: query_attributes,
34
86
  page_size: page_size, page: 1
35
87
  )
36
88
 
37
89
  related_models = {}
38
90
 
39
91
  magma_crud.page_records(model_name, request) do |response|
40
- model = response.models.model(model_name)
41
- template = model.template
42
-
92
+ logger&.info("Fetched page of #{model_name}")
43
93
  tables = []
44
94
  collections = []
45
95
  links = []
46
96
  attributes = []
47
97
 
48
- template.attributes.attribute_keys.each do |attr_name|
49
- attributes_mask = model_attributes_mask[model_name]
50
- black_listed = !attributes_mask.nil? && !attributes_mask.include?(attr_name)
51
- next if black_listed && attr_name != template.identifier && attr_name != 'parent'
52
- attributes << attr_name
98
+ model = response.models.model(model_name)
53
99
 
100
+ template.attributes.attribute_keys.each do |attr_name|
54
101
  attr = template.attributes.attribute(attr_name)
55
- if attr.attribute_type == AttributeType::TABLE
102
+ if attr.attribute_type == AttributeType::TABLE && attribute_included?(walk_attributes, attr_name)
56
103
  tables << attr_name
57
- elsif attr.attribute_type == AttributeType::COLLECTION
104
+ end
105
+
106
+ next unless attribute_included?(query_attributes, attr_name)
107
+ attributes << attr_name
108
+
109
+ if attr.attribute_type == AttributeType::COLLECTION
58
110
  related_models[attr.link_model_name] ||= Set.new
59
111
  collections << attr_name
60
112
  elsif attr.attribute_type == AttributeType::LINK
@@ -63,20 +115,38 @@ module Etna
63
115
  elsif attr.attribute_type == AttributeType::CHILD
64
116
  related_models[attr.link_model_name] ||= Set.new
65
117
  links << attr_name
66
- elsif attr.attribute_type == AttributeType::PARENT && !black_listed
118
+ elsif attr.attribute_type == AttributeType::PARENT && attribute_included?(walk_attributes, attr_name)
67
119
  related_models[attr.link_model_name] ||= Set.new
68
120
  links << attr_name
69
121
  end
70
122
  end
71
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
+
72
144
  model.documents.document_keys.each do |key|
73
145
  record = model.documents.document(key).slice(*attributes)
74
146
 
75
147
  # Inline tables inside the record
76
148
  tables.each do |table_attr|
77
- record[table_attr] = record[table_attr].map do |id|
78
- response.models.model(template.attributes.attribute(table_attr).link_model_name).documents.document(id)
79
- end unless record[table_attr].nil?
149
+ record[table_attr] = table_data[table_attr][key] unless table_data[table_attr].nil?
80
150
  end
81
151
 
82
152
  collections.each do |collection_attr|
@@ -69,6 +69,11 @@ module Etna
69
69
  @etna_client.folder_remove(delete_folder_request.to_h))
70
70
  end
71
71
 
72
+ def delete_file(delete_file_request)
73
+ FilesResponse.new(
74
+ @etna_client.file_remove(delete_file_request.to_h))
75
+ end
76
+
72
77
  def find(find_request)
73
78
  FoldersAndFilesResponse.new(
74
79
  @etna_client.bucket_find(find_request.to_h))
@@ -95,6 +95,21 @@ module Etna
95
95
  end
96
96
  end
97
97
 
98
+ class DeleteFileRequest < Struct.new(:project_name, :bucket_name, :file_path, keyword_init: true)
99
+ include JsonSerializableStruct
100
+
101
+ def initialize(**params)
102
+ super({}.update(params))
103
+ end
104
+
105
+ def to_h
106
+ # The :project_name comes in from Polyphemus as a symbol value,
107
+ # we need to make sure it's a string because it's going
108
+ # in the URL.
109
+ super().compact.transform_values(&:to_s)
110
+ end
111
+ end
112
+
98
113
  class FindRequest < Struct.new(:project_name, :bucket_name, :limit, :offset, :params, keyword_init: true)
99
114
  include JsonSerializableStruct
100
115
 
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,59 @@ 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
+ result[child_node] = []
56
+ end
57
+ end
58
+
59
+ result.values.each(&:uniq!)
60
+ end
61
+ end
62
+
10
63
  def add_connection(parent, child)
11
64
  children = @children[parent] ||= {}
12
65
  child_children = @children[child] ||= {}
@@ -18,12 +71,13 @@ class DirectedGraph
18
71
  parents[parent] = parent_parents
19
72
  end
20
73
 
21
- def serialized_path_from(root)
74
+ def serialized_path_from(root, include_root = true)
22
75
  seen = Set.new
23
76
  [].tap do |result|
24
- result << root
77
+ result << root if include_root
25
78
  seen.add(root)
26
- path_q = paths_from(root)
79
+ path_q = paths_from(root, include_root)
80
+ traversables = path_q.flatten
27
81
 
28
82
  until path_q.empty?
29
83
  next_path = path_q.shift
@@ -34,7 +88,7 @@ class DirectedGraph
34
88
  next if next_n.nil?
35
89
  next if seen.include?(next_n)
36
90
 
37
- if @parents[next_n].keys.any? { |p| !seen.include?(p) }
91
+ if @parents[next_n].keys.any? { |p| !seen.include?(p) && traversables.include?(p) }
38
92
  next_path.unshift(next_n)
39
93
  path_q.push(next_path)
40
94
  break
@@ -47,9 +101,9 @@ class DirectedGraph
47
101
  end
48
102
  end
49
103
 
50
- def paths_from(root)
104
+ def paths_from(root, include_root = true)
51
105
  [].tap do |result|
52
- parents_of_map = descendants(root)
106
+ parents_of_map = descendants(root, include_root)
53
107
  seen = Set.new
54
108
 
55
109
  parents_of_map.to_a.sort_by { |k, parents| [-parents.length, k.inspect] }.each do |k, parents|
@@ -68,7 +122,7 @@ class DirectedGraph
68
122
  end
69
123
  end
70
124
 
71
- def descendants(parent)
125
+ def descendants(parent, include_root = true)
72
126
  seen = Set.new
73
127
 
74
128
  seen.add(parent)
@@ -90,7 +144,7 @@ class DirectedGraph
90
144
  while child = queue.pop
91
145
  next if seen.include? child
92
146
  seen.add(child)
93
- path = (paths[child] ||= [parent])
147
+ path = (paths[child] ||= (include_root ? [parent] : []))
94
148
 
95
149
  @children[child].keys.each do |child_child|
96
150
  queue.push child_child
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
data/lib/etna/user.rb CHANGED
@@ -46,7 +46,7 @@ module Etna
46
46
  viewer: /[Vv]/,
47
47
  restricted: /[AEV]/,
48
48
  }
49
- def has_roles(project, *roles)
49
+ def has_any_role?(project, *roles)
50
50
  perm = permissions[project.to_s]
51
51
 
52
52
  return false unless perm
@@ -55,15 +55,23 @@ module Etna
55
55
  end
56
56
 
57
57
  def is_superuser? project=nil
58
- has_roles(:administration, :admin)
58
+ has_any_role?(:administration, :admin)
59
+ end
60
+
61
+ def is_supereditor? project=nil
62
+ has_any_role?(:administration, :admin, :editor)
63
+ end
64
+
65
+ def is_superviewer? project=nil
66
+ has_any_role?(:administration, :admin, :editor, :viewer)
59
67
  end
60
68
 
61
69
  def can_edit? project
62
- is_superuser? || has_roles(project, :admin, :editor)
70
+ is_supereditor? || has_any_role?(project, :admin, :editor)
63
71
  end
64
72
 
65
73
  def can_view? project
66
- is_superuser? || has_roles(project, :admin, :editor, :viewer)
74
+ is_superviewer? || has_any_role?(project, :admin, :editor, :viewer)
67
75
  end
68
76
 
69
77
  # superusers - administrators of the Administration group - cannot
@@ -75,7 +83,7 @@ module Etna
75
83
  end
76
84
 
77
85
  def is_admin? project
78
- is_superuser? || has_roles(project, :admin)
86
+ is_superuser? || has_any_role?(project, :admin)
79
87
  end
80
88
 
81
89
  def active? project=nil
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.30
4
+ version: 0.1.35
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-03-03 00:00:00.000000000 Z
11
+ date: 2021-04-26 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
@@ -213,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
213
201
  version: '0'
214
202
  requirements: []
215
203
  rubyforge_project:
216
- rubygems_version: 2.7.6.2
204
+ rubygems_version: 2.7.6.3
217
205
  signing_key:
218
206
  specification_version: 4
219
207
  summary: Base classes for Mount Etna applications