aspera-cli 4.26.2 → 4.27.1

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.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +310 -21
  4. data/CONTRIBUTING.md +99 -14
  5. data/TODO.md +50 -0
  6. data/bin/ascli +3 -3
  7. data/docs/README.md +10977 -0
  8. data/docs/test-mcp-with-ai.md +202 -0
  9. data/lib/aspera/agent/base.rb +29 -7
  10. data/lib/aspera/agent/connect.rb +58 -21
  11. data/lib/aspera/agent/desktop.rb +53 -18
  12. data/lib/aspera/agent/direct.rb +71 -40
  13. data/lib/aspera/agent/factory.rb +3 -3
  14. data/lib/aspera/agent/httpgw.rb +55 -14
  15. data/lib/aspera/agent/node.rb +34 -2
  16. data/lib/aspera/agent/transferd.rb +48 -12
  17. data/lib/aspera/api/alee.rb +1 -1
  18. data/lib/aspera/api/aoc.rb +42 -44
  19. data/lib/aspera/api/cos_node.rb +10 -9
  20. data/lib/aspera/api/faspex.rb +16 -15
  21. data/lib/aspera/api/httpgw.rb +23 -23
  22. data/lib/aspera/api/node.rb +51 -70
  23. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  24. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  25. data/lib/aspera/ascmd.rb +32 -30
  26. data/lib/aspera/ascp/installation.rb +75 -69
  27. data/lib/aspera/ascp/management.rb +7 -8
  28. data/lib/aspera/assert.rb +12 -13
  29. data/lib/aspera/cli/ascp_actions.rb +155 -0
  30. data/lib/aspera/cli/async_transfer_store.rb +125 -0
  31. data/lib/aspera/cli/bootstrapper.rb +197 -0
  32. data/lib/aspera/cli/command_registry.rb +145 -0
  33. data/lib/aspera/cli/command_spec.rb +141 -0
  34. data/lib/aspera/cli/context.rb +19 -6
  35. data/lib/aspera/cli/error.rb +16 -0
  36. data/lib/aspera/cli/extended_value.rb +27 -26
  37. data/lib/aspera/cli/formatter.rb +95 -59
  38. data/lib/aspera/cli/gem_checker.rb +65 -0
  39. data/lib/aspera/cli/hints.rb +3 -3
  40. data/lib/aspera/cli/http.rb +74 -30
  41. data/lib/aspera/cli/info.rb +2 -0
  42. data/lib/aspera/cli/mailer.rb +97 -0
  43. data/lib/aspera/cli/mcp_tool.rb +225 -0
  44. data/lib/aspera/cli/option_declarator.rb +77 -0
  45. data/lib/aspera/cli/options.schema.yaml +634 -7
  46. data/lib/aspera/cli/parser.rb +1217 -0
  47. data/lib/aspera/cli/plugins/alee.rb +20 -22
  48. data/lib/aspera/cli/plugins/aoc.rb +1178 -881
  49. data/lib/aspera/cli/plugins/ats.rb +205 -162
  50. data/lib/aspera/cli/plugins/base.rb +703 -175
  51. data/lib/aspera/cli/plugins/basic_auth.rb +7 -9
  52. data/lib/aspera/cli/plugins/config.rb +434 -757
  53. data/lib/aspera/cli/plugins/console.rb +107 -65
  54. data/lib/aspera/cli/plugins/cos.rb +46 -33
  55. data/lib/aspera/cli/plugins/factory.rb +6 -6
  56. data/lib/aspera/cli/plugins/faspex5.rb +627 -388
  57. data/lib/aspera/cli/plugins/faspio.rb +54 -51
  58. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  59. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  60. data/lib/aspera/cli/plugins/node.rb +988 -840
  61. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  62. data/lib/aspera/cli/plugins/orchestrator.rb +112 -135
  63. data/lib/aspera/cli/plugins/preview.rb +195 -157
  64. data/lib/aspera/cli/plugins/server.rb +141 -91
  65. data/lib/aspera/cli/plugins/shares.rb +343 -110
  66. data/lib/aspera/cli/preset_actions.rb +159 -0
  67. data/lib/aspera/cli/preset_manager.rb +82 -39
  68. data/lib/aspera/cli/result.rb +78 -26
  69. data/lib/aspera/cli/runner.rb +323 -153
  70. data/lib/aspera/cli/secret_finder.rb +40 -0
  71. data/lib/aspera/cli/special_values.rb +1 -0
  72. data/lib/aspera/cli/sync_actions.rb +89 -64
  73. data/lib/aspera/cli/terminal_formatter.rb +3 -3
  74. data/lib/aspera/cli/transfer_actions.rb +90 -0
  75. data/lib/aspera/cli/transfer_agent.rb +129 -64
  76. data/lib/aspera/cli/transfer_progress.rb +9 -9
  77. data/lib/aspera/cli/vault_manager.rb +79 -0
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +28 -30
  80. data/lib/aspera/colors.rb +3 -3
  81. data/lib/aspera/command_line_builder.rb +34 -34
  82. data/lib/aspera/command_line_converter.rb +1 -1
  83. data/lib/aspera/coverage.rb +1 -2
  84. data/lib/aspera/data_repository.rb +2 -1
  85. data/lib/aspera/dot_container.rb +12 -12
  86. data/lib/aspera/environment.rb +30 -23
  87. data/lib/aspera/exec_spec.rb +13 -0
  88. data/lib/aspera/faspex_gw.rb +5 -5
  89. data/lib/aspera/faspex_postproc.rb +16 -10
  90. data/lib/aspera/graphql.rb +37 -0
  91. data/lib/aspera/hash_ext.rb +8 -2
  92. data/lib/aspera/json_rpc/client.rb +62 -0
  93. data/lib/aspera/json_rpc/version.rb +7 -0
  94. data/lib/aspera/keychain/base.rb +10 -3
  95. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  96. data/lib/aspera/keychain/factory.rb +20 -8
  97. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  98. data/lib/aspera/keychain/macos_security.rb +23 -25
  99. data/lib/aspera/keychain/one_password_api.rb +86 -0
  100. data/lib/aspera/keychain/one_password_base.rb +34 -0
  101. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  102. data/lib/aspera/link_header.rb +82 -0
  103. data/lib/aspera/log.rb +29 -15
  104. data/lib/aspera/markdown.rb +90 -7
  105. data/lib/aspera/nagios.rb +8 -8
  106. data/lib/aspera/node_simulator.rb +35 -27
  107. data/lib/aspera/oauth/base.rb +9 -10
  108. data/lib/aspera/oauth/boot.rb +5 -5
  109. data/lib/aspera/oauth/factory.rb +12 -10
  110. data/lib/aspera/oauth/jwt.rb +9 -11
  111. data/lib/aspera/oauth/web.rb +6 -6
  112. data/lib/aspera/persistency_action_once.rb +12 -12
  113. data/lib/aspera/persistency_folder.rb +18 -11
  114. data/lib/aspera/preview/file_types.rb +5 -5
  115. data/lib/aspera/preview/generator.rb +52 -49
  116. data/lib/aspera/preview/options.rb +3 -2
  117. data/lib/aspera/preview/terminal.rb +5 -5
  118. data/lib/aspera/preview/utils.rb +66 -19
  119. data/lib/aspera/products/connect.rb +2 -2
  120. data/lib/aspera/products/desktop.rb +1 -1
  121. data/lib/aspera/products/other.rb +3 -3
  122. data/lib/aspera/products/transferd.rb +4 -4
  123. data/lib/aspera/proxy_auto_config.rb +15 -14
  124. data/lib/aspera/rest.rb +159 -151
  125. data/lib/aspera/rest_error_analyzer.rb +6 -6
  126. data/lib/aspera/rest_errors_aspera.rb +0 -10
  127. data/lib/aspera/rest_list.rb +11 -10
  128. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  129. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  130. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  131. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  132. data/lib/aspera/schema/documentation.rb +65 -28
  133. data/lib/aspera/schema/reader.rb +136 -13
  134. data/lib/aspera/schema/registry.rb +60 -8
  135. data/lib/aspera/secret_hider.rb +12 -7
  136. data/lib/aspera/ssh.rb +64 -31
  137. data/lib/aspera/ssl.rb +5 -5
  138. data/lib/aspera/sync/conf.schema.yaml +2 -2
  139. data/lib/aspera/sync/database.rb +2 -2
  140. data/lib/aspera/sync/operations.rb +22 -25
  141. data/lib/aspera/temp_file_manager.rb +25 -5
  142. data/lib/aspera/timer_limiter.rb +1 -1
  143. data/lib/aspera/transfer/faux_file.rb +24 -11
  144. data/lib/aspera/transfer/parameters.rb +36 -34
  145. data/lib/aspera/transfer/result.rb +74 -0
  146. data/lib/aspera/transfer/resumer.rb +10 -10
  147. data/lib/aspera/transfer/spec.rb +18 -1
  148. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  149. data/lib/aspera/uri_reader.rb +52 -15
  150. data/lib/aspera/web_auth.rb +8 -7
  151. data/lib/aspera/web_server_simple.rb +15 -12
  152. data/lib/aspera/yaml.rb +5 -4
  153. data.tar.gz.sig +0 -0
  154. metadata +34 -6
  155. metadata.gz.sig +0 -0
  156. data/lib/aspera/cli/manager.rb +0 -766
  157. data/lib/aspera/cli/plugins/faspex.rb +0 -539
  158. data/lib/aspera/json_rpc.rb +0 -52
  159. data/lib/aspera/transfer/uri.rb +0 -56
