etna 0.1.42 → 0.1.45

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ module Etna
2
+ module Clients
3
+ class Metis
4
+ class WalkMetisWorkflow < Struct.new(:metis_client, :project_name,
5
+ :bucket_name, :logger, :root_dir, keyword_init: true)
6
+ def each(&block)
7
+ q = [self.root_dir]
8
+
9
+ while (n = q.pop)
10
+ req = Etna::Clients::Metis::ListFolderRequest.new(
11
+ project_name: project_name,
12
+ bucket_name: bucket_name,
13
+ folder_path: n
14
+ )
15
+ next unless metis_client.folder_exists?(req)
16
+ resp = metis_client.list_folder(req)
17
+
18
+ resp.files.all.sort_by { |f| f.file_path[self.root_dir.length..-1] }.each do |file|
19
+ yield [file, file.file_path[self.root_dir.length..-1]]
20
+ end
21
+
22
+ resp.folders.all.sort_by { |f| f.folder_path[self.root_dir.length..-1] }.each do |f|
23
+ yield [f, f.folder_path[self.root_dir.length..-1]]
24
+ q << f.folder_path
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,3 +2,5 @@ require_relative "./workflows/metis_download_workflow"
2
2
  require_relative "./workflows/metis_upload_workflow"
3
3
  require_relative "./workflows/sync_metis_data_workflow"
4
4
  require_relative "./workflows/ingest_metis_data_workflow"
5
+ require_relative "./workflows/walk_metis_workflow"
6
+ require_relative "./workflows/walk_metis_diff_workflow"
@@ -1,4 +1,5 @@
1
1
  require 'erb'
2
+ require 'net/smtp'
2
3
 
3
4
  module Etna
4
5
  class Controller
@@ -44,10 +45,56 @@ module Etna
44
45
  rescue Exception => e
45
46
  error = e
46
47
  ensure
47
- log_request
48
+ log_request if !@request.env['etna.dont_log'] || error
48
49
  return handle_error(error) if error
49
50
  end
50
51
 
52
+ def try_stream(content_type, &block)
53
+ if @request.env['rack.hijack?']
54
+ @request.env['rack.hijack'].call
55
+ stream = @request.env['rack.hijack_io']
56
+
57
+ headers = [
58
+ "HTTP/1.1 200 OK",
59
+ "Content-Type: #{content_type}"
60
+ ]
61
+ stream.write(headers.map { |header| header + "\r\n" }.join)
62
+ stream.write("\r\n")
63
+ stream.flush
64
+
65
+ Thread.new do
66
+ block.call(stream)
67
+ ensure
68
+ stream.close
69
+ end
70
+
71
+ # IO is now streaming and will be processed by above thread.
72
+ @response.close
73
+ else
74
+ @response['Content-Type'] = content_type
75
+ block.call(@response)
76
+ @response.finish
77
+ end
78
+ end
79
+
80
+ def send_email(to_name, to_email, subject, content)
81
+ message = <<MESSAGE_END
82
+ From: Data Library <noreply@janus>
83
+ To: #{to_name} <#{to_email}>
84
+ Subject: #{subject}
85
+
86
+ #{content}
87
+ MESSAGE_END
88
+
89
+ unless @server.send(:application).test?
90
+ Net::SMTP.start('smtp.ucsf.edu') do |smtp|
91
+ smtp.send_message message, 'noreply@janus', to_email
92
+ end
93
+ end
94
+ rescue => e
95
+ @logger.log_error(e)
96
+ end
97
+
51
98
  def require_params(*params)
52
99
  missing_params = params.reject{|p| @params.key?(p) }
53
100
  raise Etna::BadRequest, "Missing param #{missing_params.join(', ')}" unless missing_params.empty?
data/lib/etna/cwl.rb CHANGED
@@ -559,6 +559,14 @@ module Etna
559
559
  def id
560
560
  @attributes['id']
561
561
  end
562
+
563
+ def type
564
+ @attributes['type']
565
+ end
566
+
567
+ def format
568
+ @attributes['format']
569
+ end
562
570
  end
563
571
 
564
572
  class WorkflowInputParameter < Cwl
