etna 0.1.41 → 0.1.44

Sign up to get free protection for your applications and to get access to all the features.
@@ -50,7 +50,25 @@ module Etna
50
50
  end
51
51
  end
52
52
 
53
- class ListFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
53
+ class FolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
54
+ end
55
+
56
+ class ListFolderRequest < FolderRequest
57
+ include JsonSerializableStruct
58
+
59
+ def initialize(**params)
60
+ super({}.update(params))
61
+ end
62
+
63
+ def to_h
64
+ # The :project_name comes in from Polyphemus as a symbol value,
65
+ # we need to make sure it's a string because it's going
66
+ # in the URL.
67
+ super().compact.transform_values(&:to_s)
68
+ end
69
+ end
70
+
71
+ class ListFolderByIdRequest < Struct.new(:project_name, :bucket_name, :folder_id, keyword_init: true)
54
72
  include JsonSerializableStruct
55
73
 
56
74
  def initialize(**params)
@@ -65,7 +83,7 @@ module Etna
65
83
  end
66
84
  end
67
85
 
68
- class CreateFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
86
+ class TouchFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
69
87
  include JsonSerializableStruct
70
88
 
71
89
  def initialize(**params)
@@ -80,7 +98,37 @@ module Etna
80
98
  end
81
99
  end
82
100
 
83
- class DeleteFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
101
+ class TouchFileRequest < Struct.new(:project_name, :bucket_name, :file_path, keyword_init: true)
102
+ include JsonSerializableStruct
103
+
104
+ def initialize(**params)
105
+ super({}.update(params))
106
+ end
107
+
108
+ def to_h
109
+ # The :project_name comes in from Polyphemus as a symbol value,
110
+ # we need to make sure it's a string because it's going
111
+ # in the URL.
112
+ super().compact.transform_values(&:to_s)
113
+ end
114
+ end
115
+
116
+ class CreateFolderRequest < FolderRequest
117
+ include JsonSerializableStruct
118
+
119
+ def initialize(**params)
120
+ super({}.update(params))
121
+ end
122
+
123
+ def to_h
124
+ # The :project_name comes in from Polyphemus as a symbol value,
125
+ # we need to make sure it's a string because it's going
126
+ # in the URL.
127
+ super().compact.transform_values(&:to_s)
128
+ end
129
+ end
130
+
131
+ class DeleteFolderRequest < FolderRequest
84
132
  include JsonSerializableStruct
85
133
 
86
134
  def initialize(**params)
@@ -110,11 +158,11 @@ module Etna
110
158
  end
111
159
  end
112
160
 
113
- class FindRequest < Struct.new(:project_name, :bucket_name, :limit, :offset, :params, keyword_init: true)
161
+ class FindRequest < Struct.new(:project_name, :bucket_name, :limit, :offset, :params, :hide_paths, keyword_init: true)
114
162
  include JsonSerializableStruct
115
163
 
116
164
  def initialize(**args)
117
- super({params: []}.update(args))
165
+ super({params: [], hide_paths: false}.update(args))
118
166
  end
119
167
 
120
168
  def add_param(param)
@@ -126,6 +174,17 @@ module Etna
126
174
  # easier to do from a JSON string
127
175
  JSON.parse(to_json, :symbolize_names => true)
128
176
  end
177
+
178
+ def clone
179
+ FindRequest.new(
180
+ project_name: self.project_name,
181
+ bucket_name: self.bucket_name,
182
+ limit: self.limit,
183
+ offset: self.offset,
184
+ params: self.params.dup,
185
+ hide_paths: self.hide_paths
186
+ )
187
+ end
129
188
  end
130
189
 
131
190
  class FindParam < Struct.new(:attribute, :predicate, :value, :type, keyword_init: true)
@@ -225,6 +284,13 @@ module Etna
225
284
  @raw = raw
226
285
  end
227
286
 
287
+ def with_containing_folder(folder)
288
+ folder_path = folder.is_a?(Folder) ? folder.folder_path : folder
289
+ File.new({}.update(self.raw).update({
290
+ file_path: ::File.join(folder_path, self.file_name)
291
+ }))
292
+ end
293
+
228
294
  def file_path