@@ -2,8 +2,10 @@
2
2
 
3
3
  # spellchecker: ignore workgroups mypackages passcode
4
4
 
5
+ require 'aspera/schema/registry'
5
6
  require 'aspera/cli/plugins/oauth'
6
7
  require 'aspera/cli/extended_value'
8
+ require 'aspera/transfer/result'
7
9
  require 'aspera/cli/special_values'
8
10
  require 'aspera/cli/wizard'
9
11
  require 'aspera/api/faspex'
@@ -18,11 +20,11 @@ module Aspera
18
20
  module Cli
19
21
  module Plugins
20
22
  class Faspex5 < Oauth
21
- class << self
22
- def application_name
23
- 'Faspex'
24
- end
23
+ application_name 'Faspex v5'
24
+ # Commands that may carry a query_schema annotation
25
+ QUERY_SCHEMA_COMMANDS = %i[list delete].freeze
25
26
 
27
+ class << self
26
28
  # @return [Hash,NilClass]
27
29
  def detect(address_or_url)
28
30
  # add scheme if missing
@@ -45,7 +47,7 @@ module Aspera
45
47
  }
46
48
  rescue StandardError => e
47
49
  error = e
48
- Log.log.debug{"detect error: #{e}"}
50
+ Log.log.debug { "detect error: #{e}" }
49
51
  end
50
52
  raise error if error
51
53
  return
@@ -79,24 +81,23 @@ module Aspera
79
81
  )
80
82
  return {
81
83
  preset_value: {
82
- url: app_url,
83
- username: wiz_username,
84
- auth: :jwt.to_s,
85
- private_key: "@file:#{private_key_path}",
86
- params: {
87
- client_id: client_id,
88
- client_secret: client_secret
89
- }
84
+ url: app_url,
85
+ username: wiz_username,
86
+ auth: :jwt.to_s,
87
+ private_key: "@file:#{private_key_path}",
88
+ client_id: client_id,
89
+ client_secret: client_secret
90
90
  },
91
91
  test_args: 'user profile show'
92
92
  }
93
93
  end
94
94
 
95
+ option :box, description: "Package inbox, either shared inbox name or one of: #{Api::Faspex::API_LIST_MAILBOX_TYPES.join(', ')} or #{SpecialValues::ALL}", default: 'inbox_all'
96
+ option :shared_folder, description: 'Send package with files from shared folder'
97
+ option :group_type, description: 'Type of shared box', allowed: %i[shared_inboxes workgroups], default: :shared_inboxes
98
+
95
99
  def initialize(**_)
96
100
  super
97
- options.declare(:box, "Package inbox, either shared inbox name or one of: #{Api::Faspex::API_LIST_MAILBOX_TYPES.join(', ')} or #{SpecialValues::ALL}", default: 'inbox_all')
98
- options.declare(:shared_folder, 'Send package with files from shared folder')
99
- options.declare(:group_type, 'Type of shared box', allowed: %i[shared_inboxes workgroups], default: :shared_inboxes)
100
101
  options.parse_options!
101
102
  # [Aspera::Api::Faspex]
102
103
  @api_v5 = nil
@@ -106,7 +107,7 @@ module Aspera
106
107
  def normalize_recipients(parameters, type)
107
108
  type = type.to_s
108
109
  return unless parameters.key?(type)
109
- Aspera.assert_type(parameters[type], Array){type}
110
+ Aspera.assert_type(parameters[type], Array) { type }
110
111
  recipient_types = Api::Faspex::RECIPIENT_TYPES
111
112
  if parameters.key?('recipient_types')
112
113
  recipient_types = parameters['recipient_types']
@@ -136,24 +137,24 @@ module Aspera
136
137
  # user asked to not follow
137
138
  return status if status_list.nil?
138
139
  if status['upload_status'].eql?('submitted')
139
- config.progress_bar&.event(:sessions_init, session_id: nil, info: status['upload_status'])
140
+ progress_bar&.event(:sessions_init, session_id: nil, info: status['upload_status'])
140
141
  elsif !total_sent
141
- config.progress_bar&.event(:session_start, session_id: id)
142
- config.progress_bar&.event(:session_size, session_id: id, info: status['bytes_total'].to_i)
142
+ progress_bar&.event(:session_start, session_id: id)
143
+ progress_bar&.event(:session_size, session_id: id, info: status['bytes_total'].to_i)
143
144
  total_sent = true
144
145
  else
145
- config.progress_bar&.event(:transfer, session_id: id, info: status['bytes_written'].to_i)
146
+ progress_bar&.event(:transfer, session_id: id, info: status['bytes_written'].to_i)
146
147
  end
147
148
  if status_list.include?(status['upload_status'])
148
- config.progress_bar&.event(:session_end, session_id: id)
149
- config.progress_bar&.event(:end)
149
+ progress_bar&.event(:session_end, session_id: id)
150
+ progress_bar&.event(:end)
150
151
  return status
151
152
  end
152
153
  sleep(1.0)
153
154
  end
154
155
  end
155
156
 
156
- # @param [String] job identifier
157
+ # @param job_id [String] job identifier
157
158
  # @return [Hash] result of API call for job status
158
159
  def wait_for_job(job_id)
159
160
  result = nil
@@ -167,9 +168,15 @@ module Aspera
167
168
  return result
168
169
  end
169
170
 
170
- # list all packages with optional filter
171
- def list_packages_with_filter(query: {})
172
- filter = options.get_next_argument('filter', mandatory: false, validation: Proc, default: ->(_x){true})
171
+ # List all packages with optional filter.
172
+ # The special `max` key is extracted *before* the API call and returned separately,
173
+ # so callers that apply a post-API filter (e.g. once_only) can enforce the limit
174
+ # after filtering rather than before.
175
+ # @param filter [Proc, nil] optional filter lambda applied to each package entry
176
+ # @param query [Hash] additional query parameters forwarded to the API
177
+ # @return [Array(Array, Integer|nil, Integer|nil)] [filtered list, max (or nil), total count]
178
+ def list_packages_with_filter(filter: nil, query: {})
179
+ filter ||= ->(_x) { true }
173
180
  box = options.get_option(:box)
174
181
  # Translate box name to API prefix (with ending slash)
175
182
  entity =
@@ -180,8 +187,13 @@ module Aspera
180
187
  group_type = options.get_option(:group_type)
181
188
  "#{group_type}/#{@api_v5.lookup_entity_by_field(entity: group_type, value: box)['id']}/packages"
182
189
  end
183
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: entity, query: query_read_delete(default: query))
184
- return list.select(&filter), total
190
+ # Merge default query with user-provided query: user values take precedence, but defaults are preserved
191
+ user_query = query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'packages'))
192
+ merged_query = user_query.nil? ? query.dup : query.merge(user_query)
193
+ # Extract `max` before the API call so callers can apply it after post-API filtering
194
+ max_items = merged_query.delete(RestList::MAX_ITEMS)&.to_i
195
+ list, total = @api_v5.list_entities_limit_offset_total_count(entity: entity, query: merged_query)
196
+ return list.select(&filter), max_items, total
185
197
  end
186
198
 
187
199
  # Build query to get package recipients based on package info in case of shared inbox or workgroup recipient
@@ -214,22 +226,24 @@ module Aspera
214
226
  case package_ids
215
227
  when SpecialValues::INIT
216
228
  Aspera.assert(skip_ids_persistency, 'Only with option once_only')
217
- skip_ids_persistency.data.clear.concat(list_packages_with_filter.first.map{ |p| p['id']})
229
+ skip_ids_persistency.data.clear.concat(list_packages_with_filter.first.map { |p| p['id'] }) # no filter: all packages, max ignored
218
230
  skip_ids_persistency.save