@@ -0,0 +1,50 @@
1
+ module Etna
2
+ module EnvironmentVariables
3
+ # a <- b
4
+ def self.deep_merge(a, b)
5
+ if a.is_a?(Hash)
6
+ if b.is_a?(Hash)
7
+ b.keys.each do |b_key|
8
+ a[b_key] = deep_merge(a[b_key], b[b_key])
9
+ end
10
+
11
+ return a
12
+ end
13
+ end
14
+
15
+ a.nil? ? b : a
16
+ end
17
+
18
+ def self.load_from_env(prefix, root: {}, env: ENV, downcase: true, sep: '__', &path_to_value_mapper)
19
+ env.keys.each do |key|
20
+ next unless key.start_with?(prefix + sep)
21
+
22
+ path = key.split(sep, -1)
23
+ path.shift
24
+ if downcase
25
+ path.each(&:downcase!)
26
+ end
27
+
28
+ result = path_to_value_mapper.call(path, env[key])
29
+ next unless result
30
+
31
+ path, value = result
32
+
33
+ if path.empty?
34
+ root = EnvironmentVariables.deep_merge(root, value)
35
+ next
36
+ end
37
+
38
+ target = root
39
+ while path.length > 1
40
+ n = path.shift
41
+ target = (target[n] ||= {})
42
+ end
43
+
44
+ target[path.last] = EnvironmentVariables.deep_merge(target[path.last], value)
45
+ end
46
+
47
+ root
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
1
+ module Etna
2
+ class Injection
3
+ # Extend into class
4
+ module FromHash
5
+ def from_hash(hash, hash_has_string_keys, rest: nil, key_rest: nil)
6
+ ::Etna::Injection.inject_new(self, hash ,hash_has_string_keys, rest: rest, key_rest: key_rest) do |missing_p|
7
+ raise "required argument '#{missing_p}' of #{self.name} is missing!"
8
+ end
9
+ end
10
+ end
11
+
12
+ def self.inject_new(cls, hash, hash_has_string_keys, rest: nil, key_rest: nil, &missing_req_param_cb)
13
+ args, kwds = prep_args(
14
+ cls.instance_method(:initialize), hash, hash_has_string_keys,
15
+ rest: rest, key_rest: key_rest, &missing_req_param_cb
16
+ )
17
+
18
+ cls.new(*args, **kwds)
19
+ end
20
+
21
+ def self.prep_args(method, hash, hash_has_string_keys, rest: nil, key_rest: nil, &missing_req_param_cb)
22
+ new_k_params = {}
23
+ new_p_params = []
24
+
25
+ method.parameters.each do |type, p_key|
26
+ h_key = hash_has_string_keys ? p_key.to_s : p_key
27
+ if type == :rest && rest
28
+ new_p_params.append(*rest)
29
+ elsif type == :keyrest && key_rest
30
+ new_k_params.update(key_rest)
31
+ elsif type == :req || type == :opt
32
+ if hash.include?(h_key)
33
+ new_p_params << hash[h_key]
34
+ elsif type == :req
35
+ if block_given?
36
+ new_p_params << missing_req_param_cb.call(h_key)
37
+ else
38
+ new_p_params << nil
39
+ end
40
+ end
41
+ elsif type == :keyreq || type == :key
42
+ if hash.include?(h_key)
43
+ new_k_params[p_key] = hash[h_key]
44
+ elsif type == :keyreq
45
+ if block_given?
46
+ new_k_params[p_key] = missing_req_param_cb.call(h_key)
47
+ else
48
+ new_k_params[p_key] = nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ [new_p_params, new_k_params]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,55 @@
1
+ # Utility class to work with Janus for authz and authn
2
+ module Etna
3
+ class JanusUtils
4
+ def initialize
5
+ end
6
+
7
+ def projects(token)
8
+ return [] unless has_janus_config?
9
+
10
+ janus_client(token).get_projects.projects
11
+ rescue
12
+ # If encounter any issue with Janus, we'll return no projects
13
+ []
14
+ end
15
+
16
+ def resource_projects(token)
17
+ projects(token).select do |project|
18
+ !!project.resource && !project.requires_agreement
19
+ end
20
+ end
21
+
22
+ def community_projects(token)
23
+ projects(token).select do |project|
24
+ !!project.resource && !!project.requires_agreement
25
+ end
26
+ end
27
+
28
+ def janus_client(token)
29
+ Etna::Clients::Janus.new(
30
+ token: token,
31
+ host: application.config(:janus)[:host],
32
+ )
33
+ end
34
+
35
+ def valid_task_token?(token)
36
+ return false unless has_janus_config?
37
+
38
+ response = janus_client(token).validate_task_token
39
+
40
+ return false unless response.code == "200"
41
+
42
+ return true
43
+ end
44
+
45
+ private
46
+
47
+ def application
48
+ @application ||= Etna::Application.instance
49
+ end
50
+
51
+ def has_janus_config?
52
+ application.config(:janus) && application.config(:janus)[:host]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,104 @@
1
+ module Etna
2
+ class Permissions
3
+ def initialize(permissions)
4
+ @permissions = permissions
5
+ end
6
+
7
+ def self.from_encoded_permissions(encoded_permissions)
8
+ perms = encoded_permissions.split(/\;/).map do |roles|
9
+ role, projects = roles.split(/:/)
10
+
11
+ projects.split(/\,/).reduce([]) do |perms, project_name|
12
+ perms << Etna::Permission.new(role, project_name)
13
+ end
14
+ end.flatten
15
+
16
+ Etna::Permissions.new(perms)
17
+ end
18
+
19
+ def self.from_hash(permissions_hash)
20
+ perms = permissions_hash.map do |project_name, role_hash|
21
+ Etna::Permission.new(
22
+ Etna::Role.new(role_hash[:role], role_hash[:restricted]).key,
23
+ project_name
24
+ )
25
+ end
26
+
27
+ Etna::Permissions.new(perms)
28
+ end
29
+
30
+ def to_string
31
+ @permissions_string ||= @permissions.group_by(&:role_key)
32
+ .sort_by(&:first)
33
+ .map do |role_key, permissions|
34
+ [
35
+ role_key,
36
+ permissions.map(&:project_name).sort.join(","),
37
+ ].join(":")
38
+ end.join(";")
39
+ end
40
+
41
+ def to_hash
42
+ @permissions_hash ||= @permissions.map do |permission|
43
+ [permission.project_name, permission.to_hash]
44
+ end.to_h
45
+ end
46
+
47
+ def add_permission(permission)
48
+ return if current_project_names.include?(permission.project_name)
49
+
50
+ @permissions << permission
51
+ end
52
+
53
+ def any?(&block)
54
+ @permissions.any?(&block)
55
+ end
56
+
57
+ private
58
+
59
+ def current_project_names
60
+ @permissions.map(&:project_name)
61
+ end
62
+ end
63
+
64
+ class Permission
65
+ attr_reader :role, :project_name, :role_key
66
+
67
+ ROLE_NAMES = {
68
+ "A" => :admin,
69
+ "E" => :editor,
70
+ "V" => :viewer,
71
+ "G" => :guest
72
+ }
73
+
74
+ def initialize(role_key, project_name)
75
+ @role_key = role_key
76
+ @role = Etna::Role.new(ROLE_NAMES[role_key.upcase], role_key == role_key.upcase)
77
+ @project_name = project_name
78
+ end
79
+
80
+ def to_hash
81
+ role.to_hash
82
+ end
83
+ end
84
+
85
+ class Role
86
+ attr_reader :role, :restricted
87
+ def initialize(role, restricted)
88
+ @role = role
89
+ @restricted = restricted
90
+ end
91
+
92
+ def key
93
+ role_key = role.to_s[0]
94
+ restricted ? role_key.upcase : role_key
95
+ end
96
+
97
+ def to_hash
98
+ {
99
+ role: role,
100
+ restricted: restricted,
101
+ }
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,36 @@
1
+ module Etna
2
+ def self.Redirect(req)
3
+ Redirect.new(req)
4
+ end
5
+
6
+ class Redirect
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def to(path, &block)
12
+ return Rack::Response.new(
13
+ [ { errors: [ 'Cannot redirect out of domain' ] }.to_json ], 422,
14
+ { 'Content-Type' => 'application/json' }
15
+ ).finish unless matches_domain?(path)
16
+
17
+ response = Rack::Response.new
18
+
19
+ response.redirect(path.gsub("http://", "https://"), 302)
20
+
21
+ yield response if block_given?
22
+
23
+ response.finish
24
+ end
25
+
26
+ private
27
+
28
+ def matches_domain?(path)
29
+ top_domain(@request.hostname) == top_domain(URI(path).host)
30
+ end
31
+
32
+ def top_domain(host_name)
33
+ host_name.split('.')[-2..-1].join('.')
34
+ end
35
+ end
36
+ end
data/lib/etna/route.rb CHANGED
@@ -15,6 +15,7 @@ module Etna
15
15
  @block = block