229
295
  raw[:file_path]
230
296
  end
@@ -259,6 +325,14 @@ module Etna
259
325
  def size
260
326
  raw[:size]
261
327
  end
328
+
329
+ def file_hash
330
+ raw[:file_hash]
331
+ end
332
+
333
+ def folder_id
334
+ raw[:folder_id]
335
+ end
262
336
  end
263
337
 
264
338
  class Folder
@@ -279,6 +353,19 @@ module Etna
279
353
  def bucket_name
280
354
  raw[:bucket_name]
281
355
  end
356
+
357
+ def project_name
358
+ raw[:project_name]
359
+ end
360
+
361
+ def updated_at
362
+ time = raw[:updated_at]
363
+ time.nil? ? nil : Time.parse(time)
364
+ end
365
+
366
+ def id
367
+ raw[:id]
368
+ end
282
369
  end
283
370
 
284
371
  class AuthorizeUploadRequest < Struct.new(:project_name, :bucket_name, :file_path, keyword_init: true)
@@ -18,7 +18,7 @@ module Etna
18
18
  completed = 0.0
19
19
  start = Time.now
20
20
 
21
- unless dest_file_or_io.is_a?(IO)
21
+ unless dest_file_or_io.is_a?(IO) || dest_file_or_io.is_a?(StringIO)
22
22
  ::File.open(dest_file_or_io, 'w') do |io|
23
23
  return do_download(dest_file_or_io, metis_file, &block)
24
24
  end
@@ -110,8 +110,6 @@ module Etna
110
110
 
111
111
  def initialize(source_file: nil, next_blob_size: nil, current_byte_position: nil)
112
112
  self.source_file = source_file
113
- self.next_blob_size = next_blob_size
114
- self.current_byte_position = current_byte_position
115
113
  self.next_blob_size = [file_size, INITIAL_BLOB_SIZE].min
116
114
  self.current_byte_position = 0
117
115
  end
@@ -0,0 +1,95 @@
1
+ module Etna
2
+ module Clients
3
+ class Metis
4
+ class WalkMetisDiffWorkflow < Struct.new(:left_walker, :right_walker, keyword_init: true)
5
+ # Iterates entries of the form [kind, left | nil, right | nil]
6
+ # where kind is one of
7
+ # :left_unique | :right_unique | :left_is_folder | :right_is_folder
8
+ # :unknown | :equal | :right_older | :left_older
9
+ # and left / right is one of
10
+ # nil | Etna::Clients::Metis::File | Etna::Clients::Metis::Folder
11
+ def each(&block)
12
+ left_enum = self.left_walker.to_enum
13
+ right_enum = self.right_walker.to_enum
14
+
15
+ l, l_path = next_or_nil(left_enum)
16
+ r, r_path = next_or_nil(right_enum)
17
+
18
+ while l && r
19
+ if l_path == r_path
20
+ yield [compare_file_or_folders(l, r), l, r]
21
+
22
+ l, l_path = next_or_nil(left_enum)
23
+ r, r_path = next_or_nil(right_enum)
24
+ elsif l_path < r_path
25
+ yield [:left_unique, l, nil]
26
+ l, l_path = next_or_nil(left_enum)
27
+ else
28
+ yield [:right_unique, nil, r]
29
+ r, r_path = next_or_nil(right_enum)
30
+ end
31
+ end
32
+
33
+ while l
34
+ yield [:left_unique, l, nil]
35
+ l, l_path = next_or_nil(left_enum)
36
+ end
37
+
38
+ while r
39
+ yield [:right_unique, nil, r]
40
+ r, r_path = next_or_nil(right_enum)
41
+ end
42
+ end
43
+
44
+ def next_or_nil(enum)
45
+ enum.next
46
+ rescue StopIteration
47
+ [nil, nil]
48
+ end
49
+
50
+ def compare_file_or_folders(l, r)
51
+ if l.is_a?(Etna::Clients::Metis::Folder)
52
+ if r.is_a?(Etna::Clients::Metis::Folder)
53
+ return compare_file_or_folder_age(l, r)
54
+ end
55
+
56
+ return :left_is_folder
57
+ end
58
+
59
+ if r.is_a?(Etna::Clients::Metis::Folder)
60
+ return :right_is_folder
61
+ end
62
+
63
+
64
+ if l.file_hash.nil? || r.file_hash.nil?
65
+ return :unknown
66
+ end
67
+
68
+ if l.file_hash == r.file_hash
69
+ return :equal
70
+ end
71
+
72
+ return compare_file_or_folder_age(l, r)
73
+ end
74
+
75
+ def compare_file_or_folder_age(l, r)
76
+ if l.updated_at.nil?
77
+ return :unknown
78
+ end
79
+
80
+ if r.updated_at.nil?
81
+ return :unknown
82
+ end
83
+
84
+ if l.updated_at < r.updated_at
85
+ return :left_older
86
+ elsif l.updated_at > r.updated_at
87
+ return :right_older
88
+ else
89
+ return :equal
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -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"
@@ -42,7 +42,38 @@ module Etna
42
42
 