219
231
  return Result::Status.new("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
220
232
  when SpecialValues::ALL
221
233
  # TODO: if packages have same name, they will overwrite ?
222
- packages = list_packages_with_filter(query: {'status' => 'completed'}).first
223
- Log.dump(:package_ids, level: :trace1){packages.map{ |p| p['id']}}
234
+ packages, max_items = list_packages_with_filter(query: {'status' => 'completed'}) # no filter: all completed packages
235
+ Log.dump(:package_ids, level: :trace1) { packages.map { |p| p['id'] } }
224
236
  Log.dump(:skip_ids, skip_ids_persistency.data, level: :trace1)
225
- packages.reject!{ |p| skip_ids_persistency.data.include?(p['id'])} if skip_ids_persistency
226
- Log.dump(:package_ids, level: :trace1){packages.map{ |p| p['id']}}
237
+ packages.reject! { |p| skip_ids_persistency.data.include?(p['id']) } if skip_ids_persistency
238
+ # Apply `max` after once_only filtering so we get the N first not-yet-downloaded packages
239
+ packages = packages[0, max_items] if max_items
240
+ Log.dump(:package_ids, level: :trace1) { packages.map { |p| p['id'] } }
227
241
  else
228
242
  # a single id was provided, or a list of ids
229
243
  package_ids = [package_ids] unless package_ids.is_a?(Array)
230
- Aspera.assert_array_all(package_ids, String){'Package id(s)'}
244
+ Aspera.assert_array_all(package_ids, String) { 'Package id(s)' }
231
245
  # packages = package_ids.map{|pkg_id|@api_v5.read("packages/#{pkg_id}")}
232
- packages = package_ids.map{ |pkg_id| {'id'=>pkg_id}}
246
+ packages = package_ids.map { |pkg_id| {'id'=>pkg_id} }
233
247
  end
234
248
  result_transfer = []
235
249
  param_file_list = {}
@@ -261,7 +275,7 @@ module Aspera
261
275
  statuses = transfer.start(transfer_spec)
262
276
  result_transfer.push({'package' => pkg_id, Runner::STATUS_FIELD => statuses})
263
277
  # skip only if all sessions completed
264
- if TransferAgent.session_status(statuses).eql?(:success) && skip_ids_persistency
278
+ if statuses.is_a?(Transfer::Result::Success) && skip_ids_persistency
265
279
  skip_ids_persistency.data.push(pkg_id)
266
280
  skip_ids_persistency.save
267
281
  end
@@ -269,8 +283,7 @@ module Aspera
269
283
  return Runner.result_transfer_multiple(result_transfer)
270
284
  end
271
285
 
272
- def package_send
273
- parameters = value_create_modify(command: :send, schema: Schema::Registry.req_body(Schema::Registry::FASPEX, 'packages.post'))
286
+ def package_send(parameters)
274
287
  # autofill recipient for public url
275
288
  if @api_v5.pub_link_context&.key?('recipient_type') && !parameters.key?('recipients')
276
289
  parameters['recipients'] = [{
@@ -278,7 +291,7 @@ module Aspera
278
291
  recipient_type: @api_v5.pub_link_context['recipient_type']
279
292
  }]
280
293
  end
281
- PACKAGE_RECIPIENT_TYPES.each{ |type| normalize_recipients(parameters, type)}
294
+ PACKAGE_RECIPIENT_TYPES.each { |type| normalize_recipients(parameters, type) }
282
295
  # User specified content prot in tspec, but faspex requires in package creation
283
296
  # `transfer_spec/upload` will set `content_protection`
284
297
  if transfer.user_transfer_spec['content_protection'] && !parameters.key?('ear_enabled')
@@ -299,7 +312,7 @@ module Aspera
299
312
  return Runner.result_transfer(transfer.start(transfer_spec))
300
313
  else
301
314
  # send from remote shared folder
302
- if (m = Manager.percent_selector(shared_folder))
315
+ if (m = Parser.percent_selector(shared_folder))
303
316
  shared_folder = @api_v5.lookup_entity_by_field(
304
317
  entity: 'shared_folders',
305
318
  field: m[:field],
@@ -320,16 +333,17 @@ module Aspera
320
333
 
321
334
  # Browse a folder
322
335
  # @param browse_endpoint [String] the endpoint to browse
323
- def browse_folder(browse_endpoint, base_query = {})
324
- folders_to_process = [options.get_next_argument('folder path', default: '/')]
336
+ # @param folder_path [String] starting path (default: '/')
337
+ def browse_folder(browse_endpoint, base_query = {}, folder_path: '/')
338
+ folders_to_process = [folder_path]
325
339
  query = base_query.merge(query_read_delete(default: {}))
326
- filters = query.delete('filters'){{}}
340
+ filters = query.delete('filters') { {} }
327
341
  Aspera.assert_type(filters, Hash)
328
342
  filters['basenames'] ||= []
329
- Aspera.assert_type(filters, Hash){'filters'}
343
+ Aspera.assert_type(filters, Hash) { 'filters' }
330
344
  max_items = query.delete(RestList::MAX_ITEMS)
331
345
  recursive = query.delete('recursive')
332
- use_paging = query.delete('paging'){true}
346
+ use_paging = query.delete('paging') { true }
333
347
  if use_paging
334
348
  browse_endpoint = "#{browse_endpoint}/page"
335
349
  query['per_page'] ||= 500
@@ -356,7 +370,7 @@ module Aspera
356
370
  all_items = all_items.slice(0, max_items) if all_items.count > max_items
357
371
  break
358
372
  end
359
- folders_to_process.concat(data['items'].select{ |i| i['type'].eql?('directory')}.map{ |i| i['path']}) if recursive
373
+ folders_to_process.concat(data['items'].select { |i| i['type'].eql?('directory') }.map { |i| i['path'] }) if recursive
360
374
  if use_paging
361
375
  iteration_token = http[Api::Faspex::HEADER_X_NEXT_ITER_TOKEN]
362
376
  break if iteration_token.nil? || iteration_token.empty?
@@ -374,374 +388,599 @@ module Aspera
374
388
  return Result::ObjectList.new(all_items, total: total_count)
375
389
  end
376
390
 
377
- def package_action
378
- command = options.get_next_command(%i[show browse status delete receive send list file_processing])
379
- package_id =
380
- unless %i[send list].include?(command)
381
- @api_v5.pub_link_context&.key?('package_id') ? @api_v5.pub_link_context['package_id'] : options.instance_identifier
382
- end
383
- package_instance = "packages/#{package_id}"
384
- case command
385
- when :show
386
- return Result::SingleObject.new(@api_v5.read(package_instance))
387
- when :browse
388
- return browse_folder("#{package_instance}/files/#{Api::Faspex.box_type(options.get_option(:box))}", recipient_query(package_id))
389
- when :status
390
- status_list = options.get_next_argument('list of states, or nothing', mandatory: false, validation: Array)
391
- status = wait_package_status(package_id, status_list: status_list)
392
- return Result::SingleObject.new(status)
393
- when :delete
394
- ids = package_id
395
- ids = [ids] unless ids.is_a?(Array)
396
- Aspera.assert_array_all(ids, String){'Package id(s)'}
397
- # API returns 204, empty on success
398
- @api_v5.call(
399
- operation: 'DELETE',
400
- subpath: 'packages',
401
- content_type: Mime::JSON,
402
- body: {ids: ids},
403
- headers: {'Accept' => Mime::JSON}
404
- )
405
- return Result::Status.new('Package(s) deleted')
406
- when :receive
407
- return package_receive(package_id)
408
- when :send
409
- return package_send
410
- when :list
411
- list, total = list_packages_with_filter
412
- fields = %w[id title status sender.name recipients.0.name release_date total_bytes total_files]
413
- fields.delete('recipients.0.name') if %w[inbox inbox_history].include?(options.get_option(:box))
414
- fields.delete('sender.name') if %w[outbox outbox_history].include?(options.get_option(:box))
415
- return Result::ObjectList.new(list, total: total, fields: fields)
416
- when :file_processing
417
- result, count = @api_v5.list_entities_limit_offset_total_count(
418
- entity: "#{package_instance}/file_statuses",
419
- items_key: 'files'
420
- )
421
- return Result::ObjectList.new(result, total: count)
391
+ # Per-resource configuration for admin CRUD sub-trees.
392
+ # Keys mirror entity_list/show/create/modify/delete kwargs; extra_commands lists additional leaf commands.
393
+ # @return [Hash{Symbol => Hash}]
394
+ RESOURCE_CONFIG = {
395
+ accounts: {
396
+ display_fields: -> { Formatter.all_but('user_profile_data_attributes') },
397
+ extra_commands: [:reset_password],
398
+ instance_arg_commands: {reset_password: {arguments: [{name: :contact_id, type: :identifier, lookup: ->(field, value, **) { res_lookup_id(:accounts, field, value) }}]}},
399
+ query_component: Schema::Registry::FASPEX,
400
+ body_component: Schema::Registry::FASPEX
401
+ },
402
+ alternate_addresses: {entity: 'configuration/alternate_addresses', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
403
+ contacts: {query_component: Schema::Registry::FASPEX},
404
+ distribution_lists: {entity: 'account/distribution_lists', delete_style: 'ids', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
405
+ email_notifications: {id_as_arg: 'type', query_component: Schema::Registry::FASPEX},
406
+ file_processing: {
407
+ commands: %i[next modify],
408
+ body_component: Schema::Registry::FASPEX,
409
+ is_singleton: true
410
+ },
411
+ jobs: {display_fields: %w[id job_name job_type status], query_component: Schema::Registry::FASPEX},
412
+ metadata_profiles: {entity: 'configuration/metadata_profiles', items_key: 'profiles', query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
413
+ nodes: {
414
+ extra_commands: %i[browse],
415
+ instance_arg_commands: {
416
+ browse: {
417
+ arguments: [{name: :node_id, type: :identifier, lookup: :lookup_node_id},
418
+ {name: :folder_path, type: String, mandatory: false, default: '/'}]
419
+ }
420
+ },
421
+ query_component: Schema::Registry::FASPEX,
422
+ body_component: Schema::Registry::FASPEX
423
+ },
424
+ oauth_clients: {
425
+ display_fields: -> { Formatter.all_but('public_key') },
426
+ api: -> { Api::Faspex.new(root: Api::Faspex::PATH_AUTH, **Oauth.kwargs_from_options(options)) },
427
+ list_query: {'expand': true, 'no_api_path': true, 'client_types[]': 'public'},
428
+ query_component: Schema::Registry::FASPEX,
429
+ body_component: Schema::Registry::FASPEX
430
+ },
431
+ registrations: {query_component: Schema::Registry::FASPEX},
432
+ saml_configs: {query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
433
+ shared_inboxes: {res_id_query: {'all': true}, query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
434
+ webhooks: {query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX},
435
+ workgroups: {res_id_query: {'all': true}, query_component: Schema::Registry::FASPEX, body_component: Schema::Registry::FASPEX}
436
+ }.freeze
437
+ private_constant :RESOURCE_CONFIG
438
+
439
+ # Resolve a RESOURCE_CONFIG value that may be a Proc (evaluated in instance context).
440
+ def resource_config_value(cfg, key)
441
+ v = cfg[key]
442
+ v.is_a?(Proc) ? instance_exec(&v) : v
443
+ end
444
+
445
+ # Build args hash from RESOURCE_CONFIG for use with entity_list/show/create/modify/delete.
446
+ def res_exec_args(res_sym)
447
+ cfg = RESOURCE_CONFIG.fetch(res_sym, {})
448
+ {
449
+ api: resource_config_value(cfg, :api) || @api_v5,
450
+ entity: resource_config_value(cfg, :entity) || res_sym.to_s,
451
+ items_key: resource_config_value(cfg, :items_key),
452
+ delete_style: resource_config_value(cfg, :delete_style),
453
+ id_as_arg: resource_config_value(cfg, :id_as_arg) || false,
454
+ display_fields: resource_config_value(cfg, :display_fields),
455
+ list_query: resource_config_value(cfg, :list_query),
456
+ is_singleton: resource_config_value(cfg, :is_singleton) || false,
457
+ query_component: resource_config_value(cfg, :query_component),
458
+ body_component: resource_config_value(cfg, :body_component)
459
+ }.compact
460
+ end
461
+
462
+ # admin > nodes — lookup node id by field/value
463
+ def lookup_node_id(field, value, **)
464
+ @api_v5.lookup_entity_by_field(entity: 'nodes', field: field, value: value)['id']
465
+ end
466
+
467
+ # Lookup id for a RESOURCE_CONFIG resource by field/value.
468
+ def res_lookup_id(res_sym, field, value)
469
+ cfg = RESOURCE_CONFIG.fetch(res_sym, {})
470
+ entity = resource_config_value(cfg, :entity) || res_sym.to_s
471
+ items_key = resource_config_value(cfg, :items_key)
472
+ res_id_query = resource_config_value(cfg, :res_id_query) || :default
473
+ @api_v5.lookup_entity_by_field(entity: entity, value: value, field: field, items_key: items_key, query: res_id_query)['id']
474
+ end
475
+
476
+ # Dynamically define lookup methods for admin resources (used by crud_commands)
477
+ Api::Faspex::ADMIN_RESOURCES.each do |res|
478
+ define_method(:"lookup_admin_#{res}_id") do |field, value, **|
479
+ res_lookup_id(res, field, value)
422
480
  end
423
481
  end
424
482
 
425
- def execute_resource(res_sym)
426
- # Arguments for Plugin::Base::entity_execute
427
- exec_args = {
428
- api: @api_v5,
429
- entity: res_sym.to_s
430
- }
431
- res_id_query = :default
432
- available_commands = Operations::ALL
433
- case res_sym
434
- when :metadata_profiles
435
- exec_args[:entity] = 'configuration/metadata_profiles'
436
- exec_args[:items_key] = 'profiles'
437
- when :alternate_addresses
438
- exec_args[:entity] = 'configuration/alternate_addresses'
439
- when :distribution_lists
440
- exec_args[:entity] = 'account/distribution_lists'
441
- exec_args[:delete_style] = 'ids'
442
- when :email_notifications
443
- exec_args.delete(:items_key)
444
- exec_args[:id_as_arg] = 'type'
445
- when :accounts
446
- exec_args[:display_fields] = Formatter.all_but('user_profile_data_attributes')
447
- available_commands += [:reset_password]
448
- when :oauth_clients
449
- exec_args[:display_fields] = Formatter.all_but('public_key')
450
- exec_args[:api] = Api::Faspex.new(root: Api::Faspex::PATH_AUTH, **Oauth.kwargs_from_options(options))
451
- exec_args[:list_query] = {'expand': true, 'no_api_path': true, 'client_types[]': 'public'}
452
- when :shared_inboxes, :workgroups
453
- available_commands += %i[members saml_groups invite_external_collaborator]
454
- res_id_query = {'all': true}
455
- when :nodes
456
- available_commands += %i[shared_folders browse]
457
- when :jobs
458
- exec_args[:display_fields] = %w[id job_name job_type status]
459
- when :file_processing
460
- available_commands = %i[next modify]
461
- # schema for modify
462
- exec_args[:schema] = Schema::Registry.req_body(Schema::Registry::FASPEX, 'file_processing.put')
463
- exec_args[:is_singleton] = true
464
- end
465
- res_command = options.get_next_command(available_commands)
466
- return Result::ValueList.new(Api::Faspex::EMAIL_NOTIF_LIST, name: 'email_id') if res_command.eql?(:list) && res_sym.eql?(:email_notifications)
467
- case res_command
468
- when :create, :modify, :delete, :show
469
- return entity_execute(command: res_command, **exec_args) do |field, value|
470
- @api_v5.lookup_entity_by_field(entity: exec_args[:entity], value: value, field: field, items_key: exec_args[:items_key], query: res_id_query)['id']
471
- end
472
- when :list
473
- data, total = exec_args[:api].list_entities_limit_offset_total_count(entity: exec_args[:entity], items_key: exec_args[:items_key], query: query_read_delete(default: exec_args[:list_query]))
474
- return Result::ObjectList.new(data, total: total, fields: exec_args[:display_fields])
475
- when :shared_folders
476
- # nodes
477
- node_id = options.instance_identifier do |field, value|
478
- @api_v5.lookup_entity_by_field(entity: 'nodes', field: field, value: value)['id']
483
+ # --- DSL ---
484
+
485
+ # Commands that need @api_v5 carry setup: :setup_api_v5.
486
+ # :health and :postprocessing work without authentication, so they have no setup.
487
+ command :health, description: 'Check Faspex 5 health'
488
+ command :version, description: 'Show Faspex 5 version', setup: :setup_api_v5, action: -> { Result::SingleObject.new(@api_v5.read('version')) }
489
+ command :bearer_token, description: 'Show OAuth bearer token', setup: :setup_api_v5, action: -> { Result::Text.new(@api_v5.oauth.authorization) }
490
+ command :packages, description: 'Manage packages', setup: :setup_api_v5
491
+ commands_under :packages do
492
+ command :list, description: 'List packages',
493
+ arguments: [{name: :filter, mandatory: false, default: nil, type: Proc}]
494
+ command :send, description: 'Send a package', transfer_paths: :send,
495
+ arguments: [{name: :data, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::FASPEX, 'packages.post')}],
496
+ action: ->(data:, **) { package_send(data) }
497
+ command :show, description: 'Show a package', setup: :setup_package_id,
498
+ action: ->(package_id:, **) { Result::SingleObject.new(@api_v5.read("packages/#{package_id}")) }
499
+ command :browse, description: 'Browse package files', setup: :setup_package_id,
500
+ arguments: [{name: :folder_path, type: String, mandatory: false, default: '/'}],
501
+ action: ->(folder_path:, package_id:, **) { browse_folder("packages/#{package_id}/files/#{Api::Faspex.box_type(options.get_option(:box))}", recipient_query(package_id), folder_path: folder_path) }
502
+ command :status, description: 'Wait for package status', setup: :setup_package_id,
503
+ arguments: [{name: :status_list, type: Array, mandatory: false, default: nil}],
504
+ action: ->(status_list:, package_id:, **) { Result::SingleObject.new(wait_package_status(package_id, status_list: status_list)) }
505
+ command :delete, description: 'Delete packages', setup: :setup_package_id
506
+ command :receive, description: 'Receive a package', setup: :setup_package_id, transfer_paths: :receive,
507
+ action: ->(package_id:, **) { package_receive(package_id) }
508
+ command(:file_processing, description: 'Show file processing status', setup: :setup_package_id, action: lambda do |package_id:, **|
509
+ result, count = @api_v5.list_entities_limit_offset_total_count(entity: "packages/#{package_id}/file_statuses", items_key: 'files')
510
+ Result::ObjectList.new(result, total: count)
511
+ end)
512
+ end
513
+ command :admin, description: 'Administer Faspex 5', setup: :setup_api_v5
514
+ command :user, description: 'Manage current user', setup: :setup_api_v5
515
+ command :shared_folders, description: 'Browse shared folders', setup: :setup_api_v5
516
+ command :gateway, description: 'Start Faspex 4 gateway emulation', setup: :setup_api_v5,
517
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
518
+ command :postprocessing, description: 'Start Faspex 4 post-processing server',
519
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
520
+ command :invitations, description: 'Manage invitations', setup: :setup_api_v5
521
+
522
+ commands_under :invitations do
523
+ command :create, description: 'Create an invitation',
524
+ arguments: [{name: :input_data, type: Hash, bulk: true}]
525
+ command :resend, description: 'Resend an invitation',
526
+ arguments: [{name: :invitation_id, type: :identifier}]
527
+ crud_commands entity: 'invitations',
528
+ api: :@api_v5,
529
+ operations: Operations::ALL - %i[create],
530
+ items_key: 'invitations',
531
+ display_fields: %w[id public recipient_type recipient_name email_address]
532
+ end
533
+
534
+ commands_under :user do
535
+ command :account, description: 'Show account information', action: -> { Result::SingleObject.new(@api_v5.read('account', query_read_delete)) }
536
+ command :profile, description: 'Manage user profile'
537
+ end
538
+
539
+ commands_under %i[user profile] do
540
+ command :show, description: 'Show user profile', action: -> { Result::SingleObject.new(@api_v5.read('account/preferences')) }
541
+ command(
542
+ :modify,
543
+ description: 'Modify user profile',
544
+ arguments: [{name: :properties, type: Hash}],
545
+ action: lambda do |properties:, **|
546
+ @api_v5.update('account/preferences', properties)
547
+ Result::Status.new('modified')
479
548
  end
480
- shared_folder_entity = "nodes/#{node_id}/shared_folders"
481
- sh_command = options.get_next_command(Operations::ALL + [:user])
482
- case sh_command
483
- when *Operations::ALL
484
- return entity_execute(
485
- api: @api_v5,
486
- entity: shared_folder_entity,
487
- command: sh_command
488
- ) do |field, value|
489
- @api_v5.lookup_entity_by_field(entity: shared_folder_entity, field: field, value: value)['id']
490
- end
491
- when :user
492
- sh_id = options.instance_identifier do |field, value|
493
- @api_v5.lookup_entity_by_field(entity: shared_folder_entity, field: field, value: value)['id']
549
+ )
550
+ end
551
+
552
+ commands_under :shared_folders do
553
+ command :list, description: 'List shared folders', action: -> { Result::ObjectList.new(@api_v5.read('shared_folders')['shared_folders']) }
554
+ command :browse, description: 'Browse a shared folder',
555
+ arguments: [{name: :shared_folder_id, type: :identifier, lookup: :lookup_shared_folder_id},
556
+ {name: :folder_path, type: String, mandatory: false, default: '/'}]
557
+ end
558
+
559
+ # admin sub-tree: fixed commands + all ADMIN_RESOURCES with their sub-commands
560
+ commands_under :admin do
561
+ command :configuration, description: 'Manage Faspex 5 configuration'
562
+ command :smtp, description: 'Manage SMTP configuration'
563
+ command :events, description: 'List events'
564
+ command :clean_deleted, description: 'Clean deleted packages',
565
+ arguments: [{name: :input_data, type: Hash, mandatory: false, default: {}}]
566
+ Api::Faspex::ADMIN_RESOURCES.each do |res|
567
+ cfg = RESOURCE_CONFIG.fetch(res, {})
568
+ extra = cfg[:extra_commands] || []
569
+ ia_cmds = cfg[:instance_arg_commands] || {}
570
+ is_singleton = cfg[:is_singleton] || false
571
+ entity_path = cfg[:entity] || res.to_s
572
+ crud_ops = ((cfg[:commands] || Operations::ALL) - %i[list]) & Operations::ALL
573
+
574
+ command res, description: "Manage #{res.to_s.tr('_', ' ')}"
575
+ commands_under(res) do
576
+ # List is handled with Faspex 5 pagination (item_list_with_total)
577
+ unless is_singleton
578
+ list_kwargs = {description: 'List'}
579
+ list_kwargs[:query_schema] = Schema::Registry.query_params(cfg[:query_component], entity_path) if cfg[:query_component]
580
+ command :list, **list_kwargs
494
581
  end
495
- user_path = "#{shared_folder_entity}/#{sh_id}/custom_access_users"
496
- return entity_execute(api: @api_v5, entity: user_path, items_key: 'users') do |field, value|
497
- @api_v5.lookup_entity_by_field(entity: user_path, items_key: 'users', field: field, value: value)['id']
498
- end
499
582
 
583
+ # Standard CRUD operations handled via crud_commands
584
+ crud_commands(
585
+ api: -> { res_exec_args(res)[:api] },
586
+ entity: entity_path,
587
+ operations: crud_ops,
588
+ lookup: :"lookup_admin_#{res}_id",
589
+ is_singleton: is_singleton,
590
+ body_component: cfg[:body_component],
591
+ display_fields: cfg[:display_fields],
592
+ delete_style: cfg[:delete_style],
593
+ id_as_arg: cfg[:id_as_arg] || false
594
+ )
595
+
596
+ # Extra commands (e.g. browse, reset_password, next)
597
+ extra.each do |c|
598
+ ia = ia_cmds[c] || {}
599
+ command c, description: c.to_s.tr('_', ' ').capitalize, **ia
600
+ end
500
601
  end
501
- when :browse
502
- # nodes
503
- node_id = options.instance_identifier do |field, value|
504
- @api_v5.lookup_entity_by_field(entity: 'nodes', value: value, field: field)['id']
505
- end
506
- return browse_folder("nodes/#{node_id}/browse")
507
- when :invite_external_collaborator
508
- # :shared_inboxes, :workgroups
509
- shared_inbox_id = options.instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
510
- creation_payload = value_create_modify(command: res_command)
511
- result = @api_v5.create("#{res_sym}/#{shared_inbox_id}/external_collaborator", creation_payload)
512
- formatter.display_status(result['message'])
513
- result = @api_v5.lookup_entity_by_field(
514
- entity: "#{res_sym}/#{shared_inbox_id}/members",
515
- items_key: 'members',
516
- value: creation_payload['email_address'],
517
- query: {}
518
- )
519
- return Result::SingleObject.new(result)
520
- when :members, :saml_groups
521
- # res_command := :shared_inboxes, :workgroups
522
- res_id = options.instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: res_sym.to_s, field: field, value: value, query: res_id_query)['id']}
523
- res_path = "#{res_sym}/#{res_id}/#{res_command}"
524
- list_key = res_command.to_s
525
- list_key = 'groups' if res_command.eql?(:saml_groups)
526
- sub_command = options.get_next_command(%i[create list modify delete])
527
- if sub_command.eql?(:create) && res_command.eql?(:members)
528
- # first arg is one user name or list of users
529
- users = options.get_next_argument('user id, %name:, or Array')
530
- users = [users] unless users.is_a?(Array)
531
- users = users.map do |user|
532
- if (m = Manager.percent_selector(user))
533
- @api_v5.lookup_entity_by_field(
534
- entity: 'accounts',
535
- field: m[:field],
536
- value: m[:value],
537
- query: Rest.php_style({type: ACCOUNT_TYPES})
538
- )['id']
602
+ end
603
+ end
604
+
605
+ commands_under %i[admin nodes] do
606
+ command :shared_folders, description: 'Manage shared folders',
607
+ arguments: [{name: :node_id, type: :identifier, lookup: :lookup_node_id}],
608
+ setup: :setup_admin_nodes_shared_folders
609
+ end
610
+
611
+ # admin > nodes > shared_folders sub-tree
612
+ # entity: :sf_entity is resolved at runtime from ctx (injected by setup_admin_nodes_shared_folders).
613
+ commands_under %i[admin nodes shared_folders] do
614
+ crud_commands entity: :sf_entity,
615
+ api: :@api_v5,
616
+ name: 'shared folder',
617
+ lookup: :lookup_sf_id,
618
+ items_key: 'shared_folders'
619
+ command :user, description: 'Custom access users',
620
+ arguments: [{name: :sf_id, type: :identifier, lookup: :lookup_sf_id}],
621
+ setup: :setup_admin_nodes_shared_folders_user
622
+ end
623
+
624
+ # admin > nodes > shared_folders > user sub-tree (custom access users)
625
+ # entity: :user_path is resolved at runtime from ctx (injected by setup_admin_nodes_shared_folders_user).
626
+ commands_under %i[admin nodes shared_folders user] do
627
+ crud_commands entity: :user_path, api: :@api_v5, name: 'custom access user', lookup: :lookup_sf_user_id, items_key: 'users'
628
+ end
629
+
630
+ # `members` and `saml_groups`
631
+ CRUD_NO_SHOW = %i[create list modify delete].freeze
632
+ CRUD_NO_LIST = %i[create modify delete show].freeze
633
+
634
+ RES_SINGULAR = {shared_inboxes: :shared_inbox, workgroups: :workgroup}.freeze
635
+ private_constant :RES_SINGULAR
636
+
637
+ # admin > shared_inboxes|workgroups > members|saml_groups|invite_external_collaborator:
638
+ # res_id consumed via arguments:(:identifier) + lookup:, builds res_instance_path for all children
639
+ %i[shared_inboxes workgroups].each do |res|
640
+ lookup_res_id = ->(field, value, **) { @api_v5.lookup_entity_by_field(entity: res.to_s, field: field, value: value, query: {'all': true})['id'] }
641
+ commands_under [:admin, res] do
642
+ command :members, description: 'Members',
643
+ arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id}],
644
+ setup: :"setup_admin_#{res}_instance"
645
+ command :saml_groups, description: 'Saml groups',
646
+ arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id}],
647
+ setup: :"setup_admin_#{res}_instance"
648
+ command :invite_external_collaborator, description: 'Invite external collaborator',
649
+ arguments: [{name: :"#{RES_SINGULAR[res]}_id", type: :identifier, lookup: lookup_res_id},
650
+ {name: :input_data, type: Hash}]
651
+ end
652
+
653
+ commands_under [:admin, res, :members] do
654
+ CRUD_NO_SHOW.each do |c|
655
+ args =
656
+ if c.eql?(:create)
657
+ {arguments: [{name: :users, bulk: true}, {name: :access, mandatory: false, default: :standard}]}
658
+ elsif Operations::INSTANCE.include?(c)
659
+ {arguments: [{name: :member_id, type: :identifier, lookup: :"lookup_#{res}_members_id"}]}
539
660
  else