16
16
  @match_ext = options[:match_ext]
17
17
  @log_redact_keys = options[:log_redact_keys]
18
+ @dont_log = options[:dont_log]
18
19
  end
19
20
 
20
21
  def to_hash
@@ -41,7 +42,7 @@ module Etna
41
42
  if params
42
43
  PARAM_TYPES.reduce(route) do |path,pat|
43
44
  path.gsub(pat) do
44
- URI.encode( params[$1.to_sym], UNSAFE)
45
+ params[$1.to_sym].split('/').map { |c| URI.encode_www_form_component(c) }.join('/')
45
46
  end
46
47
  end
47
48
  else
@@ -123,10 +124,19 @@ module Etna
123
124
  update_params(request)
124
125
 
125
126
  unless authorized?(request)
127
+ if cc_available?(request)
128
+ if request.content_type == 'application/json'
129
+ return [ 403, { 'Content-Type' => 'application/json' }, [ { error: 'You are forbidden from performing this action, but you can visit the project home page and request access.' }.to_json ] ]
130
+ else
131
+ return cc_redirect(request)
132
+ end
133
+ end
134
+
126
135
  return [ 403, { 'Content-Type' => 'application/json' }, [ { error: 'You are forbidden from performing this action.' }.to_json ] ]
127
136
  end