43
43
  [501, {}, ['This controller is not implemented.']]
44
44
  rescue Exception => e
45
- handle_error(e)
45
+ error = e
46
+ ensure
47
+ log_request
48
+ return handle_error(error) if error
49
+ end
50
+
51
+ def try_stream(content_type, &block)
52
+ if @request.env['rack.hijack?']
53
+ @request.env['rack.hijack'].call
54
+ stream = @request.env['rack.hijack_io']
55
+
56
+ headers = [
57
+ "HTTP/1.1 200 OK",
58
+ "Content-Type: #{content_type}"
59
+ ]
60
+ stream.write(headers.map { |header| header + "\r\n" }.join)
61
+ stream.write("\r\n")
62
+ stream.flush
63
+
64
+ Thread.new do
65
+ block.call(stream)
66
+ ensure
67
+ stream.close
68
+ end
69
+
70
+ # IO is now streaming and will be processed by above thread.
71
+ @response.close
72
+ else
73
+ @response['Content-Type'] = content_type
74
+ block.call(@response)
75
+ @response.finish
76
+ end
46
77
  end
47
78
 
48
79
  def require_params(*params)
@@ -82,8 +113,36 @@ module Etna
82
113
  @response.finish
83
114
  end
84
115
 
116
+ def config_hosts
117
+ [:janus, :magma, :timur, :metis, :vulcan, :polyphemus].map do |host|
118
+ [ :"#{host}_host", @server.send(:application).config(host)&.dig(:host) ]
119
+ end.to_h.compact
120
+ end
121
+
85
122
  private
86
123
 
124
+ def redact_keys
125
+ @request.env['etna.redact_keys']
126
+ end
127
+
128
+ def add_redact_keys(new_redact_keys=[])
129
+ @request.env['etna.redact_keys'] = (@request.env['etna.redact_keys'] || []).concat(new_redact_keys)
130
+ end
131
+
132
+ def log_request
133
+ censor = Etna::Censor.new(redact_keys)
134
+
135
+ redacted_params = @params.map do |key,value|
136
+ [ key, censor.redact(key, value) ]
137
+ end.to_h
138
+
139
+ log("User #{@user ? @user.email : :unknown} calling #{controller_name}##{@action} with params #{redacted_params}")
140
+ end
141
+
142
+ def controller_name
143
+ self.class.name.sub("Kernel::", "").sub("Controller", "").downcase
144
+ end
145
+
87
146
  def success(msg, content_type='text/plain')
88
147
  @response['Content-Type'] = content_type
89
148
  @response.write(msg)