540
- # it's the user id (not member id...)
541
- user
661
+ {}
542
662
  end
543
- end
544
- access = options.get_next_argument('level', mandatory: false, accept_list: SHARED_INBOX_MEMBER_LEVELS, default: :standard)
545
- options.unshift_next_argument({user: users.map{ |u| {id: u, access: access}}})
663
+ command c, description: c.to_s.capitalize, **args
546
664
  end
547
- # TODO: test SAML group
548
- return entity_execute(
549
- api: @api_v5,
550
- entity: res_path,
551
- command: sub_command,
552
- items_key: list_key
553
- ) do |field, value|
554
- @api_v5.lookup_entity_by_field(
555
- entity: res_path,
556
- field: field,
557
- value: value,
558
- query: Rest.php_style({type: %w[user]})
559
- )['user_id']
560
- end
561
- when :reset_password
562
- # :accounts
563
- contact_id = options.instance_identifier{ |field, value| @api_v5.lookup_entity_by_field(entity: 'accounts', field: field, value: value, query: res_id_query)['id']}
564
- @api_v5.create("accounts/#{contact_id}/reset_password", {})
565
- return Result::Status.new('password reset, user shall check email')
566
- when :next
567
- result, count = @api_v5.list_entities_limit_offset_total_count(
568
- entity: exec_args[:entity],
569
- operation: 'POST',
570
- items_key: 'files'
571
- )
572
- return Result::ObjectList.new(result, total: count)
573
- end
574
- Aspera.error_unreachable_line
575
- end
576
-
577
- def execute_admin
578
- command = options.get_next_command(%i[configuration smtp events clean_deleted].concat(Api::Faspex::ADMIN_RESOURCES).freeze)
579
- case command
580
- when *Api::Faspex::ADMIN_RESOURCES
581
- return execute_resource(command)
582
- when :clean_deleted
583
- delete_data = value_create_modify(command: command, default: {})
584
- delete_data = @api_v5.read('configuration').slice('days_before_deleting_package_records') if delete_data.empty?
585
- res = @api_v5.create('internal/packages/clean_deleted', delete_data)
586
- return Result::SingleObject.new(res)
587
- when :events
588
- event_type = options.get_next_command(%i[application webhook])
589
- case event_type
590
- when :application
591
- list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'application_events', query: query_read_delete)
592
- return Result::ObjectList.new(list, total: total, fields: %w[event_type created_at application user.name])
593
- when :webhook
594
- list, total = @api_v5.list_entities_limit_offset_total_count(
595
- entity: 'all_webhooks_events',
596
- query: query_read_delete,
597
- items_key: 'events'
598
- )
599
- return Result::ObjectList.new(list, total: total)
665
+ end
666
+
667
+ commands_under [:admin, res, :saml_groups] do
668
+ crud_commands entity: :saml_groups_path,
669
+ api: :@api_v5,
670
+ name: 'SAML group',
671
+ operations: CRUD_NO_SHOW,
672
+ lookup: :"lookup_#{res}_saml_groups_id",
673
+ items_key: 'groups'
674
+ end
675
+ end
676
+
677
+ commands_under %i[admin configuration] do
678
+ command :show, description: 'Show configuration', action: -> { Result::SingleObject.new(@api_v5.read('configuration')) }
679
+ command :modify, description: 'Modify configuration',
680
+ arguments: [{name: :input_data, type: Hash}],
681
+ action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.update('configuration', input_data)) }
682
+ end
683
+
684
+ commands_under %i[admin smtp] do
685
+ command :show, description: 'Show SMTP configuration', action: -> { Result::SingleObject.new(@api_v5.read('configuration/smtp')) }
686
+ command :create, description: 'Create SMTP configuration',
687
+ arguments: [{name: :input_data, type: Hash}],
688
+ action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.create('configuration/smtp', input_data)) }
689
+ command :modify, description: 'Modify SMTP configuration',
690
+ arguments: [{name: :input_data, type: Hash}],
691
+ action: ->(input_data:, **) { Result::SingleObject.new(@api_v5.update('configuration/smtp', input_data)) }
692
+ command(
693
+ :delete, description: 'Delete SMTP configuration',
694
+ action: lambda do
695
+ @api_v5.delete('configuration/smtp')
696
+ Result::Status.new('SMTP configuration deleted')
600
697
  end
601
- when :configuration
602
- conf_path = 'configuration'
603
- conf_cmd = options.get_next_command(%i[show modify])
604
- case conf_cmd
605
- when :show
606
- return Result::SingleObject.new(@api_v5.read(conf_path))
607
- when :modify
608
- return Result::SingleObject.new(@api_v5.update(conf_path, value_create_modify(command: conf_cmd)))
698
+ )
699
+ command :test, description: 'Test SMTP configuration',
700
+ arguments: [{name: :test_data, type: nil}]
701
+ end
702
+
703
+ commands_under %i[admin events] do
704
+ command(
705
+ :application, description: 'List application events',
706
+ action: lambda do
707
+ list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'application_events', query: query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'application_events')))
708
+ Result::ObjectList.new(list, total: total, fields: %w[event_type created_at application user.name])
609
709
  end