128
137
 
129
138
  request.env['etna.redact_keys'] = @log_redact_keys
139
+ request.env['etna.dont_log'] = @dont_log
130
140
 
131
141
  if @action
132
142
  controller, action = @action.split('#')
@@ -154,8 +164,29 @@ module Etna
154
164
  @auth && @auth[:ignore_janus]
155
165
  end
156
166
 
167
+ def has_user_constraint?(constraint)
168
+ @auth && @auth[:user] && @auth[:user][constraint]
169
+ end
170
+
157
171
  private
158
172
 
173
+ def janus
174
+ @janus ||= Etna::JanusUtils.new
175
+ end
176
+
177
+ # If the application asks for a code of conduct redirect
178
+ def cc_redirect(request, msg = 'You are unauthorized')
179
+ return [ 401, { 'Content-Type' => 'text/html' }, [msg] ] unless application.config(:auth_redirect)
180
+
181
+ params = request.env['rack.request.params']
182
+
183
+ uri = URI(
184
+ application.config(:auth_redirect).chomp('/') + "/#{params[:project_name]}/cc"
185
+ )
186
+ uri.query = URI.encode_www_form(refer: request.url)
187
+ Etna::Redirect(request).to(uri.to_s)
188
+ end
189
+
159
190
  def application
160
191
  @application ||= Etna::Application.instance
161
192
  end
@@ -185,6 +216,24 @@ module Etna
185
216
  end
186
217
  end
187
218
 
219
+ def cc_available?(request)
220
+ user = request.env['etna.user']
221
+
222
+ return false unless user
223
+
224
+ params = request.env['rack.request.params']
225
+
226
+ return false unless params[:project_name]
227
+
228
+ # Only check for a CC if the user does not currently have permissions
229
+ # for the project
230
+ return false if user.permissions.keys.include?(params[:project_name])
231
+
232
+ !janus.community_projects(user.token).select do |project|
233
+ project.project_name == params[:project_name]
234
+ end.first.nil?
235
+ end
236
+
188
237
  def hmac_authorized?(request)
189
238
  # either there is no hmac requirement, or we have a valid hmac
190
239
  !@auth[:hmac] || request.env['etna.hmac']&.valid?
@@ -207,7 +256,7 @@ module Etna
207
256
  Hash[
208
257
  match.names.map(&:to_sym).zip(
209
258
  match.captures.map do |capture|
210
- URI.decode(capture)
259
+ capture.split('/').map {|c| URI.decode_www_form_component(c) }.join('/')
211
260
  end
212
261
  )
213
262
  ]
@@ -18,11 +18,22 @@ module Etna::Spec
18
18
  },
19
19
  non_user: {
20
20
  email: 'nessus@centaurs.org', name: 'Nessus', perm: ''
21
+ },
22
+ guest: {
23
+ email: 'sinon@troy.org', name: 'Sinon', perm: 'g:labors'
21
24
  }
22
25
  }
23
26
 
24
27
  def auth_header(user_type)
25
28
  header(*Etna::TestAuth.token_header(AUTH_USERS[user_type]))
26
29
  end
30
+
31
+ def below_admin_roles
32
+ [:editor, :viewer, :guest]
33
+ end
34
+
35
+ def below_editor_roles
36
+ [:viewer, :guest]
37
+ end
27
38
  end
28
39
  end