@@ -0,0 +1,99 @@
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
+ private
54
+
55
+ def current_project_names
56
+ @permissions.map(&:project_name)
57
+ end
58
+ end
59
+
60
+ class Permission
61
+ attr_reader :role, :project_name, :role_key
62
+
63
+ ROLE_NAMES = {
64
+ "A" => :admin,
65
+ "E" => :editor,
66
+ "V" => :viewer,
67
+ }
68
+
69
+ def initialize(role_key, project_name)
70
+ @role_key = role_key
71
+ @role = Etna::Role.new(ROLE_NAMES[role_key.upcase], role_key == role_key.upcase)
72
+ @project_name = project_name
73
+ end
74
+
75
+ def to_hash
76
+ role.to_hash
77
+ end
78
+ end
79
+
80
+ class Role
81
+ attr_reader :role, :restricted
82
+ def initialize(role, restricted)
83
+ @role = role
84
+ @restricted = restricted
85
+ end
86
+
87
+ def key
88
+ role_key = role.to_s[0]
89
+ restricted ? role_key.upcase : role_key
90
+ end
91
+
92
+ def to_hash
93
+ {
94
+ role: role,
95
+ restricted: restricted,
96
+ }
97
+ end
98
+ end
99
+ end
data/lib/etna/route.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'digest'
2
2
  require 'date'
3
+ require_relative "./censor"
3
4
 
4
5
  module Etna
5
6
  class Route
@@ -13,6 +14,7 @@ module Etna
13
14
  @route = route.gsub(/\A(?=[^\/])/, '/')
14
15
  @block = block
15
16
  @match_ext = options[:match_ext]
17
+ @log_redact_keys = options[:log_redact_keys]
16
18
  end
17
19
 
18
20
  def to_hash
@@ -124,6 +126,8 @@ module Etna
124
126
  return [ 403, { 'Content-Type' => 'application/json' }, [ { error: 'You are forbidden from performing this action.' }.to_json ] ]
125
127
  end
126
128
 
129
+ request.env['etna.redact_keys'] = @log_redact_keys
130
+
127
131
  if @action
128
132
  controller, action = @action.split('#')
129
133
  controller_class = Kernel.const_get(
@@ -132,11 +136,6 @@ module Etna
132
136
  logger = request.env['etna.logger']
133
137
  user = request.env['etna.user']
134
138
 
135
- params = request.env['rack.request.params'].map do |key,value|
136
- [ key, redact(key, value) ]
137
- end.to_h
138
-
139
- logger.warn("User #{user ? user.email : :unknown} calling #{controller}##{action} with params #{params}")
140
139
  return controller_class.new(request, action).response
141
140
  elsif @block
142
141
  application = Etna::Application.find(app.class).class
@@ -155,41 +154,16 @@ module Etna
155
154
  @auth && @auth[:ignore_janus]
156
155
  end
157
156
 
157
+ def has_user_constraint?(constraint)
158
+ @auth && @auth[:user] && @auth[:user][constraint]
159
+ end
160
+
158
161
  private
159
162
 
160
163
  def application
161
164
  @application ||= Etna::Application.instance
162
165
  end
163
166
 
164
- def compact(value)
165
- value = value.to_s
166
- value = value[0..500] + "..." + value[-100..-1] if value.length > 600
167
- value
168
- end
169
-
170
- def redact_keys
171
- @redact_keys ||= application.config(:log_redact_keys).split(",").map do |key|
172
- key.to_sym
173
- end
174
- end
175
-
176
- def redact(key, value)
177
- # From configuration, redact any values for the supplied key values, so they
178
- # don't appear in the logs.
179
- return compact(value) unless application.config(:log_redact_keys)
180
-
181
- if value.is_a?(Hash)
182
- redacted_value = value.map do |value_key, value_value|
183
- [ value_key, redact(value_key, value_value) ]
184
- end.to_h
185
- return redacted_value
186
- elsif redact_keys.include?(key)
187
- return "*"
188
- end
189
-
190
- return compact(value)
191
- end
192
-
193
167
  def authorized?(request)
194
168
  # If there is no @auth requirement, they are ok - this doesn't preclude
195
169
  # them being rejected in the controller response
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
@@ -0,0 +1,14 @@
1
+ module Etna
2
+ class SynchronizeDb
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ # Do a coarse checkout of the connection
9
+ Etna::Application.instance.db.synchronize do
10
+ @app.call(env)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -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)