610
- when :smtp
611
- # only one SMTP config
612
- smtp_path = 'configuration/smtp'
613
- smtp_cmd = options.get_next_command(%i[show create modify delete test])
614
- case smtp_cmd
615
- when :show
616
- return Result::SingleObject.new(@api_v5.read(smtp_path))
617
- when :create
618
- return Result::SingleObject.new(@api_v5.create(smtp_path, value_create_modify(command: smtp_cmd)))
619
- when :modify
620
- return Result::SingleObject.new(@api_v5.update(smtp_path, value_create_modify(command: smtp_cmd)))
621
- when :delete
622
- @api_v5.delete(smtp_path)
623
- return Result::Status.new('SMTP configuration deleted')
624
- when :test
625
- test_data = options.get_next_argument('Email or test data, see API')
626
- test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
627
- creation = @api_v5.create(File.join(smtp_path, 'test'), test_data)
628
- result = wait_for_job(creation['job_id'])
629
- result['serialized_args'] = JSON.parse(result['serialized_args']) rescue result['serialized_args']
630
- return Result::SingleObject.new(result)
710
+ )
711
+ command(
712
+ :webhook, description: 'List webhook events',
713
+ action: lambda do
714
+ list, total = @api_v5.list_entities_limit_offset_total_count(entity: 'all_webhooks_events', query: query_read_delete(schema: Schema::Registry.query_params(Schema::Registry::FASPEX, 'all_webhooks_events')), items_key: 'events')
715
+ Result::ObjectList.new(list, total: total)
631
716
  end
717
+ )
718
+ end
719
+
720
+ # admin > clean_deleted handler (leaf, no sub-commands)
721
+ define_action_method(%i[admin clean_deleted]) do |input_data: {}, **|
722
+ input_data = @api_v5.read('configuration').slice('days_before_deleting_package_records') if input_data.empty?
723
+ Result::SingleObject.new(@api_v5.create('internal/packages/clean_deleted', input_data))
724
+ end
725
+
726
+ # admin > <resource> > list
727
+ Api::Faspex::ADMIN_RESOURCES.each do |res|
728
+ define_action_method([:admin, res, :list]) do
729
+ args = res_exec_args(res)
730
+ # Special case: email_notifications list returns a fixed value list
731
+ next Result::ValueList.new(Api::Faspex::EMAIL_NOTIF_LIST, name: 'email_id') if res.eql?(:email_notifications)
732
+ data, total = args[:api].list_entities_limit_offset_total_count(
733
+ entity: args[:entity], items_key: args[:items_key],
734
+ query: query_read_delete(
735
+ default: args[:list_query],
736
+ schema: args[:query_component] ? Schema::Registry.query_params(args[:query_component], args[:entity]) : nil
737
+ )
738
+ )
739
+ Result::ObjectList.new(data, total: total, fields: args[:display_fields])
632
740
  end