data/lib/etna/spec/vcr.rb CHANGED
@@ -1,9 +1,27 @@
1
+ require 'cgi'
1
2
  require 'webmock/rspec'
2
3
  require 'vcr'
3
4
  require 'openssl'
4
5
  require 'digest/sha2'
5
6
  require 'base64'
6
7
 
8
+ def clean_query(json_or_string)
9
+ if json_or_string.is_a?(Hash) && json_or_string.include?('upload_path')
10
+ json_or_string['upload_path'] = clean_query(json_or_string['upload_path'])
11
+ json_or_string
12
+ elsif json_or_string.is_a?(String)
13
+ uri = URI(json_or_string)
14
+
15
+ if uri.query&.include?('X-Etna-Signature')
16
+ uri.query = 'etna-signature'
17
+ end
18
+
19
+ uri.to_s
20
+ else
21
+ json_or_string
22
+ end
23
+ end
24
+
7
25
  def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
8
26
  VCR.configure do |c|
9
27
  c.hook_into :webmock
@@ -30,6 +48,10 @@ def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
30
48
  end
31
49
  end
32
50
 
51
+ c.register_request_matcher :try_uri do |request_1, request_2|
52
+ clean_query(request_1.uri) == clean_query(request_2.uri)
53
+ end
54
+
33
55
  c.register_request_matcher :try_body do |request_1, request_2|
34
56
  if request_1.headers['Content-Type'].first =~ /application\/json/
35
57
  if request_2.headers['Content-Type'].first =~ /application\/json/
@@ -40,7 +62,7 @@ def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
40
62
  JSON.parse(request_2.body) rescue 'not-json'
41
63
  end
42
64
 
43
- request_1_json == request_2_json
65
+ clean_query(request_1_json) == clean_query(request_2_json)
44
66
  else
45
67
  false
46
68
  end
@@ -49,7 +71,9 @@ def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
49
71
  end
50
72
  end
51
73
 
52
- # c.debug_logger = File.open('log/vcr_debug.log', 'w')
74
+ if File.exists?('log')
75
+ c.debug_logger = File.open('log/vcr_debug.log', 'w')
76
+ end
53
77
 
54
78
  c.default_cassette_options = {
55
79
  serialize_with: :compressed,
@@ -58,7 +82,7 @@ def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
58
82
  else
59
83
  ENV['RERECORD'] ? :all : :once
60
84
  end,
61
- match_requests_on: [:method, :uri, :try_body, :verify_uri_route]
85
+ match_requests_on: [:method, :try_uri, :try_body, :verify_uri_route]
62
86
  }
63
87
 
64
88
  # Filter the authorization headers of any request by replacing any occurrence of that request's
@@ -111,6 +135,7 @@ def setup_base_vcr(spec_helper_dir, server: nil, application: nil)
111
135
  end
112
136
 
113
137
  def prepare_vcr_secret
138
+ p ENV
114
139
  secret = ENV["CI_SECRET"]
115
140
 
116
141
  if (secret.nil? || secret.empty?) && ENV['IS_CI'] != '1'
@@ -31,6 +31,24 @@ module Etna
31
31
  end.to_h
32
32
  end
33
33
 
34
+ def update_payload(payload, token, request)
35
+ route = server.find_route(request)
36
+
37
+ payload = payload.map{|k,v| [k.to_sym, v]}.to_h
38
+
39
+ return payload unless route
40
+
41
+ begin
42
+ permissions = Etna::Permissions.from_encoded_permissions(payload[:perm])
43
+
44
+ # Skip making an actual call to Janus. This behavior is tested in auth_spec
45
+
46
+ payload[:perm] = permissions.to_string
47
+ end if (!route.ignore_janus? && route.has_user_constraint?(:can_view?))
48
+
49
+ payload
50
+ end
51
+
34
52
  def approve_user(request)
35
53
  token = auth(request,:etna)
36
54
 
@@ -42,7 +60,10 @@ module Etna
42
60
  # We do this to support Metis client tests, we pass in tokens with multiple "."-separated parts, so
43
61
  # have to account for that.
44
62
  payload = JSON.parse(Base64.decode64(token.split('.')[1]))
45
- request.env['etna.user'] = Etna::User.new(payload.map{|k,v| [k.to_sym, v]}.to_h, token)
63
+ request.env['etna.user'] = Etna::User.new(
64
+ update_payload(payload, token, request),
65
+ token
66
+ )
46
67
  end
47
68
 
48
69
  def approve_hmac(request)