633
741
  end
634
742
 
635
- ACTIONS = %i[health version user bearer_token packages shared_folders admin gateway postprocessing invitations].freeze
743
+ def action_admin_smtp_test(test_data:, **)
744
+ test_data = {test_email_recipient: test_data} if test_data.is_a?(String)
745
+ creation = @api_v5.create('configuration/smtp/test', test_data)
746
+ result = wait_for_job(creation['job_id'])
747
+ begin
748
+ result['serialized_args'] = JSON.parse(result['serialized_args'])
749
+ rescue JSON::ParserError
750
+ # keep as string if not valid JSON
751
+ end
752
+ Result::SingleObject.new(result)
753
+ end
754
+
755
+ # admin > accounts > reset_password
756
+ def action_admin_accounts_reset_password(contact_id:, **)
757
+ @api_v5.create("accounts/#{contact_id}/reset_password", {})
758
+ Result::Status.new('password reset, user shall check email')
759
+ end
636
760
 
637
- def execute_action
638
- command = options.get_next_command(ACTIONS)
639
- unless %i{postprocessing health}.include?(command)
640
- # create an API object with the same options, but with a different subpath
641
- @api_v5 = Api::Faspex.new(**Oauth.kwargs_from_options(options))
642
- # in case user wants to use HTTPGW tell transfer agent how to get address
643
- transfer.httpgw_url_cb = lambda{@api_v5.read('account')['gateway_url']}
761
+ # admin > file_processing > next
762
+ def action_admin_file_processing_next
763
+ args = res_exec_args(:file_processing)
764
+ result, count = @api_v5.list_entities_limit_offset_total_count(entity: args[:entity], operation: 'POST', items_key: 'files')
765
+ Result::ObjectList.new(result, total: count)
766
+ end
767
+
768
+ # admin > nodes > browse
769
+ def action_admin_nodes_browse(folder_path:, node_id:, **)
770
+ browse_folder("nodes/#{node_id}/browse", {}, folder_path: folder_path)
771
+ end
772
+
773
+ # admin > nodes > shared_folders — node_id: already in ctx via arguments: on the :shared_folders command
774
+ def setup_admin_nodes_shared_folders(node_id:, **)
775
+ {sf_entity: "nodes/#{node_id}/shared_folders"}
776
+ end
777
+
778
+ # Lookup shared folder id by field/value within a node's shared_folders entity.
779
+ # sf_entity is in ctx from setup_admin_nodes_shared_folders (Phase A of parent).
780
+ def lookup_sf_id(field, value, sf_entity:, **)
781
+ @api_v5.lookup_entity_by_field(entity: sf_entity, items_key: 'shared_folders', field: field, value: value)['id']
782
+ end
783
+
784
+ # Lookup custom access user id by field/value within a shared folder's users entity.
785
+ # user_path is in ctx from setup_admin_nodes_shared_folders_user (Phase A of parent).
786
+ def lookup_sf_user_id(field, value, user_path:, **)
787
+ @api_v5.lookup_entity_by_field(entity: user_path, items_key: 'users', field: field, value: value)['id']
788
+ end
789
+
790
+ # admin > nodes > shared_folders > user — sf_id: already in ctx via arguments: on the :user command
791
+ def setup_admin_nodes_shared_folders_user(sf_entity:, sf_id:, **)
792
+ {user_path: "#{sf_entity}/#{sf_id}/custom_access_users"}
793
+ end
794
+
795
+ # admin > shared_inboxes|workgroups — res_id: already in ctx via arguments: on the :members|:saml_groups command
796
+ %i[shared_inboxes workgroups].each do |res|
797
+ define_method(:"setup_admin_#{res}_instance") do |**kwargs|
798
+ res_id = kwargs[:"#{RES_SINGULAR[res]}_id"]
799
+ {
800
+ res_instance_path: "#{res}/#{res_id}",
801
+ saml_groups_path: "#{res}/#{res_id}/saml_groups"
802
+ }
644
803
  end
645
- case command
646
- when :version
647
- return Result::SingleObject.new(@api_v5.read('version'))
648
- when :health
649
- nagios = Nagios.new
650
- begin
651
- data, http = Rest.new(base_url: options.get_option(:url, mandatory: true))
652
- .read('health', ret: :both)
653
- data.each do |k, v|
654
- nagios.add_ok(k, v.to_s)
655
- end
656
- nagios.add_ok('version', http['X-IBM-Aspera']) if http['X-IBM-Aspera']
657
- rescue StandardError => e
658
- nagios.add_critical('core', e.to_s)
659
- end
660
- Result::ObjectList.new(nagios.status_list)
661
- when :user
662
- case options.get_next_command(%i[account profile])
663
- when :account
664
- return Result::SingleObject.new(@api_v5.read('account', query_read_delete))
665
- when :profile
666
- case options.get_next_command(%i[show modify])
667
- when :show
668
- return Result::SingleObject.new(@api_v5.read('account/preferences'))
669
- when :modify
670
- @api_v5.update('account/preferences', options.get_next_argument('modified parameters', validation: Hash))
671
- return Result::Status.new('modified')
672
- end
673
- end
674
- when :bearer_token
675
- return Result::Text.new(@api_v5.oauth.authorization)
676
- when :packages
677
- return package_action
678
- when :shared_folders
679
- all_shared_folders = @api_v5.read('shared_folders')['shared_folders']
680
- case options.get_next_command(%i[list browse])
681
- when :list
682
- return Result::ObjectList.new(all_shared_folders)
683
- when :browse
684
- shared_folder_id = options.instance_identifier do |field, value|
685
- matches = all_shared_folders.select{ |i| i[field].eql?(value)}
686
- raise "no match for #{field} = #{value}" if matches.empty?
687
- raise "multiple matches for #{field} = #{value}" if matches.length > 1
688
- matches.first['id']
689
- end
690
- node = all_shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
691
- raise "No such shared folder id #{shared_folder_id}" if node.nil?
692
- return browse_folder("nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse")
693
- end
694
- when :admin
695
- return execute_admin
696
- when :invitations
697
- invitation_endpoint = 'invitations'
698
- invitation_command = options.get_next_command(%i[resend].concat(Operations::ALL))
699
- case invitation_command
700
- when :create
701
- return do_bulk_operation(command: invitation_command, descr: 'data') do |params|
702
- invitation_endpoint = params.key?('recipient_name') ? 'public_invitations' : 'invitations'
703
- @api_v5.create(invitation_endpoint, params)
704
- end
705
- when :resend
706
- @api_v5.create("#{invitation_endpoint}/#{options.instance_identifier}/resend", nil)
707
- return Result::Status.new('Invitation resent')
804
+ end
805
+
806
+ # Resolve user ids for shared_inbox/workgroup members/create, handling percent selectors.
807
+ # @param users [Array] raw user ids or percent-selector strings
808
+ # @return [Array<String>] resolved user ids
809
+ def resolve_member_user_ids(users)
810
+ users = [users] unless users.is_a?(Array)
811
+ users.map do |user|
812
+ if (m = Parser.percent_selector(user))
813
+ @api_v5.lookup_entity_by_field(entity: 'accounts', field: m[:field], value: m[:value], query: Rest.php_style({type: ACCOUNT_TYPES}))['id']
708
814
  else
709
- return entity_execute(
710
- api: @api_v5,
711
- entity: invitation_endpoint,
712
- command: invitation_command,
713
- items_key: invitation_endpoint,
714
- display_fields: %w[id public recipient_type recipient_name email_address]
715
- ) do |field, value|
716
- @api_v5.lookup_entity_by_field(entity: invitation_endpoint, field: field, value: value, query: {})['id']
717
- end
815
+ user
718
816
  end
719
- when :gateway
720
- require 'aspera/faspex_gw'
721
- parameters = value_create_modify(command: command, default: {}).symbolize_keys
722
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
723
- server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
724
- Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?){"unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}"}
725
- server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
726
- server.start
727
- return Result::Status.new('Gateway terminated')
728
- when :postprocessing
729
- require 'aspera/faspex_postproc' # cspell:disable-line
730
- parameters = value_create_modify(command: command, default: {}).symbolize_keys
731
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
732
- parameters[:root] = uri.path
733
- server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
734
- server.mount(uri.path, Faspex4PostProcServlet, parameters.except(*WebServerSimple::PARAMS))
735
- server.start
736
- return Result::Status.new('Gateway terminated')
737
817
  end
738
818
  end
819
+
820
+ # Lookup methods for members/saml_groups identity resolution (used by arguments: lookup:)
821
+ # res_instance_path is in ctx from setup_admin_{res}_instance (Phase A of parent).
822
+ %i[shared_inboxes workgroups].each do |res|
823
+ define_method(:"lookup_#{res}_members_id") do |field, value, res_instance_path:, **|
824
+ res_path = "#{res_instance_path}/members"
825
+ @api_v5.lookup_entity_by_field(entity: res_path, field: field, value: value, query: Rest.php_style({type: %w[user]}))['user_id']
826
+ end
827
+ define_method(:"lookup_#{res}_saml_groups_id") do |field, value, res_instance_path:, **|
828
+ res_path = "#{res_instance_path}/saml_groups"
829
+ @api_v5.lookup_entity_by_field(entity: res_path, field: field, value: value, query: Rest.php_style({type: %w[user]}))['user_id']
830
+ end
831
+ end
832
+
833
+ # admin > shared_inboxes|workgroups > members > create/list/modify/delete
834
+ # (saml_groups CRUD is handled by crud_commands)
835
+ %i[shared_inboxes workgroups].each do |res|
836
+ define_action_method([:admin, res, :members, :list]) do |res_instance_path:, **|
837
+ entity_list(api: @api_v5, entity: "#{res_instance_path}/members", items_key: 'members')
838
+ end
839
+
840
+ define_action_method([:admin, res, :members, :modify]) do |res_instance_path:, **kwargs|
841
+ entity_modify(api: @api_v5, entity: "#{res_instance_path}/members", id: kwargs[:member_id])
842
+ end
843
+
844
+ define_action_method([:admin, res, :members, :delete]) do |res_instance_path:, **kwargs|
845
+ entity_delete(api: @api_v5, entity: "#{res_instance_path}/members", id: kwargs[:member_id])
846
+ end
847
+
848
+ define_action_method([:admin, res, :members, :create]) do |users:, access:, res_instance_path:, **|
849
+ res_path = "#{res_instance_path}/members"
850
+ resolved = resolve_member_user_ids(users)
851
+ input_data = [{user: resolved.map { |u| {id: u, access: access} }}]
852
+ entity_create(api: @api_v5, entity: res_path, input_data: input_data)
853
+ end
854
+ end
855
+
856
+ # admin > shared_inboxes|workgroups > invite_external_collaborator
857
+ %i[shared_inboxes workgroups].each do |res|
858
+ define_action_method([:admin, res, :invite_external_collaborator]) do |input_data:, **kwargs|
859
+ res_instance_path = "#{res}/#{kwargs[:"#{RES_SINGULAR[res]}_id"]}"
860
+ result = @api_v5.create("#{res_instance_path}/external_collaborator", input_data)
861
+ formatter.display_status(result['message'])
862
+ Result::SingleObject.new(@api_v5.lookup_entity_by_field(entity: "#{res_instance_path}/members", items_key: 'members', value: input_data['email_address'], query: {}))
863
+ end
864
+ end
865
+
866
+ # --- root setup ---
867
+
868
+ # Build @api_v5 for all commands that need it (all except :health and :postprocessing).
869
+ # @return [Hash] empty ctx (state stored in @api_v5)
870
+ def setup_api_v5(**)
871
+ return {} if @api_v5
872
+ @api_v5 = Api::Faspex.new(**Oauth.kwargs_from_options(options))
873
+ # in case user wants to use HTTPGW tell transfer agent how to get address
874
+ transfer.httpgw_url_cb = lambda { @api_v5.read('account')['gateway_url'] }
875
+ {}
876
+ end
877
+
878
+ # Setup for package sub-commands that need an id: resolves package_id from pub_link or argument.
879
+ # @return [Hash] ctx key: package_id
880
+ def setup_package_id(**)
881
+ package_id = @api_v5.pub_link_context&.key?('package_id') ? @api_v5.pub_link_context['package_id'] : options.instance_identifier
882
+ {package_id: package_id}
883
+ end
884
+
885
+ def action_packages_list(filter: nil, **)
886
+ list, max_items, total = list_packages_with_filter(filter: filter)
887
+ list = list[0, max_items] if max_items
888
+ fields = %w[id title status sender.name recipients.0.name release_date total_bytes total_files]
889
+ fields.delete('recipients.0.name') if %w[inbox inbox_history].include?(options.get_option(:box))
890
+ fields.delete('sender.name') if %w[outbox outbox_history].include?(options.get_option(:box))
891
+ Result::ObjectList.new(list, total: total, fields: fields)
892
+ end
893
+
894
+ def action_packages_delete(package_id:, **)
895
+ ids = package_id.is_a?(Array) ? package_id : [package_id]
896
+ Aspera.assert_array_all(ids, String) { 'Package id(s)' }
897
+ # API returns 204, empty on success
898
+ @api_v5.call(
899
+ operation: 'DELETE',
900
+ subpath: 'packages',
901
+ content_type: Mime::JSON,
902
+ body: {ids: ids},
903
+ headers: {'Accept' => Mime::JSON}
904
+ )
905
+ Result::Status.new('Package(s) deleted')
906
+ end
907
+
908
+ # --- handlers ---
909
+
910
+ def action_health
911
+ nagios = Nagios.new
912
+ begin
913
+ data, http = Rest.new(base_url: options.get_option(:url, mandatory: true))
914
+ .read('health', ret: :both)
915
+ data.each do |k, v|
916
+ nagios.add_ok(k, v.to_s)
917
+ end
918
+ nagios.add_ok('version', http['X-IBM-Aspera']) if http['X-IBM-Aspera']
919
+ rescue StandardError => e
920
+ nagios.add_critical('core', e.to_s)
921
+ end
922
+ Result::ObjectList.new(nagios.status_list)
923
+ end
924
+
925
+ # Lookup a shared folder id by field/value.
926
+ # Called via lookup: :lookup_shared_folder_id on the shared_folders > browse command.
927
+ def lookup_shared_folder_id(field, value, **)
928
+ all = @api_v5.read('shared_folders')['shared_folders']
929
+ matches = all.select { |i| i[field].eql?(value) }
930
+ Aspera.assert(!matches.empty?) { "no match for #{field} = #{value}" }
931
+ Aspera.assert(matches.length == 1) { "multiple matches for #{field} = #{value}" }
932
+ matches.first['id']
933
+ end
934
+
935
+ def action_shared_folders_browse(folder_path:, shared_folder_id:, **)
936
+ all_shared_folders = @api_v5.read('shared_folders')['shared_folders']
937
+ node = all_shared_folders.find { |i| i['id'].eql?(shared_folder_id) }
938
+ Aspera.assert(!node.nil?) { "No such shared folder id #{shared_folder_id}" }
939
+ browse_folder("nodes/#{node['node_id']}/shared_folders/#{shared_folder_id}/browse", {}, folder_path: folder_path)
940
+ end
941
+
942
+ # invitations sub-handlers
943
+
944
+ def action_invitations_resend(invitation_id:, **)
945
+ @api_v5.create("invitations/#{invitation_id}/resend", nil)
946
+ Result::Status.new('Invitation resent')
947
+ end
948
+
949
+ def action_invitations_create(input_data:, **)
950
+ bulk_result(input_data, command: :create) do |params|
951
+ endpoint = params.key?('recipient_name') ? 'public_invitations' : 'invitations'
952
+ @api_v5.create(endpoint, params)
953
+ end
954
+ end
955
+
956
+ def action_gateway(parameters: {}, **)
957
+ require 'aspera/faspex_gw'
958
+ parameters = parameters.symbolize_keys
959
+ uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
960
+ server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
961
+ Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?) { "unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}" }
962
+ server.mount(uri.path, Faspex4GWServlet, @api_v5, nil)
963
+ server.start
964
+ Result::Status.new('Gateway terminated')
965
+ end
966
+
967
+ def action_postprocessing(parameters: {}, **)
968
+ require 'aspera/faspex_postproc' # cspell:disable-line
969
+ parameters = parameters.symbolize_keys
970
+ uri = URI.parse(parameters.delete(:url) { WebServerSimple::DEFAULT_URL })
971
+ parameters[:root] = uri.path
972
+ server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
973
+ server.mount(uri.path, Faspex4PostProcServlet, parameters.except(*WebServerSimple::PARAMS))
974
+ server.start
975
+ Result::Status.new('Gateway terminated')
976
+ end
739
977
  SHARED_INBOX_MEMBER_LEVELS = %i[submit_only standard shared_inbox_admin].freeze
740
978
  ACCOUNT_TYPES = %w{local_user saml_user self_registered_user external_user}.freeze
741
979
  WORKGROUP_TYPES = %w{workgroup shared_inbox}.freeze
742
980
  CONTACT_TYPES = (WORKGROUP_TYPES + %w{distribution_list user external_user}).freeze
743
981
  PACKAGE_RECIPIENT_TYPES = %i{recipients private_recipients notified_on_upload notified_on_download notified_on_receipt}
744
- private_constant :SHARED_INBOX_MEMBER_LEVELS, :ACCOUNT_TYPES, :CONTACT_TYPES, :PACKAGE_RECIPIENT_TYPES
982
+ private_constant :SHARED_INBOX_MEMBER_LEVELS, :ACCOUNT_TYPES, :CONTACT_TYPES, :PACKAGE_RECIPIENT_TYPES,
983
+ :CRUD_NO_SHOW, :CRUD_NO_LIST
745
984
  end
746
985
  end
747
986
  end