aspera-cli 4.26.1 → 4.27.0

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 (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +300 -22
  4. data/CONTRIBUTING.md +100 -12
  5. data/bin/ascli +5 -5
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +27 -25
  12. data/lib/aspera/agent/factory.rb +4 -0
  13. data/lib/aspera/agent/httpgw.rb +2 -2
  14. data/lib/aspera/agent/node.rb +34 -2
  15. data/lib/aspera/agent/transferd.rb +39 -3
  16. data/lib/aspera/api/alee.rb +1 -1
  17. data/lib/aspera/api/aoc.rb +56 -51
  18. data/lib/aspera/api/cos_node.rb +2 -2
  19. data/lib/aspera/api/faspex.rb +22 -20
  20. data/lib/aspera/api/httpgw.rb +4 -4
  21. data/lib/aspera/api/node.rb +33 -30
  22. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  23. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  24. data/lib/aspera/ascmd.rb +25 -23
  25. data/lib/aspera/ascp/installation.rb +43 -40
  26. data/lib/aspera/ascp/management.rb +2 -3
  27. data/lib/aspera/cli/ascp_actions.rb +155 -0
  28. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  29. data/lib/aspera/cli/bootstrapper.rb +197 -0
  30. data/lib/aspera/cli/command_registry.rb +145 -0
  31. data/lib/aspera/cli/command_spec.rb +138 -0
  32. data/lib/aspera/cli/context.rb +71 -0
  33. data/lib/aspera/cli/error.rb +16 -0
  34. data/lib/aspera/cli/extended_value.rb +5 -3
  35. data/lib/aspera/cli/formatter.rb +122 -209
  36. data/lib/aspera/cli/gem_checker.rb +65 -0
  37. data/lib/aspera/cli/hints.rb +2 -2
  38. data/lib/aspera/cli/http.rb +218 -0
  39. data/lib/aspera/cli/info.rb +2 -0
  40. data/lib/aspera/cli/mailer.rb +97 -0
  41. data/lib/aspera/cli/mcp_tool.rb +198 -0
  42. data/lib/aspera/cli/options.schema.yaml +364 -7
  43. data/lib/aspera/cli/{manager.rb → parser.rb} +337 -144
  44. data/lib/aspera/cli/plugins/alee.rb +20 -22
  45. data/lib/aspera/cli/plugins/aoc.rb +1173 -849
  46. data/lib/aspera/cli/plugins/ats.rb +200 -161
  47. data/lib/aspera/cli/plugins/base.rb +470 -107
  48. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  49. data/lib/aspera/cli/plugins/config.rb +434 -1032
  50. data/lib/aspera/cli/plugins/console.rb +106 -64
  51. data/lib/aspera/cli/plugins/cos.rb +44 -32
  52. data/lib/aspera/cli/plugins/factory.rb +7 -4
  53. data/lib/aspera/cli/plugins/faspex.rb +296 -259
  54. data/lib/aspera/cli/plugins/faspex5.rb +592 -344
  55. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  56. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  57. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  58. data/lib/aspera/cli/plugins/node.rb +1001 -797
  59. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  60. data/lib/aspera/cli/plugins/orchestrator.rb +111 -139
  61. data/lib/aspera/cli/plugins/preview.rb +183 -142
  62. data/lib/aspera/cli/plugins/server.rb +125 -80
  63. data/lib/aspera/cli/plugins/shares.rb +301 -107
  64. data/lib/aspera/cli/preset_actions.rb +139 -0
  65. data/lib/aspera/cli/preset_manager.rb +236 -0
  66. data/lib/aspera/cli/result.rb +360 -0
  67. data/lib/aspera/cli/runner.rb +473 -0
  68. data/lib/aspera/cli/secret_finder.rb +40 -0
  69. data/lib/aspera/cli/special_values.rb +1 -0
  70. data/lib/aspera/cli/sync_actions.rb +84 -56
  71. data/lib/aspera/cli/terminal_formatter.rb +65 -0
  72. data/lib/aspera/cli/transfer_actions.rb +83 -0
  73. data/lib/aspera/cli/transfer_agent.rb +116 -51
  74. data/lib/aspera/cli/transfer_progress.rb +9 -9
  75. data/lib/aspera/cli/vault_manager.rb +57 -0
  76. data/lib/aspera/cli/version.rb +1 -1
  77. data/lib/aspera/cli/wizard.rb +26 -26
  78. data/lib/aspera/command_line_builder.rb +23 -23
  79. data/lib/aspera/data_repository.rb +6 -1
  80. data/lib/aspera/dot_container.rb +5 -5
  81. data/lib/aspera/environment.rb +39 -13
  82. data/lib/aspera/exec_spec.rb +13 -0
  83. data/lib/aspera/faspex_gw.rb +2 -2
  84. data/lib/aspera/faspex_postproc.rb +4 -3
  85. data/lib/aspera/graphql.rb +35 -0
  86. data/lib/aspera/hash_ext.rb +6 -0
  87. data/lib/aspera/json_rpc/client.rb +62 -0
  88. data/lib/aspera/json_rpc/version.rb +7 -0
  89. data/lib/aspera/keychain/base.rb +1 -1
  90. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  91. data/lib/aspera/keychain/factory.rb +4 -4
  92. data/lib/aspera/keychain/macos_security.rb +4 -4
  93. data/lib/aspera/link_header.rb +82 -0
  94. data/lib/aspera/log.rb +23 -5
  95. data/lib/aspera/markdown.rb +85 -2
  96. data/lib/aspera/nagios.rb +2 -2
  97. data/lib/aspera/node_simulator.rb +3 -1
  98. data/lib/aspera/oauth/base.rb +10 -10
  99. data/lib/aspera/oauth/boot.rb +43 -0
  100. data/lib/aspera/oauth/factory.rb +41 -15
  101. data/lib/aspera/oauth/jwt.rb +4 -4
  102. data/lib/aspera/oauth/web.rb +4 -4
  103. data/lib/aspera/oauth.rb +1 -0
  104. data/lib/aspera/persistency_action_once.rb +10 -10
  105. data/lib/aspera/persistency_folder.rb +10 -3
  106. data/lib/aspera/preview/file_types.rb +7 -3
  107. data/lib/aspera/preview/generator.rb +3 -3
  108. data/lib/aspera/preview/utils.rb +1 -1
  109. data/lib/aspera/products/connect.rb +4 -1
  110. data/lib/aspera/products/other.rb +1 -1
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -9
  113. data/lib/aspera/rest.rb +18 -40
  114. data/lib/aspera/rest_error_analyzer.rb +7 -3
  115. data/lib/aspera/rest_errors_aspera.rb +0 -10
  116. data/lib/aspera/rest_list.rb +13 -6
  117. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +14 -4
  118. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +2730 -648
  119. data/lib/aspera/schema/async_tables.yaml +361 -0
  120. data/lib/aspera/schema/documentation.rb +57 -26
  121. data/lib/aspera/schema/reader.rb +60 -11
  122. data/lib/aspera/schema/registry.rb +50 -10
  123. data/lib/aspera/secret_hider.rb +11 -2
  124. data/lib/aspera/ssl.rb +4 -4
  125. data/lib/aspera/sync/conf.schema.yaml +2 -2
  126. data/lib/aspera/sync/database.rb +14 -10
  127. data/lib/aspera/sync/operations.rb +15 -18
  128. data/lib/aspera/temp_file_manager.rb +5 -1
  129. data/lib/aspera/timer_limiter.rb +1 -1
  130. data/lib/aspera/transfer/faux_file.rb +24 -11
  131. data/lib/aspera/transfer/parameters.rb +27 -25
  132. data/lib/aspera/transfer/result.rb +74 -0
  133. data/lib/aspera/transfer/resumer.rb +7 -7
  134. data/lib/aspera/transfer/spec.rb +18 -1
  135. data/lib/aspera/transfer/spec.schema.yaml +26 -8
  136. data/lib/aspera/transfer/uri.rb +1 -1
  137. data/lib/aspera/uri_reader.rb +2 -2
  138. data/lib/aspera/web_auth.rb +2 -2
  139. data/lib/aspera/web_server_simple.rb +8 -5
  140. data/lib/aspera/yaml.rb +2 -1
  141. data.tar.gz.sig +0 -0
  142. metadata +45 -16
  143. metadata.gz.sig +0 -0
  144. data/lib/aspera/cli/main.rb +0 -480
  145. data/lib/aspera/json_rpc.rb +0 -51
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'aspera/schema/registry'
3
4
  require 'aspera/cli/plugins/oauth'
4
5
  require 'aspera/cli/plugins/node'
5
6
  require 'aspera/cli/plugins/ats'
@@ -7,12 +8,14 @@ require 'aspera/cli/transfer_agent'
7
8
  require 'aspera/cli/special_values'
8
9
  require 'aspera/cli/wizard'
9
10
  require 'aspera/agent/node'
11
+ require 'aspera/transfer/result'
10
12
  require 'aspera/transfer/spec'
11
13
  require 'aspera/api/aoc'
12
14
  require 'aspera/api/node'
13
15
  require 'aspera/persistency_action_once'
14
16
  require 'aspera/id_generator'
15
17
  require 'aspera/assert'
18
+ require 'aspera/graphql'
16
19
  require 'securerandom'
17
20
  require 'date'
18
21
 
@@ -27,12 +30,14 @@ module Aspera
27
30
  client
28
31
  client_access_key
29
32
  client_registration_token
33
+ configuration_policy
30
34
  contact
31
35
  dropbox
32
36
  dropbox_membership
33
37
  group
34
38
  group_membership
35
39
  kms_profile
40
+ network_policy
36
41
  node
37
42
  operation
38
43
  organization
@@ -54,11 +59,9 @@ module Aspera
54
59
  PACKAGE_LIST_DEFAULT_FIELDS = %w[id name created_at files_completed bytes_transferred].freeze
55
60
 
56
61
  private_constant :REDIRECT_LOCALHOST, :ADMIN_OBJECTS, :PACKAGE_RECEIVED_BASE_QUERY, :PACKAGE_LIST_DEFAULT_FIELDS
57
- class << self
58
- def application_name
59
- 'Aspera on Cloud'
60
- end
62
+ application_name 'Aspera on Cloud'
61
63
 
64
+ class << self
62
65
  # @return [Hash,NilClass]
63
66
  def detect(base_url)
64
67
  # no protocol ?
@@ -97,10 +100,10 @@ module Aspera
97
100
  # or a sequential number if extension == :seq
98
101
  # @param package_info [Hash] Package information
99
102
  # @param destination_folder [String] Base folder
100
- # @param fld. [Array] List of fields of package
103
+ # @param fld [Array] List of fields of package
101
104
  def unique_folder(package_info, destination_folder, fld: nil, seq: false, opt: false)
102
105
  Aspera.assert_array_all(fld, String, type: BadArgument){'fld'}
103
- Aspera.assert([1, 2].include?(fld.length)){'fld must have 1 or 2 elements'}
106
+ Aspera.assert_values(fld.length, [1, 2]){'fld length'}
104
107
  folder = Environment.instance.sanitized_filename(package_info[fld[0]])
105
108
  if seq
106
109
  folder = next_available_folder(folder, always: !opt)
@@ -108,7 +111,6 @@ module Aspera
108
111
  # NOTE: it might already exist
109
112
  folder = "#{folder}.#{Environment.instance.sanitized_filename(fld[1])}"
110
113
  end
111
- puts("sub= #{folder}")
112
114
  File.join(destination_folder, folder)
113
115
  end
114
116
  end
@@ -131,7 +133,7 @@ module Aspera
131
133
  test_args: 'organization'
132
134
  }
133
135
  end
134
- options.declare(:use_generic_client, 'Wizard: AoC: use global or org specific jwt client id', allowed: Allowed::TYPES_BOOLEAN, default: Api::AoC.saas_url?(app_url))
136
+ options.declare(:use_generic_client, description: 'Wizard: AoC: use global or org specific jwt client id', allowed: Allowed::TYPES_BOOLEAN, default: Api::AoC.saas_url?(app_url))
135
137
  options.parse_options!
136
138
  # make username mandatory for jwt, this triggers interactive input
137
139
  wiz_username = options.get_option(:username, mandatory: true)
@@ -178,7 +180,7 @@ module Aspera
178
180
  end
179
181
  myself = aoc_api.read('self')
180
182
  if auto_set_pub_key
181
- Aspera.assert(myself['public_key'].empty?, type: Error){'Public key is already set in profile (use --override=yes)'} unless option_override
183
+ Aspera.assert(myself['public_key'].empty?, 'Public key is already set in profile (use --override=yes)', type: Error) unless option_override
182
184
  formatter.display_status('Updating profile with the public key.')
183
185
  aoc_api.update("users/#{myself['id']}", {'public_key' => pub_key_pem})
184
186
  end
@@ -199,16 +201,17 @@ module Aspera
199
201
  }
200
202
  end
201
203
 
204
+ option :workspace, description: 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE
205
+ option :new_user_option, description: 'New user creation option for unknown package recipients', allowed: [Hash, NilClass]
206
+ option :validate_metadata, description: 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true
207
+ option :package_folder, schema: Schema::Registry::PACKAGE_FOLDER_OPTIONS
208
+
202
209
  def initialize(**_)
203
210
  super
204
211
  @cache_workspace_info = nil
205
212
  @cache_home_node_file = nil
206
213
  @cache_api_aoc = nil
207
214
  @scope = Api::AoC::Scope::USER
208
- options.declare(:workspace, 'Name of workspace', allowed: [String, NilClass], default: Api::AoC::DEFAULT_WORKSPACE)
209
- options.declare(:new_user_option, 'New user creation option for unknown package recipients', allowed: Hash)
210
- options.declare(:validate_metadata, 'Validate shared inbox metadata', allowed: Allowed::TYPES_BOOLEAN, default: true)
211
- options.declare(:package_folder, 'Handling of reception of packages in folders', allowed: Hash, default: {})
212
215
  options.parse_options!
213
216
  # add node plugin options (for manual)
214
217
  Node.declare_options(options)
@@ -231,13 +234,13 @@ module Aspera
231
234
  api = Api::AoC.new(
232
235
  scope: @scope,
233
236
  subpath: base_path,
234
- secret_finder: config,
237
+ secret_finder: context.secret_finder,
235
238
  **Oauth.kwargs_from_options(options)
236
239
  )
237
240
  # User set a workspace ?
238
241
  # @type [String, nil]
239
242
  workspace = options.get_option(:workspace)
240
- if !workspace.nil? && (m = Manager.percent_selector(workspace))
243
+ if !workspace.nil? && (m = Parser.percent_selector(workspace))
241
244
  case m[:field]
242
245
  when 'name' then api.ws_ids[:name] = m[:value]
243
246
  when 'id' then api.ws_ids[:id] = m[:value]
@@ -285,11 +288,11 @@ module Aspera
285
288
  end
286
289
 
287
290
  # Get resource identifier from command line, either directly specifying the `id` or from `name` (percent selector).
288
- # @param resource_class_path url path for resource
289
- # @return identifier
291
+ # @param resource_class_path [String] url path for resource
292
+ # @return [String] identifier
290
293
  def get_resource_id_from_args(resource_class_path)
291
294
  return options.instance_identifier do |field, value|
292
- Aspera.assert(field.eql?('name'), type: BadArgument){'only selection by name is supported'}
295
+ Aspera.assert_values(field, ['name'], type: BadArgument){'selector field'}
293
296
  aoc_api.lookup_with_q(resource_class_path, value: value)['id']
294
297
  end
295
298
  end
@@ -300,26 +303,28 @@ module Aspera
300
303
  end
301
304
 
302
305
  # List all entities, given additional, default and user's queries
303
- # @param resource_class_path path to query on API
304
- # @param fields fields to display
305
- # @param base_query a query applied always
306
- # @param default_query default query unless overridden by user
306
+ # @param resource_class_path [String] path to query on API
307
+ # @param fields [Array, nil] fields to display
308
+ # @param base_query [Hash] a query applied always
309
+ # @param default_query [Hash] default query unless overridden by user
310
+ # @param query_component [String, nil] registry component key; when set, --query=help shows filter schema
307
311
  # @yieldparam query [Hash] The user's or default query for modification
308
- def result_list(resource_class_path, fields: nil, base_query: {}, default_query: {})
312
+ def result_list(resource_class_path, fields: nil, base_query: {}, default_query: {}, query_component: nil)
309
313
  Aspera.assert_type(base_query, Hash)
310
314
  Aspera.assert_type(default_query, Hash)
311
- query = query_read_delete(default: default_query)
315
+ qs_path = query_component ? Schema::Registry.query_params(query_component, resource_class_path) : nil
316
+ query = query_read_delete(default: default_query, schema: qs_path)
312
317
  # caller may add specific modifications or checks to query
313
318
  yield(query) if block_given?
314
319
  result = aoc_api.read_with_paging(resource_class_path, base_query.merge(query).compact)
315
- return Main.result_object_list(result[:items], fields: fields, total: result[:total])
320
+ return Result::ObjectList.new(result[:items], fields: fields, total: result[:total])
316
321
  end
317
322
 
318
323
  # Translates `dropbox_name` to `dropbox_id` and fills current workspace_id
319
324
  def resolve_dropbox_name_default_ws_id(query)
320
325
  if query.key?('dropbox_name')
321
326
  # convenience: specify name instead of id
322
- raise BadArgument, 'Use field dropbox_name or dropbox_id, not both' if query.key?('dropbox_id')
327
+ Aspera.assert(!query.key?('dropbox_id'), type: BadArgument){'Use field dropbox_name or dropbox_id, not both'}
323
328
  # TODO : craft a query that looks for dropbox only in current workspace
324
329
  query['dropbox_id'] = aoc_api.lookup_with_q('dropboxes', value: query.delete('dropbox_name'))['id']
325
330
  end
@@ -329,7 +334,6 @@ module Aspera
329
334
  end
330
335
 
331
336
  # List all packages according to `query` option.
332
- # @param <none>
333
337
  # @return [Hash] {items,total} with all packages according to combination of user's query and default query
334
338
  def list_all_packages_with_query
335
339
  query = query_read_delete(default: {})
@@ -342,11 +346,13 @@ module Aspera
342
346
  FILES_COMMANDS = (Node::COMMANDS_GEN4 + %i[transfer]).freeze
343
347
 
344
348
  # Execute a node gen4 command starting at given node and file IDs
345
- # @param command_repo [Symbol] Command to execute
346
- # @param node_id [String] Node identifier
347
- # @param file_id [String] Root file id for the operation (can be AK root, or other, e.g. package, or link). If `nil` use AK root file id.
348
- # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
349
- def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil)
349
+ # @param command_repo [Symbol] Command to execute
350
+ # @param node_id [String] Node identifier
351
+ # @param file_id [String] Root file id for the operation. If nil, uses AK root file id.
352
+ # @param scope [String] node scope (Node::SCOPE_<USER|ADMIN>), or nil (requires secret)
353
+ # @param transfer_direction [Symbol] :push or :pull (only for command_repo == :transfer)
354
+ # @param transfer_source [String] source folder (only for command_repo == :transfer)
355
+ def execute_nodegen4_command(command_repo, node_id, file_id: nil, scope: nil, transfer_direction: nil, transfer_source: nil)
350
356
  top_node_api = aoc_api.node_api_from(
351
357
  node_id: node_id,
352
358
  scope: scope,
@@ -356,13 +362,16 @@ module Aspera
356
362
  node_plugin = Node.new(context: context, api: top_node_api)
357
363
  case command_repo
358
364
  when *Node::COMMANDS_GEN4
359
- return node_plugin.execute_command_gen4(command_repo, file_id)
365
+ # For permission: the handler consumes the path first then re-dispatches to sub-commands.
366
+ # Calling dispatch_from_registry with skip_setup would bypass path consumption and fail.
367
+ return node_plugin.send(:"action_access_keys_do_#{command_repo}", do_root_file_id: file_id) if command_repo.eql?(:permission)
368
+ return node_plugin.dispatch_from_registry([:access_keys, :do, command_repo], {do_root_file_id: file_id}, skip_setup: true)
360
369
  when :transfer
361
370
  # client side is agent
362
371
  # server side is transfer server
363
372
  # in same workspace
364
- push_pull = options.get_next_argument('direction', accept_list: %i[push pull])
365
- source_folder = options.get_next_argument('folder or source files', validation: String)
373
+ push_pull = transfer_direction
374
+ source_folder = transfer_source
366
375
  case push_pull
367
376
  when :push
368
377
  client_direction = Transfer::Spec::DIRECTION_SEND
@@ -389,7 +398,7 @@ module Aspera
389
398
  'destination_root_id' => server_apifid.file_id,
390
399
  'source_root_id' => client_apifid.file_id
391
400
  }
392
- return Main.result_transfer(transfer.start(server_apifid.node_api.transfer_spec_gen4(
401
+ return Runner.result_transfer(transfer.start(server_apifid.node_api.transfer_spec_gen4(
393
402
  server_apifid.file_id,
394
403
  client_direction,
395
404
  add_ts
@@ -401,578 +410,154 @@ module Aspera
401
410
 
402
411
  # Execute an action on admin resources
403
412
  # @param resource_type [Symbol] One of ADMIN_OBJECTS
404
- def execute_resource_action(resource_type)
405
- # Set to `true` is resource creation requires a workspace id
406
- require_workspace_id = false
407
- # Default fields to display
408
- list_default_fields = %w[id name]
409
- # Default query for `list` action
410
- list_default_query = {}
411
- # In result of create, what is the field that contains the id of the created resource
412
- id_result = 'id'
413
- supported_operations = Operations::ALL
414
- # API path
415
- resource_class_path = "#{resource_type}s"
416
- # path in openapi where post is located to get creation schema
417
- create_schema_path = resource_class_path
418
- case resource_type
419
- when :client
420
- supported_operations += %i[set_pub_key]
421
- # schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{resource_class_path}.post")
422
- when :client_access_key
423
- resource_class_path = "admin/#{resource_type}s"
424
- when :client_registration_token
425
- resource_class_path = "admin/#{resource_type}s"
426
- list_default_fields = %w[id value data.client_subject_scopes data.name created_at]
427
- id_result = 'token'
428
- when :contact
429
- list_default_fields = %w[source_type source_id name email]
430
- # list_default_query = {'include_only_user_personal_contacts' => true} if @scope == Api::AoC::Scope::USER
431
- when :dropbox
432
- require_workspace_id = true
433
- resource_class_path = "#{resource_type}es"
434
- create_schema_path = resource_class_path
435
- when :group, :saml_configuration
436
- create_schema_path = nil
437
- when :group_membership
438
- list_default_fields = %w[id group_id member_type member_id]
439
- create_schema_path = nil
440
- when :kms_profile
441
- resource_class_path = "integrations/#{resource_type}s"
442
- create_schema_path = nil
443
- when :node
444
- list_default_fields = %w[id name host access_key]
445
- supported_operations += %i[do bearer_token]
446
- when :operation
447
- list_default_fields = %w[id type status created_at updated_at workspace_id user_id workspace_membership_id group_membership_id]
448
- supported_operations = %i[list show modify]
449
- when :organization, :self
450
- supported_operations = Operations::SINGLETON
451
- resource_instance_path = resource_class_path = resource_type
452
- when :short_link
453
- list_default_fields = %w[id short_url data.url_token_data.purpose password_enabled password_protected updated_by_user_id updated_at]
454
- when :user
455
- list_default_fields = %w[id name email]
456
- supported_operations += %i[preferences notifications]
457
- when :workspace
458
- supported_operations += %i[shared_folder dropbox]
459
- when :workspace_membership
460
- list_default_fields = %w[id workspace_id member_type member_id]
413
+ # Per-resource configuration for admin CRUD resources.
414
+ # Keys: path, list_fields, id_result, require_ws_id, create_schema, extra_ops, singleton, op_setup
415
+ # op_setup: Hash of op => setup method name, used for ops that require consuming an instance identifier.
416
+ # For Operations::INSTANCE ops (show/modify/delete), use the auto-generated :setup_admin_<res>_instance.
417
+ # For extra_ops that are instance ops, specify explicitly (or rely on the auto-generated one).
418
+ ADMIN_OBJECT_CONFIG = {
419
+ client: {extra_ops: %i[set_pub_key], extra_op_args: {set_pub_key: [{name: :private_key_pem, type: String}]}},
420
+ client_access_key: {path: 'admin/client_access_keys'},
421
+ client_registration_token: {path: 'admin/client_registration_tokens', list_fields: %w[id value data.client_subject_scopes data.name created_at], id_result: 'token'},
422
+ configuration_policy: {list_fields: nil},
423
+ contact: {list_fields: %w[source_type source_id name email]},
424
+ dropbox: {path: 'dropboxes', require_ws_id: true},
425
+ dropbox_membership: {},
426
+ group: {create_schema: false},
427
+ group_membership: {list_fields: %w[id group_id member_type member_id], create_schema: false},
428
+ kms_profile: {path: 'integrations/kms_profiles', create_schema: false},
429
+ network_policy: {list_fields: nil},
430
+ node: {list_fields: %w[id name host access_key], extra_ops: %i[do bearer_token], extra_op_args: {bearer_token: [{name: :scope, mandatory: false, default: nil}]}},
431
+ operation: {list_fields: %w[id type status created_at updated_at workspace_id user_id workspace_membership_id group_membership_id], ops: %i[list show modify]},
432
+ organization: {singleton: true},
433
+ package: {},
434
+ saml_configuration: {create_schema: false},
435
+ self: {singleton: true},
436
+ short_link: {list_fields: %w[id short_url data.url_token_data.purpose password_enabled password_protected updated_by_user_id updated_at]},
437
+ user: {list_fields: %w[id name email], extra_ops: %i[preferences notifications], op_setup: {preferences: :setup_admin_user_instance, notifications: :setup_admin_user_instance}},
438
+ workspace: {extra_ops: %i[shared_folder dropbox], op_setup: {shared_folder: :setup_admin_workspace_shared_folder, dropbox: :setup_admin_workspace_dropbox}},
439
+ workspace_membership: {list_fields: %w[id workspace_id member_type member_id]}
440
+ }.freeze
441
+ private_constant :ADMIN_OBJECT_CONFIG
442
+
443
+ class << self
444
+ # @return [String] AoC REST path for an admin resource type
445
+ def aoc_res_path(res)
446
+ cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
447
+ return cfg[:path] if cfg[:path]
448
+ "#{res}s".gsub(/ys$/, 'ies')
461
449
  end
462
- # Default location of creation payload in schema
463
- schema_create_modify = Schema::Registry.req_body(Schema::Registry::AOC, "#{create_schema_path}.post") if create_schema_path && supported_operations.include?(:create)
464
- command = options.get_next_command(supported_operations)
465
- # Require identifier for non global commands
466
- if (supported_operations != Operations::SINGLETON) && !Operations::GLOBAL.include?(command)
467
- res_id = get_resource_id_from_args(resource_class_path)
468
- resource_instance_path = "#{resource_class_path}/#{res_id}"
450
+
451
+ # @return [Hash] {path:, ops:, id_result:, require_ws_id:, list_fields:, schema:, query_component:}
452
+ def aoc_res_cfg(res)
453
+ cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
454
+ path = aoc_res_path(res)
455
+ ops = cfg[:ops] || (Base::Operations::ALL + (cfg[:extra_ops] || []))
456
+ schema = cfg[:create_schema] == false ? nil : Schema::Registry.req_body(Schema::Registry::AOC, "#{path}.post")
457
+ {
458
+ path: path,
459
+ ops: ops,
460
+ id_result: cfg[:id_result] || 'id',
461
+ require_ws_id: cfg[:require_ws_id] || false,
462
+ list_fields: cfg.key?(:list_fields) ? cfg[:list_fields] : %w[id name],
463
+ schema: schema,
464
+ query_component: Schema::Registry::AOC
465
+ }
469
466
  end
470
- case command
471
- when :create
472
- # TODO: report inconsistency: creation url is !=, and does not return id.
473
- resource_class_path = 'admin/client_registration/token' if resource_class_path.eql?('admin/client_registration_tokens')
474
- workspace_id = aoc_api.workspace_info[:id] if require_workspace_id
475
- return do_bulk_operation(command: command, descr: 'creation data', id_result: id_result, schema: schema_create_modify) do |params|
476
- params['workspace_id'] = workspace_id if require_workspace_id && workspace_id && !params.key?('workspace_id')
477
- aoc_api.create(resource_class_path, params)
467
+
468
+ # DSL helper: register the 5 short_link leaf commands under the given parent path
469
+ # and define the corresponding action methods on `base`.
470
+ # @param base [Class] the plugin class
471
+ # @param parent_path [Array<Symbol>] full path ending with :short_link
472
+ def register_short_link_commands(base, parent_path)
473
+ base.commands_under(parent_path) do
474
+ base.command(
475
+ :create, description: 'Create',
476
+ arguments: [{name: :custom_data, type: Hash, mandatory: false, default: {}}]
477
+ )
478
+ base.command(
479
+ :modify, description: 'Modify',
480
+ arguments: [{name: :short_link_id, type: :identifier}, {name: :custom_data, type: Hash, mandatory: false, default: {}}]
481
+ )
482
+ base.command(:list, description: 'List short links')
483
+ base.command(
484
+ :show, description: 'Show a short link',
485
+ arguments: [{name: :short_link_id, type: :identifier}]
486
+ )
487
+ base.command(
488
+ :delete, description: 'Delete a short link',
489
+ arguments: [{name: :short_link_id, type: :identifier}]
490
+ )
478
491
  end
479
- when :list
480
- return result_list(resource_class_path, fields: list_default_fields, default_query: list_default_query)
481
- when :show
482
- object = aoc_api.read(resource_instance_path, query_read_delete)
483
- return Main.result_single_object(object, fields: Formatter.all_but('certificate'))
484
- when :modify
485
- changes = options.get_next_argument('properties', validation: Hash, schema: schema_create_modify)
486
- return do_bulk_operation(command: command, values: res_id) do |one_id|
487
- aoc_api.update("#{resource_class_path}/#{one_id}", changes)
488
- {'id' => one_id}
492
+ base.define_action_method(parent_path + [:create]) do |custom_data: {}, **ctx|
493
+ sl_exec_create(custom_data, **ctx)
489
494
  end
490
- when :delete
491
- return do_bulk_operation(command: command, values: res_id) do |one_id|
492
- aoc_api.delete("#{resource_class_path}/#{one_id}")
493
- {'id' => one_id}
495
+ base.define_action_method(parent_path + [:list]) do |**ctx|
496
+ sl_exec_list(**sl_fetch_list(**ctx))
494
497
  end
495
- when :set_pub_key
496
- # special : reads private and generate public
497
- the_private_key = options.get_next_argument('private_key PEM value', validation: String)
498
- the_public_key = OpenSSL::PKey::RSA.new(the_private_key).public_key.to_s
499
- aoc_api.update(resource_instance_path, {jwt_grant_enabled: true, public_key: the_public_key})
500
- return Main.result_success
501
- when :do
502
- command_repo = options.get_next_command(FILES_COMMANDS)
503
- return execute_nodegen4_command(command_repo, res_id, scope: Api::Node::Scope::ADMIN)
504
- when :bearer_token
505
- node_api = aoc_api.node_api_from(
506
- node_id: res_id,
507
- scope: options.get_next_argument('scope', default: Api::Node::Scope::ADMIN)
508
- )
509
- return Main.result_text(node_api.oauth.authorization)
510
- when :dropbox
511
- command_shared = options.get_next_command(%i[list])
512
- case command_shared
513
- when :list
514
- query = options.get_option(:query) || {}
515
- res_data = aoc_api.read('dropboxes', query.merge({'workspace_id'=>res_id}))
516
- return Main.result_object_list(res_data, fields: %w[id name description])
498
+ base.define_action_method(parent_path + [:show]) do |**ctx|
499
+ sl_exec_show(**sl_fetch_list(**ctx))
517
500
  end
518
- when :shared_folder
519
- query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
520
- shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
521
- # inside a workspace
522
- command_shared = options.get_next_command(%i[list node member])
523
- case command_shared
524
- when :list
525
- return Main.result_object_list(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
526
- when :node
527
- shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
528
- shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
529
- Aspera.assert(shared_folder)
530
- command_repo = options.get_next_command(FILES_COMMANDS)
531
- return execute_nodegen4_command(command_repo, shared_folder['node_id'], file_id: shared_folder['file_id'], scope: Api::Node::Scope::ADMIN)
532
- when :member
533
- shared_folder_id = options.instance_identifier(description: 'Shared folder ID')
534
- shared_folder = shared_folders.find{ |i| i['id'].eql?(shared_folder_id)}
535
- Aspera.assert(shared_folder)
536
- command_shared_member = options.get_next_command(%i[list])
537
- case command_shared_member
538
- when :list
539
- node_api = aoc_api.node_api_from(
540
- node_id: shared_folder['node_id'],
541
- workspace_id: res_id,
542
- workspace_name: nil,
543
- scope: Api::Node::Scope::USER
544
- )
545
- result = node_api.read(
546
- 'permissions',
547
- {'file_id' => shared_folder['file_id'], 'tag' => "aspera.files.workspace.id=#{res_id}"}
548
- )
549
- result.each do |item|
550
- item['member'] = begin
551
- if Api::AoC.workspace_access?(item)
552
- {'name'=>'[Internal permission]'}
553
- else
554
- aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name': 'not found'}
555
- end
556
- rescue => e
557
- {'name'=>e.to_s}
558
- end
559
- end
560
- # TODO : read users and group name and add, if query "include_members"
561
- return Main.result_object_list(result, fields: %w[access_type access_id access_level last_updated_at member.name member.email member.system_group_type member.system_group])
562
- end
501
+ base.define_action_method(parent_path + [:delete]) do |**ctx|
502
+ sl_exec_delete(**sl_fetch_list(**ctx), **ctx)
563
503
  end
564
- when :preferences, :notifications
565
- user_preferences_res = "#{resource_instance_path}/#{command.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'}"
566
- case options.get_next_command(%i[show modify])
567
- when :show
568
- return Main.result_single_object(aoc_api.read(user_preferences_res))
569
- when :modify
570
- aoc_api.update(user_preferences_res, options.get_next_argument('properties', validation: Hash))
571
- return Main.result_status('modified')
504
+ base.define_action_method(parent_path + [:modify]) do |custom_data: {}, **ctx|
505
+ sl_exec_modify(custom_data, **sl_fetch_list(**ctx), **ctx)
572
506
  end
573
- else Aspera.error_unexpected_value(command)
574
507
  end
575
508
  end
576
509
 
577
- def execute_application_action
578
- apps_info = aoc_api.read('admin/apps')
579
- all_app_types = apps_info.map{ |i| i['app_type'].to_sym}
580
- command_apps = options.get_next_command(%i[types settings instance membership])
581
- case command_apps
582
- when :types
583
- return Main.result_object_list(apps_info)
584
- when :settings
585
- app_type = options.get_next_command(all_app_types)
586
- cmd_path = "/apps/#{app_type}/settings"
587
- command_app_settings = options.get_next_command(Operations::SINGLETON)
588
- case command_app_settings
589
- when :show
590
- return Main.result_single_object(aoc_api.read(cmd_path))
591
- when :modify
592
- aoc_api.update(cmd_path, options.get_next_argument('properties', validation: Hash))
593
- return Main.result_status('modified')
594
- end
595
- when :instance
596
- list_default_query = {workspace_id: aoc_api.workspace_info[:id]}
597
- list_default_fields = %w[id app_type available workspace_id]
598
- command_app_instances = options.get_next_command(%i[list] + Operations::SINGLETON)
599
- resource_path = 'admin/apps_new'
600
- if Operations::SINGLETON.include?(command_app_instances)
601
- app_type = options.get_next_command(all_app_types)
602
- resource_path = "#{resource_path}/#{app_type}/#{options.instance_identifier(description: "#{app_type} identifier")}"
603
- end
604
- case command_app_instances
605
- when :list
606
- return result_list(resource_path, fields: list_default_fields, default_query: list_default_query)
607
- when :show
608
- return Main.result_single_object(aoc_api.read(resource_path, query_read_delete))
609
- when :modify
610
- aoc_api.update(resource_path, options.get_next_argument('properties', validation: Hash))
611
- return Main.result_status('modified')
612
- end
613
- when :membership
614
- resource_path = 'apps/app_memberships'
615
- command_app_member = options.get_next_command(%i[create list show delete])
616
- resource_path = "#{resource_path}/#{options.instance_identifier(description: 'membership id')}" unless Operations::GLOBAL.include?(command_app_member)
617
- case command_app_member
618
- when :list
619
- return result_list(resource_path)
620
- when :delete
621
- aoc_api.delete("#{resource_path}}")
622
- return Main.result_status('deleted')
623
- when :show
624
- return Main.result_single_object(aoc_api.read(resource_path, query_read_delete))
625
- when :create
626
- aoc_api.update(resource_path, options.get_next_argument('membership properties', validation: Hash))
627
- return Main.result_status('modified')
628
- end
629
- end
630
- end
510
+ # Instance delegators so instance methods can call aoc_res_path/aoc_res_cfg without self.class.
511
+ def aoc_res_path(res) = self.class.aoc_res_path(res)
512
+ def aoc_res_cfg(res) = self.class.aoc_res_cfg(res)
513
+
514
+ # Known fixed set of AoC application types (verified against API: activity, automation, files, packages)
515
+ APP_TYPES = %i[activity automation files packages].freeze
631
516
 
632
517
  ADMIN_ACTIONS = %i[bearer_token application ats usage_reports analytics subscription auth_providers].concat(ADMIN_OBJECTS).freeze
633
518
 
634
- def execute_admin_action
635
- # change scope to admin
636
- change_api_scope(Api::AoC::Scope::ADMIN)
637
- command_admin = options.get_next_command(ADMIN_ACTIONS)
638
- case command_admin
639
- when :bearer_token
640
- return Main.result_text(aoc_api.oauth.authorization)
641
- when *ADMIN_OBJECTS
642
- return execute_resource_action(command_admin)
643
- when :application
644
- return execute_application_action
645
- when :auth_providers
646
- command_auth_prov = options.get_next_command(%i[list update])
647
- case command_auth_prov
648
- when :list
649
- return result_list('admin/auth_providers')
650
- when :update
651
- Aspera.error_not_implemented
652
- end
653
- when :subscription
654
- org = aoc_api.read('organization')
655
- bss_graphql = api_from_options('bss/platform/graphql')
656
- command_subscription = options.get_next_command(%i[account usage])
657
- case command_subscription
658
- when :account
659
- # cspell:disable
660
- graphql_query = <<-GRAPHQL
661
- query ($organization_id: ID!) {
662
- aoc (organization_id: $organization_id) {
663
- bssSubscription {
664
- aocVersion
665
- endDate
666
- startDate
667
- termMonths
668
- plan
669
- trial
670
- termType
671
- aocOrganizations {
672
- id
673
- }
674
- additionalStorageVolumeGb
675
- additionalEgressVolumeGb
676
- term {
677
- startDate
678
- endDate
679
- transferVolumeGb
680
- egressVolumeGb
681
- storageVolumeGb
682
- transferVolumeOffsetGb
683
- }
684
- paygoRate {
685
- transferRate
686
- storageRate
687
- currency
688
- }
689
- aocPlanData {
690
- tier
691
- trial
692
- workspaces { max }
693
- users {
694
- planAmount
695
- max
696
- }
697
- samlIntegration
698
- activity
699
- sharedInboxes
700
- uniqueUrls
701
- support
702
- watermarking
703
- byok
704
- automation { planAmount, max }
705
- }
706
- }
707
- }
708
- }
709
- GRAPHQL
710
- # cspell:enable
711
- result = bss_graphql.create(nil, {query: graphql_query, variables: {organization_id: org['id']}})['data']
712
- return Main.result_single_object(result['aoc']['bssSubscription'])
713
- when :usage
714
- # cspell:disable
715
- graphql_query = <<-GRAPHQL
716
- query ($organization_id: ID!, $startDate: Date!, $endDate: Date!, $aggregate: TransferUsageAggregateOption!) {
717
- aoc (organization_id: $organization_id) {
718
- bssSubscription {
719
- aocOrganizations { id }
720
- additionalStorageVolumeGb
721
- additionalEgressVolumeGb
722
- aocPlanData {
723
- tier
724
- trial
725
- }
726
- term {
727
- transferVolumeGb
728
- egressVolumeGb
729
- storageVolumeGb
730
- startDate
731
- endDate
732
- transferVolumeOffsetGb
733
- }
734
- termMonths
735
- transferUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { mbTotal }
736
- egressUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { usageMb }
737
- }
738
- subscriptionEntitlements {
739
- id
740
- transferUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { mbTotal }
741
- egressUsages (startDate: $startDate, endDate: $endDate, aggregate: $aggregate) { usageMb }
742
- }
743
- }
744
- }
745
- GRAPHQL
746
- aggregate = options.get_next_argument('aggregation', accept_list: %i[ALL MONTHLY], default: :ALL)
747
- today = Date.today
748
- start_date = options.get_next_argument('start date', mandatory: false, default: today.prev_year.strftime('%Y-%m-%d'))
749
- end_date = options.get_next_argument('end date', mandatory: false, default: today.strftime('%Y-%m-%d'))
750
- # cspell:enable
751
- result = bss_graphql.create(
752
- nil,
753
- {
754
- query: graphql_query,
755
- variables: {
756
- organization_id: org['id'],
757
- aggregate: aggregate,
758
- startDate: start_date,
759
- endDate: end_date
760
- }
761
- }
762
- )['data']
763
- return Main.result_single_object(result['aoc'])
764
- end
765
- when :ats
766
- ats_api = Rest.new(**aoc_api.params.deep_merge({
767
- base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
768
- auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
769
- }))
770
- return Ats.new(context: context, api: ats_api).execute_action
771
- when :analytics
772
- analytics_api = Rest.new(**aoc_api.params.deep_merge({
773
- base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
774
- auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
775
- }))
776
- command_analytics = options.get_next_command(%i[application_events transfers files])
777
- case command_analytics
778
- when :application_events
779
- event_type = command_analytics.to_s
780
- events = analytics_api.read("organizations/#{aoc_api.current_user_info['organization_id']}/#{event_type}")[event_type]
781
- return Main.result_object_list(events)
782
- when :transfers
783
- event_type = command_analytics.to_s
784
- event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
785
- event_resource_id = options.get_next_argument("#{event_resource_type} identifier", mandatory: false) ||
786
- case event_resource_type
787
- when :organizations then aoc_api.current_user_info['organization_id']
788
- when :users then aoc_api.current_user_info['id']
789
- when :nodes then aoc_api.current_user_info['read_only_home_node_id']
790
- else Aspera.error_unreachable_line
791
- end
792
- filter = query_read_delete(default: {})
793
- filter['limit'] ||= 100
794
- if options.get_option(:once_only, mandatory: true)
795
- saved_date = []
796
- start_date_persistency = PersistencyActionOnce.new(
797
- manager: persistency,
798
- data: saved_date,
799
- id: IdGenerator.from_list(
800
- 'aoc_ana_date',
801
- options.get_option(:url, mandatory: true),
802
- aoc_api.workspace_info[:name],
803
- event_resource_type.to_s,
804
- event_resource_id
805
- )
806
- )
807
- start_date_time = saved_date.first
808
- stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
809
- saved_date[0] = stop_date_time
810
- filter['start_time'] = start_date_time unless start_date_time.nil?
811
- filter['stop_time'] = stop_date_time
812
- end
813
- events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/#{event_type}", filter)[event_type]
814
- start_date_persistency&.save
815
- if !options.get_option(:notify_to).nil?
816
- events.each do |tr_event|
817
- config.send_email_template(values: {ev: tr_event})
818
- end
819
- end
820
- return Main.result_object_list(events)
821
- when :files
822
- event_type = command_analytics.to_s
823
- event_resource_type = options.get_next_argument('resource', accept_list: %i[organizations users nodes])
824
- event_resource_id = options.instance_identifier(description: "#{event_resource_type} identifier")
825
- event_resource_id =
826
- case event_resource_type
827
- when :organizations then aoc_api.current_user_info['organization_id']
828
- when :users then aoc_api.current_user_info['id']
829
- when :nodes then aoc_api.current_user_info['read_only_home_node_id']
830
- else Aspera.error_unreachable_line
831
- end if event_resource_id.empty?
832
- event_uuid = options.instance_identifier(description: 'event uuid')
833
- filter = query_read_delete(default: {})
834
- filter['limit'] ||= 100
835
- events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/#{event_type}", filter)[event_type]
836
- return Main.result_object_list(events)
837
- end
838
- when :usage_reports
839
- return result_list('usage_reports', base_query: workspace_id_hash)
840
- end
519
+ # Build analytics REST API (shared by action_admin_analytics_*)
520
+ def build_analytics_api
521
+ Rest.new(**aoc_api.params.deep_merge({
522
+ base_url: "#{aoc_api.base_url.gsub('/api/v1', '')}/analytics/v2",
523
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
524
+ }))
841
525
  end
842
526
 
843
- # Create a short link for the given entity: Shared folder or Shared Inbox
844
- # Short link entity: `short_links` have:
845
- # - a numerical id, e.g. `764412`
846
- # - a resource type, e.g. `UrlToken`
847
- # - a resource id, e.g. `scQ7uXPbvQ`
848
- # - a short URL path, e.g. `dxyRpT9`
849
- # @param shared_data [Hash] Information for shared data: dropbox_id+name or file_id+node_id
850
- # @param shared_data [Hash] Shared data for the short link
851
- # @yieldparam operation [Symbol] Operation type (:create, :update, :delete)
852
- # @yieldparam resource_id [String] Resource ID for permission management
853
- # @yieldparam access_levels [Object] Access levels for permissions
854
- def short_link_command(**shared_data, &perm_block)
855
- link_type = options.get_next_argument('link access (public or private)', accept_list: %i[public private])
527
+ # Compute short-link purposes from shared_data keys and link_type.
528
+ # @param shared_data [Hash] :dropbox_id+:name or :file_id+:node_id
529
+ # @param link_type [Symbol] :public or :private
530
+ # @return [Array(String,String)] [token_purpose, short_link_purpose]
531
+ def short_link_purposes(shared_data, link_type)
856
532
  if shared_data.keys.sort == %i[dropbox_id name]
857
- # Packages app, Shared inbox
858
533
  token_purpose = 'send_package_to_dropbox'
859
534
  short_link_purpose = link_type.eql?(:public) ? 'send_package_to_dropbox' : 'shared_folder_auth_link'
860
535
  elsif shared_data.keys.sort == %i[file_id node_id]
861
- # Files app, Shared folder
862
536
  token_purpose = 'view_shared_file'
863
537
  short_link_purpose = link_type.eql?(:public) ? 'token_auth_redirection' : 'shared_folder_auth_link'
864
538
  else
865
539
  Aspera.error_unexpected_value(shared_data.keys)
866
540
  end
867
- command = options.get_next_command(%i[create delete list show] + (link_type.eql?(:public) ? %i[modify] : []))
868
- case command
869
- when :create
870
- # Add workspace id
871
- workspace_id_hash(shared_data)
872
- create_payload = {
873
- purpose: short_link_purpose,
874
- user_selected_name: nil
875
- }
876
- case link_type
877
- when :private
878
- create_payload[:data] = shared_data
879
- when :public
880
- create_payload[:expires_at] = nil
881
- create_payload[:password_enabled] = false
882
- shared_data[:name] = ''
883
- create_payload[:data] = {
884
- aoc: true,
885
- url_token_data: {
886
- data: shared_data,
887
- purpose: token_purpose
888
- }
889
- }
890
- end
891
- custom_data = value_create_modify(command: command, default: {})
892
- access_levels = custom_data.delete('access_levels')
893
- if (pass = custom_data.delete('password'))
894
- create_payload[:data][:url_token_data][:password] = pass
895
- create_payload[:password_enabled] = true
896
- end
897
- create_payload.deep_merge!(custom_data)
898
- result_create_short_link = aoc_api.create('short_links', create_payload)
899
- # Creation: perm_block: permission on node
900
- yield(:create, result_create_short_link['resource_id'], access_levels) if block_given? && link_type.eql?(:public)
901
- return Main.result_single_object(result_create_short_link)
902
- when :delete, :list, :show, :modify
903
- workspace_id_hash(shared_data)
904
- query = if link_type.eql?(:private)
905
- shared_data
906
- else
907
- {
908
- url_token_data: {
909
- data: shared_data,
910
- purpose: token_purpose
911
- }
912
- }
913
- end
914
- list_params = {
915
- json_query: query.to_json,
916
- purpose: short_link_purpose,
917
- edit_access: true,
918
- # embed: 'updated_by_user',
919
- sort: '-created_at'
920
- }
921
- short_list = aoc_api.read_with_paging('short_links', list_params.merge(query_read_delete(default: {})).compact)
922
- case command
923
- when :delete
924
- one_id = options.instance_identifier(description: 'short link id')
925
- if link_type.eql?(:public)
926
- found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
927
- raise BadIdentifier.new('Short link', one_id) if found.nil?
928
- yield(:delete, found['resource_id'], nil)
929
- end
930
- aoc_api.delete("short_links/#{one_id}", {
931
- edit_access: true,
932
- json_query: shared_data.to_json
933
- })
934
- return Main.result_status('deleted')
935
- when :list
936
- return Main.result_object_list(short_list[:items], fields: Formatter.all_but('data'), total: short_list[:total])
937
- when :show
938
- one_id = options.instance_identifier(description: 'short link id')
939
- found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
940
- raise BadIdentifier.new('Short link', one_id) if found.nil?
941
- return Main.result_single_object(found, fields: Formatter.all_but('data'))
942
- when :modify
943
- one_id = options.instance_identifier(description: 'short link id')
944
- node_file = shared_data.slice(:node_id, :file_id)
945
- modify_payload = {
946
- edit_access: true,
947
- json_query: node_file
948
- }
949
- custom_data = value_create_modify(command: command)
950
- if (pass = custom_data.delete('password'))
951
- modify_payload[:password_enabled] = true
952
- modify_payload[:data] = {
953
- url_token_data: {
954
- password: pass,
955
- data: node_file
956
- }
957
- }
958
- else
959
- modify_payload[:password_enabled] = false
960
- end
961
- if custom_data.delete('access_levels')
962
- # Modification: perm_block: permission on node
963
- found = short_list[:items].find{ |item| item['id'].eql?(one_id)}
964
- raise BadIdentifier.new('Short link', one_id) if found.nil?
965
- yield(:update, found['resource_id'], access_levels)
966
- end
967
- modify_payload.deep_merge!(custom_data)
968
- aoc_api.update("short_links/#{one_id}", modify_payload)
969
- return Main.result_status('modified')
970
- end
971
- else Aspera.error_unexpected_value(command)
541
+ [token_purpose, short_link_purpose]
542
+ end
543
+
544
+ # Build the list_params hash used by delete/list/show/modify short link operations.
545
+ # @return [Hash]
546
+ def short_link_list_params(shared_data:, link_type:, token_purpose:, short_link_purpose:, **)
547
+ query = if link_type.eql?(:private)
548
+ shared_data
549
+ else
550
+ {url_token_data: {data: shared_data, purpose: token_purpose}}
972
551
  end
552
+ {
553
+ json_query: query.to_json,
554
+ purpose: short_link_purpose,
555
+ edit_access: true,
556
+ sort: '-created_at'
557
+ }
973
558
  end
974
559
 
975
- # @return persistency object if option `once_only` is used.
560
+ # @return [PersistencyActionOnce, nil] persistency object if option `once_only` is used.
976
561
  def package_persistency
977
562
  return unless options.get_option(:once_only, mandatory: true)
978
563
  # TODO: add query info to id
@@ -994,288 +579,1027 @@ module Aspera
994
579
  all_packages.reject!{ |pkg| skip_package[pkg['id']]}
995
580
  end
996
581
 
997
- # must be public
998
- ACTIONS = %i[reminder servers bearer_token organization tier_restrictions user packages files admin automation gateway].freeze
582
+ # --- DSL command declarations ---
999
583
 
1000
- def execute_action
1001
- command = options.get_next_command(ACTIONS)
1002
- if %i[files packages].include?(command)
1003
- formatter.display_status("Workspace: #{aoc_api.workspace_info[:name].to_s.red}#{' (default)' if aoc_api.default_workspace?}")
1004
- if !aoc_api.private_link.nil?
1005
- folder_name = aoc_api.node_api_from(node_id: aoc_api.home[:node_id]).read("files/#{aoc_api.home[:file_id]}")['name']
1006
- formatter.display_status("Private Folder: #{folder_name}")
1007
- end
584
+ # Root-level commands
585
+ command :reminder, description: 'Send reminder email with list of orgs'
586
+ command(
587
+ :servers, description: 'List AoC servers (no auth)',
588
+ action: lambda do
589
+ no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
590
+ Result::ObjectList.new(no_auth_api.read('servers'))
1008
591
  end
1009
- case command
1010
- when :reminder
1011
- # send an email reminder with list of orgs
1012
- user_email = options.get_option(:username, mandatory: true)
1013
- Rest.new(base_url: "#{Api::AoC.api_base_url}/#{Api::AoC::API_V1}").create('organization_reminders', {email: user_email})
1014
- return Main.result_status("List of organizations user is member of, has been sent by e-mail to #{user_email}")
1015
- when :servers
1016
- return Main.result_object_list(Rest.new(base_url: "#{Api::AoC.api_base_url}/#{Api::AoC::API_V1}").read('servers'))
1017
- when :bearer_token
1018
- return Main.result_text(aoc_api.oauth.authorization)
1019
- when :organization
1020
- return Main.result_single_object(aoc_api.read('organization'))
1021
- when :tier_restrictions
1022
- return Main.result_single_object(aoc_api.read('tier_restrictions'))
1023
- when :user
1024
- user_cmd = options.get_next_command(%i[workspaces profile preferences notifications contacts])
1025
- case user_cmd
1026
- when :contacts
1027
- return execute_resource_action(:contact)
1028
- # when :settings
1029
- # return Main.result_object_list(aoc_api.read('client_settings/'))
1030
- when :workspaces
1031
- case options.get_next_command(%i[list current])
1032
- when :list
1033
- return result_list('workspaces', fields: %w[id name])
1034
- when :current
1035
- return Main.result_single_object(aoc_api.workspace_info)
1036
- end
1037
- when :profile
1038
- case options.get_next_command(%i[show modify])
1039
- when :show
1040
- return Main.result_single_object(aoc_api.current_user_info(exception: true))
1041
- when :modify
1042
- aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", options.get_next_argument('properties', validation: Hash))
1043
- return Main.result_status('modified')
1044
- end
1045
- when :preferences, :notifications
1046
- user_preferences_res = "users/#{aoc_api.current_user_info(exception: true)['id']}/#{user_cmd.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'}"
1047
- case options.get_next_command(%i[show modify])
1048
- when :show
1049
- return Main.result_single_object(aoc_api.read(user_preferences_res))
1050
- when :modify
1051
- aoc_api.update(user_preferences_res, options.get_next_argument('properties', validation: Hash))
1052
- return Main.result_status('modified')
1053
- end
592
+ )
593
+ command :bearer_token, description: 'Display bearer token',
594
+ action: ->{Result::Text.new(aoc_api.oauth.authorization)}
595
+ command :organization, description: 'Show organization info',
596
+ action: ->{Result::SingleObject.new(aoc_api.read('organization'))}
597
+ command :tier_restrictions, description: 'Show tier restrictions',
598
+ action: ->{Result::SingleObject.new(aoc_api.read('tier_restrictions'))}
599
+ command :user, description: 'User commands'
600
+ command :packages, description: 'Package commands', setup: :setup_workspace_display
601
+ command :files, description: 'Files commands (workspace-aware)', setup: :setup_workspace_display
602
+ command :admin, description: 'Administration commands', setup: :setup_admin_scope
603
+ commands_under(:admin) do
604
+ command :bearer_token, description: 'Show admin bearer token',
605
+ action: ->{Result::Text.new(aoc_api.oauth.authorization)}
606
+ command :application, description: 'Manage applications'
607
+ command(
608
+ :ats, description: 'Manage ATS (Aspera Transfer Service)',
609
+ action: lambda do
610
+ ats_api = Rest.new(**aoc_api.params.deep_merge({
611
+ base_url: "#{aoc_api.base_url}/admin/ats/pub/v1",
612
+ auth: {params: {scope: Api::AoC::Scope::ADMIN_USER}}
613
+ }))
614
+ Ats.new(context: context, api: ats_api).execute_action
1054
615
  end
1055
- when :packages
1056
- package_command = options.get_next_command(%i[shared_inboxes send receive list show delete modify].concat(Node::NODE4_READ_ACTIONS), aliases: {recv: :receive})
1057
- case package_command
1058
- when :shared_inboxes
1059
- case options.get_next_command(%i[list show short_link])
1060
- when :list
1061
- default_query = {'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}
1062
- workspace_id_hash(default_query, string: true)
1063
- return result_list('dropbox_memberships', fields: %w[dropbox_id dropbox.name], default_query: default_query)
1064
- when :show
1065
- return Main.result_single_object(aoc_api.read(get_resource_path_from_args('dropboxes')))
1066
- when :short_link
1067
- # TODO: check name
1068
- return short_link_command(dropbox_id: get_resource_id_from_args('dropboxes'), name: '')
1069
- end
1070
- when :send
1071
- package_data = value_create_modify(command: package_command, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post'))
1072
- new_user_option = options.get_option(:new_user_option)
1073
- option_validate = options.get_option(:validate_metadata)
1074
- # Works for both normal user auth and link auth.
1075
- workspace_id_hash(package_data, string: true) unless package_data.key?('workspace_id')
1076
- if !aoc_api.public_link.nil?
1077
- aoc_api.assert_public_link_types(%w[send_package_to_user send_package_to_dropbox])
1078
- box_type = aoc_api.public_link['purpose'].split('_').last
1079
- package_data['recipients'] = [{'id' => aoc_api.public_link['data']["#{box_type}_id"], 'type' => box_type}]
1080
- # enforce workspace id from link (should be already ok, but in case user wanted to override)
1081
- package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
1082
- end
1083
- package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
1084
- # transfer may raise an error
1085
- created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
1086
- Main.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
1087
- # return all info on package (especially package id)
1088
- return Main.result_single_object(created_package[:info])
1089
- when :receive
1090
- ids_to_download = nil
1091
- if !aoc_api.public_link.nil?
1092
- aoc_api.assert_public_link_types(['view_received_package'])
1093
- # Set the package id from link
1094
- ids_to_download = aoc_api.public_link['data']['package_id']
1095
- end
1096
- # Get from command line unless it was a public link
1097
- ids_to_download ||= options.instance_identifier
1098
- skip_ids_persistency = package_persistency
1099
- case ids_to_download
1100
- when SpecialValues::INIT
1101
- all_packages = list_all_packages_with_query[:items]
1102
- Aspera.assert(skip_ids_persistency){'INIT requires option once_only'}
1103
- skip_ids_persistency.data.clear.concat(all_packages.map{ |e| e['id']})
1104
- skip_ids_persistency.save
1105
- return Main.result_status("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
1106
- when SpecialValues::ALL
1107
- all_packages = list_all_packages_with_query[:items]
1108
- # remove from list the ones already downloaded
1109
- reject_packages_from_persistency(all_packages, skip_ids_persistency)
1110
- ids_to_download = all_packages.map{ |e| e['id']}
1111
- formatter.display_status("Found #{ids_to_download.length} package(s).")
1112
- else
1113
- # single id to array
1114
- ids_to_download = [ids_to_download] unless ids_to_download.is_a?(Array)
1115
- end
1116
- # download all files, or specified list only
1117
- ts_paths = transfer.ts_source_paths(default: ['.'])
1118
- per_package_def = options.get_option(:package_folder).symbolize_keys
1119
- save_metadata = per_package_def.delete(:inf)
1120
- # get value outside of loop
1121
- destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
1122
- result_transfer = []
1123
- ids_to_download.each do |package_id|
1124
- package_info = aoc_api.read("packages/#{package_id}")
1125
- package_node_api = aoc_api.node_api_from(
1126
- node_id: package_info['node_id'],
1127
- package_info: package_info,
1128
- **workspace_id_hash(name: true)
1129
- )
1130
- transfer_spec = package_node_api.transfer_spec_gen4(
1131
- package_info['contents_file_id'],
1132
- Transfer::Spec::DIRECTION_RECEIVE,
1133
- {'paths'=> ts_paths}
1134
- )
1135
- transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(package_info, destination_folder, **per_package_def) unless per_package_def.empty?
1136
- dest_folder = transfer.user_transfer_spec['destination_root'] || destination_folder
1137
- formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{dest_folder}]})
1138
- statuses = transfer.start(
1139
- transfer_spec,
1140
- rest_token: package_node_api
1141
- )
1142
- File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
1143
- result_transfer.push({'package' => package_id, Main::STATUS_FIELD => statuses})
1144
- # update skip list only if all transfer sessions completed
1145
- if skip_ids_persistency && TransferAgent.session_status(statuses).eql?(:success)
1146
- skip_ids_persistency.data.push(package_id)
1147
- skip_ids_persistency.save
616
+ )
617
+ command :usage_reports, description: 'List usage reports',
618
+ action: ->{result_list('usage_reports', base_query: workspace_id_hash)}
619
+ command :auth_providers, description: 'Manage auth providers'
620
+ command :subscription, description: 'Show subscription info'
621
+ command :analytics, description: 'Query analytics'
622
+ ADMIN_OBJECTS.each do |res|
623
+ cfg = ADMIN_OBJECT_CONFIG.fetch(res, {})
624
+ op_setup = cfg[:op_setup] || {}
625
+ extra_op_args = cfg[:extra_op_args] || {}
626
+ is_singleton = cfg[:singleton]
627
+ id_arg_spec = is_singleton ? [] : [{name: :res_id, type: :identifier, lookup: :"lookup_aoc_#{res}_id"}]
628
+ ops = if cfg[:ops]
629
+ cfg[:ops]
630
+ elsif is_singleton
631
+ %i[show]
632
+ else
633
+ Operations::ALL + (cfg[:extra_ops] || [])
634
+ end
635
+ command(res, description: "Manage #{res.to_s.tr('_', ' ')}")
636
+ commands_under([:admin, res]) do
637
+ ops.each do |op|
638
+ extra_setup = op_setup[op]
639
+ base_attrs = extra_setup ? {setup: extra_setup} : {}
640
+ extra_arg_list =
641
+ if !is_singleton && op.eql?(:create)
642
+ c = aoc_res_cfg(res)
643
+ [{name: :data, type: Hash, bulk: true, schema: c[:schema]}]
644
+ elsif !is_singleton && op.eql?(:modify)
645
+ c = aoc_res_cfg(res)
646
+ [{name: :data, type: Hash, schema: c[:schema]}]
647
+ elsif extra_op_args.key?(op)
648
+ extra_op_args[op]
649
+ else
650
+ []
651
+ end
652
+ # For non-global ops, prepend the identifier spec
653
+ arg_list = (Operations::GLOBAL.include?(op) ? [] : id_arg_spec) + extra_arg_list
654
+ merged = arg_list.empty? ? base_attrs : base_attrs.merge(arguments: arg_list)
655
+ # Attach query_schema (full path) to :list CommandSpec so --help shows the tip
656
+ if op.eql?(:list)
657
+ c = aoc_res_cfg(res)
658
+ merged = merged.merge(query_schema: Schema::Registry.query_params(c[:query_component], c[:path]))
1148
659
  end
660
+ command(op, description: op.to_s.tr('_', ' ').capitalize, **merged)
1149
661
  end
1150
- return Main.result_transfer_multiple(result_transfer)
1151
- when :show
1152
- package_id = options.instance_identifier
1153
- package_info = aoc_api.read("packages/#{package_id}")
1154
- return Main.result_single_object(package_info)
1155
- when :list
1156
- result = list_all_packages_with_query
1157
- skip_ids_persistency = package_persistency
1158
- reject_packages_from_persistency(result[:items], skip_ids_persistency)
1159
- display_fields = PACKAGE_LIST_DEFAULT_FIELDS
1160
- display_fields += ['workspace_id'] if aoc_api.workspace_info[:id].nil?
1161
- return Main.result_object_list(result[:items], fields: display_fields, total: result[:total])
1162
- when :delete
1163
- return do_bulk_operation(command: package_command, values: options.instance_identifier) do |package_id|
1164
- Aspera.assert_type(package_id, String, Integer){'identifier'}
1165
- aoc_api.delete("packages/#{package_id}")
1166
- end
1167
- when :modify
1168
- package_id = options.instance_identifier
1169
- package_data = value_create_modify(command: package_command)
1170
- aoc_api.update("packages/#{package_id}", package_data)
1171
- return Main.result_status('modified')
1172
- when *Node::NODE4_READ_ACTIONS
1173
- package_id = options.instance_identifier
1174
- package_info = aoc_api.read("packages/#{package_id}")
1175
- return execute_nodegen4_command(package_command, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1176
662
  end
1177
- when :files
1178
- command_repo = options.get_next_command([:short_link].concat(FILES_COMMANDS))
1179
- case command_repo
1180
- when *FILES_COMMANDS
1181
- return execute_nodegen4_command(command_repo, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1182
- when :short_link
1183
- folder_dest = options.get_next_argument('path', validation: String)
1184
- home_node_api = aoc_api.node_api_from(
1185
- node_id: aoc_api.home[:node_id],
1186
- **workspace_id_hash(name: true)
1187
- )
1188
- shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1189
- return short_link_command(
1190
- node_id: shared_apifid.node_api.app_info.node_info['id'],
1191
- file_id: shared_apifid.file_id
1192
- ) do |op, id, access_levels|
1193
- case op
1194
- when :create
1195
- # `id` is the resource id
1196
- perm_data = {
1197
- 'file_id' => shared_apifid.file_id,
1198
- 'access_id' => id,
1199
- 'access_type' => 'user',
1200
- 'access_levels' => Api::AoC.expand_access_levels(access_levels),
1201
- 'tags' => {
1202
- 'url_token' => true,
1203
- 'folder_name' => File.basename(folder_dest),
1204
- 'created_by_name' => aoc_api.current_user_info['name'],
1205
- 'created_by_email' => aoc_api.current_user_info['email'],
1206
- 'access_key' => shared_apifid.node_api.app_info.node_info['access_key'],
1207
- 'node' => shared_apifid.node_api.app_info.node_info['name'],
1208
- **workspace_id_hash(string: true, name: true)
1209
- }
1210
- }
1211
- created_data = shared_apifid.node_api.create('permissions', perm_data)
1212
- aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apifid.node_api.app_info)
1213
- when :update
1214
- # `id` is the permission_id
1215
- found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).find{ |i| i['access_id'].eql?(id)}
1216
- raise Error, "Short link not found: #{id}" if found.nil?
1217
- shared_apifid.node_api.update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1218
- when :delete
1219
- # `id` is the resource id, i.e. `access_id`
1220
- found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).first
1221
- raise Error, "Short link not found: #{id}" if found.nil?
1222
- shared_apifid.node_api.delete("permissions/#{found['id']}")
1223
- else Aspera.error_unexpected_value(op)
663
+ end
664
+ end
665
+ # admin > workspace > shared_folder sub-tree
666
+ commands_under(%i[admin workspace shared_folder]) do
667
+ command :list, description: 'List shared folders'
668
+ command :node, description: 'Execute node command on shared folder',
669
+ arguments: [{name: :sf_id, type: :identifier}],
670
+ setup: :setup_admin_workspace_shared_folder_node
671
+ command :member, description: 'Show folder members',
672
+ arguments: [{name: :sf_id, type: :identifier}],
673
+ setup: :setup_admin_workspace_shared_folder_member
674
+ end
675
+ # admin > workspace > shared_folder > node sub-tree (FILES_COMMANDS)
676
+ commands_under(%i[admin workspace shared_folder node]) do
677
+ FILES_COMMANDS.each{ |c| command(c, description: c.to_s.tr('_', ' ').capitalize)}
678
+ end
679
+ commands_under(%i[admin workspace shared_folder member]) do
680
+ command :list, description: 'List members of a shared folder'
681
+ end
682
+ # admin > workspace > dropbox sub-tree
683
+ commands_under(%i[admin workspace dropbox]) do
684
+ command :list, description: 'List dropboxes in workspace'
685
+ end
686
+ # admin > node > do sub-tree (FILES_COMMANDS)
687
+ commands_under(%i[admin node do]) do
688
+ FILES_COMMANDS.each{ |c| command(c, description: c.to_s.tr('_', ' ').capitalize)}
689
+ end
690
+ # admin > user > preferences|notifications sub-trees
691
+ %i[preferences notifications].each do |pref|
692
+ commands_under([:admin, :user, pref]) do
693
+ command :show, description: "Show #{pref}"
694
+ command :modify, description: "Modify #{pref}",
695
+ arguments: [{name: :properties, type: Hash}]
696
+ end
697
+ end
698
+ commands_under(%i[admin auth_providers]) do
699
+ command :list, description: 'List auth providers',
700
+ action: ->{result_list('admin/auth_providers')}
701
+ command :update, description: 'Update auth provider', action: ->{Aspera.error_not_implemented}
702
+ end
703
+ commands_under(%i[admin subscription]) do
704
+ command :account, description: 'Show subscription account'
705
+ command :usage, description: 'Show subscription usage',
706
+ arguments: [
707
+ {name: :aggregate, mandatory: false, default: :ALL, allowed: %i[ALL MONTHLY]},
708
+ {name: :start_date, mandatory: false, default: nil},
709
+ {name: :end_date, mandatory: false, default: nil}
710
+ ]
711
+ end
712
+ commands_under(%i[admin analytics]) do
713
+ command :application_events, description: 'List application events'
714
+ command :transfers, description: 'List transfer events',
715
+ arguments: [
716
+ {name: :event_resource_type, mandatory: true, allowed: %i[organizations users nodes]},
717
+ {name: :event_resource_id, mandatory: false, default: nil}
718
+ ]
719
+ command :files, description: 'List file events',
720
+ arguments: [
721
+ {name: :event_resource_type, mandatory: true, allowed: %i[organizations users nodes]},
722
+ {name: :event_resource_id, mandatory: true},
723
+ {name: :event_uuid, mandatory: true}
724
+ ]
725
+ end
726
+ # application sub-commands
727
+ commands_under(%i[admin application]) do
728
+ command :types, description: 'List application types',
729
+ action: ->{Result::ObjectList.new(aoc_api.read('admin/apps'))}
730
+ command :settings, description: 'Manage per-app-type settings'
731
+ command :instance, description: 'Manage app instances'
732
+ command :membership, description: 'Manage app memberships'
733
+ end
734
+ APP_SETTINGS_PATH = %i[admin application settings].freeze
735
+ APP_INSTANCE_PATH = %i[admin application instance].freeze
736
+ private_constant :APP_SETTINGS_PATH, :APP_INSTANCE_PATH
737
+ commands_under(APP_SETTINGS_PATH) do
738
+ APP_TYPES.each do |app_type|
739
+ command(app_type, description: "Settings for #{app_type} app")
740
+ commands_under(APP_SETTINGS_PATH + [app_type]) do
741
+ command :show, description: "Show #{app_type} settings",
742
+ action: ->{Result::SingleObject.new(aoc_api.read("/apps/#{app_type}/settings"))}
743
+ command(
744
+ :modify, description: "Modify #{app_type} settings",
745
+ arguments: [{name: :properties, type: Hash}],
746
+ action: lambda do |properties:, **|
747
+ aoc_api.update("/apps/#{app_type}/settings", properties)
748
+ Result::Status.new('modified')
1224
749
  end
1225
- end
750
+ )
751
+ end
752
+ end
753
+ end
754
+ commands_under(APP_INSTANCE_PATH) do
755
+ command(:list, description: 'List app instances', action: lambda do
756
+ result_list(
757
+ 'admin/apps_new',
758
+ fields: %w[id app_type available workspace_id],
759
+ default_query: {workspace_id: aoc_api.workspace_info[:id]}
760
+ )
761
+ end)
762
+ APP_TYPES.each do |app_type|
763
+ command(app_type, description: "Show or modify a #{app_type} instance")
764
+ commands_under(APP_INSTANCE_PATH + [app_type]) do
765
+ command :show, description: "Show a #{app_type} instance",
766
+ arguments: [{name: :res_id, type: :identifier}]
767
+ command :modify, description: "Modify a #{app_type} instance",
768
+ arguments: [{name: :res_id, type: :identifier}, {name: :properties, type: Hash}]
769
+ end
770
+ end
771
+ end
772
+ commands_under(%i[admin application membership]) do
773
+ command :list, description: 'List app memberships',
774
+ action: ->{result_list('apps/app_memberships')}
775
+ command :show, description: 'Show an app membership',
776
+ arguments: [{name: :res_id, type: :identifier}]
777
+ command :delete, description: 'Delete an app membership',
778
+ arguments: [{name: :res_id, type: :identifier}]
779
+ command :create, description: 'Create an app membership',
780
+ arguments: [{name: :membership, type: Hash}]
781
+ end
782
+ command :automation, description: 'Automation commands (BETA)', setup: :setup_automation_api
783
+ command :gateway, description: 'Start AoC Faspex4 gateway',
784
+ arguments: [{name: :parameters, type: Hash, mandatory: false, default: {}}]
785
+
786
+ # user sub-commands
787
+ commands_under(:user) do
788
+ command :workspaces, description: 'Workspace commands'
789
+ command :profile, description: 'User profile commands'
790
+ command :preferences, description: 'User interaction preferences'
791
+ command :notifications, description: 'Notification preferences'
792
+ command :contacts, description: 'Manage contacts'
793
+ entity_command :settings, api: :aoc_api, entity: 'client_settings', description: 'Manage client settings'
794
+ end
795
+ # user > contacts sub-commands (same CRUD as admin > contact)
796
+ commands_under(%i[user contacts]) do
797
+ Operations::ALL.each{ |op| command(op, description: op.to_s.capitalize)}
798
+ end
799
+
800
+ commands_under(%i[user workspaces]) do
801
+ command :list, description: 'List workspaces',
802
+ action: ->{result_list('workspaces', fields: %w[id name])}
803
+ command :current, description: 'Show current workspace',
804
+ action: ->{Result::SingleObject.new(aoc_api.workspace_info)}
805
+ end
806
+
807
+ commands_under(%i[user profile]) do
808
+ command :show, description: 'Show user profile',
809
+ action: ->{Result::SingleObject.new(aoc_api.current_user_info(exception: true))}
810
+ command(
811
+ :modify, description: 'Modify user profile',
812
+ arguments: [{name: :properties, type: Hash}],
813
+ action: lambda do |properties:, **|
814
+ aoc_api.update("users/#{aoc_api.current_user_info(exception: true)['id']}", properties)
815
+ Result::Status.new('modified')
816
+ end
817
+ )
818
+ end
819
+
820
+ commands_under(%i[user preferences]) do
821
+ command(
822
+ :show, description: 'Show user preferences',
823
+ action: lambda do
824
+ user_id = aoc_api.current_user_info(exception: true)['id']
825
+ Result::SingleObject.new(aoc_api.read("users/#{user_id}/user_interaction_preferences"))
826
+ end
827
+ )
828
+ command(
829
+ :modify, description: 'Modify user preferences',
830
+ arguments: [{name: :properties, type: Hash}],
831
+ action: lambda do |properties:, **|
832
+ user_id = aoc_api.current_user_info(exception: true)['id']
833
+ aoc_api.update("users/#{user_id}/user_interaction_preferences", properties)
834
+ Result::Status.new('modified')
1226
835
  end
1227
- when :automation
1228
- change_api_scope(Api::AoC::Scope::ADMIN_USER)
1229
- Log.log.warn('BETA: work under progress')
1230
- # automation api is not in the same place
1231
- automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
1232
- command_automation = options.get_next_command(%i[workflows instances])
1233
- case command_automation
1234
- when :instances
1235
- return entity_execute(api: aoc_api, entity: 'workflow_instances')
1236
- when :workflows
1237
- wf_command = options.get_next_command(%i[action launch].concat(Operations::ALL))
1238
- case wf_command
1239
- when *Operations::ALL
1240
- return entity_execute(
1241
- api: automation_api,
1242
- entity: 'workflows',
1243
- command: wf_command
1244
- )
1245
- when :launch
1246
- wf_id = options.instance_identifier
1247
- data = automation_api.create("workflows/#{wf_id}/launch", {})
1248
- return Main.result_single_object(data)
1249
- when :action
1250
- # TODO: not complete
1251
- wf_id = options.instance_identifier
1252
- wf_action_cmd = options.get_next_command(%i[list create show])
1253
- Log.log.warn{"Not implemented: #{wf_action_cmd}"}
1254
- step = automation_api.create('steps', {'workflow_id' => wf_id})
1255
- automation_api.update("workflows/#{wf_id}", {'step_order' => [step['id']]})
1256
- action = automation_api.create('actions', {'step_id' => step['id'], 'type' => 'manual'})
1257
- automation_api.update("steps/#{step['id']}", {'action_order' => [action['id']]})
1258
- wf = automation_api.read("workflows/#{wf_id}")
1259
- return Main.result_single_object(wf)
836
+ )
837
+ end
838
+
839
+ commands_under(%i[user notifications]) do
840
+ command(
841
+ :show, description: 'Show notification preferences',
842
+ action: lambda do
843
+ user_id = aoc_api.current_user_info(exception: true)['id']
844
+ Result::SingleObject.new(aoc_api.read("users/#{user_id}/notification_preferences"))
845
+ end
846
+ )
847
+ command(
848
+ :modify, description: 'Modify notification preferences',
849
+ arguments: [{name: :properties, type: Hash}],
850
+ action: lambda do |properties:, **|
851
+ user_id = aoc_api.current_user_info(exception: true)['id']
852
+ aoc_api.update("users/#{user_id}/notification_preferences", properties)
853
+ Result::Status.new('modified')
854
+ end
855
+ )
856
+ end
857
+
858
+ # packages sub-commands — instance commands consume package_id
859
+ commands_under(:packages) do
860
+ command :shared_inboxes, description: 'Shared inbox commands'
861
+ command :send, description: 'Send a package', transfer_paths: :send,
862
+ arguments: [{name: :data, type: Hash, schema: Schema::Registry.req_body(Schema::Registry::AOC, 'packages.post')}]
863
+ command :receive, description: 'Receive package(s)', aliases: [:recv], transfer_paths: :receive,
864
+ arguments: [{name: :package_id, type: :identifier}]
865
+ command :list, description: 'List packages'
866
+ command :show, description: 'Show a package',
867
+ arguments: [{name: :package_id, type: :identifier}]
868
+ command :delete, description: 'Delete package(s)',
869
+ arguments: [{name: :package_id, type: :identifier}]
870
+ command :modify, description: 'Modify a package',
871
+ arguments: [{name: :package_id, type: :identifier}, {name: :data, type: Hash}]
872
+ # Node Gen4 read-only actions on packages
873
+ command :bearer_token_node, description: 'Show bearer token for package node',
874
+ arguments: [{name: :package_id, type: :identifier}]
875
+ command :node_info, description: 'Show node info for package',
876
+ arguments: [{name: :package_id, type: :identifier}]
877
+ command :browse, description: 'Browse package contents',
878
+ arguments: [{name: :package_id, type: :identifier}]
879
+ command :find, description: 'Find files in package',
880
+ arguments: [{name: :package_id, type: :identifier}]
881
+ end
882
+
883
+ commands_under(%i[packages shared_inboxes]) do
884
+ command :list, description: 'List shared inboxes',
885
+ action: (lambda do
886
+ result_list(
887
+ 'dropbox_memberships',
888
+ fields: %w[dropbox_id dropbox.name],
889
+ default_query: workspace_id_hash({'embed[]' => 'dropbox', 'aggregate_permissions_by_dropbox' => true, 'sort' => 'dropbox_name'}, string: true)
890
+ )
891
+ end)
892
+ command :show, description: 'Show a shared inbox',
893
+ arguments: [{name: :dropbox_id, type: :identifier, lookup: :lookup_aoc_dropbox_id}],
894
+ action: ->(dropbox_id:, **){Result::SingleObject.new(aoc_api.read("dropboxes/#{dropbox_id}"))}
895
+ command :short_link, description: 'Manage shared inbox short links',
896
+ arguments: [{name: :link_type, allowed: %i[public private]}],
897
+ setup: :setup_packages_short_link
898
+ end
899
+ # packages > shared_inboxes > short_link sub-commands
900
+ register_short_link_commands(self, %i[packages shared_inboxes short_link])
901
+
902
+ # files sub-commands: all FILES_COMMANDS + :short_link
903
+ # Declared dynamically after FILES_COMMANDS is available (class body evaluated after constants)
904
+ commands_under(:files) do
905
+ command :short_link, description: 'Manage file short link',
906
+ arguments: [{name: :folder_dest, type: String}, {name: :link_type, allowed: %i[public private]}],
907
+ setup: :setup_files_short_link
908
+ command :transfer, description: 'Transfer files (node-to-node)',
909
+ arguments: [{name: :direction, allowed: %i[push pull]}, {name: :source_folder, type: String}]
910
+ command :mkdir, description: 'Create folder'
911
+ command :mklink, description: 'Create symbolic link'
912
+ command :mkfile, description: 'Create file'
913
+ command :rename, description: 'Rename entry'
914
+ command :delete, description: 'Delete entry'
915
+ command :upload, description: 'Upload files', transfer_paths: :send
916
+ command :download, description: 'Download files', transfer_paths: :receive
917
+ command :sync, description: 'Synchronize folders'
918
+ command :cat, description: 'Show file contents'
919
+ command :show, description: 'Show file info'
920
+ command :modify, description: 'Modify file'
921
+ command :permission, description: 'Manage permissions'
922
+ command :thumbnail, description: 'Show file thumbnail'
923
+ command :v3, description: 'Legacy v3 commands on files'
924
+ command :bearer_token_node, description: 'Show bearer token for file node'
925
+ command :node_info, description: 'Show node info for file'
926
+ command :browse, description: 'Browse files'
927
+ command :find, description: 'Find files'
928
+ end
929
+ # files > short_link sub-commands
930
+ register_short_link_commands(self, %i[files short_link])
931
+
932
+ # automation sub-commands
933
+ commands_under(:automation) do
934
+ entity_command :instances, description: 'Manage workflow instances', api: :aoc_api, entity: 'workflow_instances'
935
+ command :workflows, description: 'Manage workflows'
936
+ end
937
+
938
+ commands_under(%i[automation workflows]) do
939
+ command :create, description: 'Create a workflow'
940
+ command :list, description: 'List workflows'
941
+ command :show, description: 'Show a workflow'
942
+ command :modify, description: 'Modify a workflow'
943
+ command :delete, description: 'Delete a workflow'
944
+ command :launch, description: 'Launch a workflow',
945
+ arguments: [{name: :wf_id, type: :identifier}],
946
+ action: ->(wf_id:, **){Result::SingleObject.new(@automation_api.create("workflows/#{wf_id}/launch", {}))}
947
+ command :action, description: 'Add action to workflow (TODO)'
948
+ end
949
+
950
+ commands_under(%i[automation workflows action]) do
951
+ command :list, description: 'List actions (TODO)',
952
+ arguments: [{name: :wf_id, type: :identifier}]
953
+ command :create, description: 'Create an action (TODO)',
954
+ arguments: [{name: :wf_id, type: :identifier}]
955
+ command :show, description: 'Show an action (TODO)',
956
+ arguments: [{name: :wf_id, type: :identifier}]
957
+ end
958
+
959
+ # --- setup methods ---
960
+
961
+ # Display workspace info before dispatching files/packages sub-commands.
962
+ # Returns {} so it does not inject anything into ctx.
963
+ def setup_workspace_display(**)
964
+ formatter.display_status("Workspace: #{aoc_api.workspace_info[:name].to_s.red}#{' (default)' if aoc_api.default_workspace?}")
965
+ if !aoc_api.private_link.nil?
966
+ folder_name = aoc_api.node_api_from(node_id: aoc_api.home[:node_id]).read("files/#{aoc_api.home[:file_id]}")['name']
967
+ formatter.display_status("Private Folder: #{folder_name}")
968
+ end
969
+ {}
970
+ end
971
+
972
+ # Build automation API and store in @automation_api ivar.
973
+ def setup_automation_api(**)
974
+ change_api_scope(Api::AoC::Scope::ADMIN_USER)
975
+ Log.log.warn('BETA: work under progress')
976
+ @automation_api = Rest.new(**aoc_api.params, base_url: aoc_api.base_url.gsub('/api/', '/automation/'))
977
+ {}
978
+ end
979
+
980
+ # --- handler methods ---
981
+
982
+ def action_reminder
983
+ user_email = options.get_option(:username, mandatory: true)
984
+ no_auth_api = Api::AoC.new(url: options.get_option(:url), auth: :none)
985
+ no_auth_api.create('organization_reminders', {email: user_email})
986
+ return Result::Status.new("List of organizations user is member of, has been sent by e-mail to #{user_email}")
987
+ end
988
+
989
+ # packages > send
990
+ def action_packages_send(data:, **)
991
+ package_data = data
992
+ new_user_option = options.get_option(:new_user_option)
993
+ option_validate = options.get_option(:validate_metadata)
994
+ workspace_id_hash(package_data, string: true) unless package_data.key?('workspace_id')
995
+ if !aoc_api.public_link.nil?
996
+ aoc_api.assert_public_link_types(%w[send_package_to_user send_package_to_dropbox])
997
+ box_type = aoc_api.public_link['purpose'].split('_').last
998
+ package_data['recipients'] = [{'id' => aoc_api.public_link['data']["#{box_type}_id"], 'type' => box_type}]
999
+ package_data['workspace_id'] = aoc_api.public_link['data']['workspace_id']
1000
+ end
1001
+ package_data['encryption_at_rest'] = true if transfer.user_transfer_spec['content_protection'].eql?('encrypt')
1002
+ created_package = aoc_api.create_package_simple(package_data, option_validate, new_user_option)
1003
+ Runner.result_transfer(transfer.start(created_package[:spec], rest_token: created_package[:node]))
1004
+ return Result::SingleObject.new(created_package[:info])
1005
+ end
1006
+
1007
+ # packages > receive — package_id: from arguments:(:identifier) (or overridden by public_link)
1008
+ def action_packages_receive(package_id:, **)
1009
+ ids_to_download = if aoc_api.public_link.nil?
1010
+ package_id
1011
+ else
1012
+ aoc_api.assert_public_link_types(['view_received_package'])
1013
+ aoc_api.public_link['data']['package_id']
1014
+ end
1015
+ skip_ids_persistency = package_persistency
1016
+ case ids_to_download
1017
+ when SpecialValues::INIT
1018
+ all_packages = list_all_packages_with_query[:items]
1019
+ Aspera.assert(skip_ids_persistency, 'INIT requires option once_only')
1020
+ skip_ids_persistency.data.clear.concat(all_packages.map{ |e| e['id']})
1021
+ skip_ids_persistency.save
1022
+ return Result::Status.new("Initialized skip for #{skip_ids_persistency.data.count} package(s)")
1023
+ when SpecialValues::ALL
1024
+ all_packages = list_all_packages_with_query[:items]
1025
+ reject_packages_from_persistency(all_packages, skip_ids_persistency)
1026
+ ids_to_download = all_packages.map{ |e| e['id']}
1027
+ formatter.display_status("Found #{ids_to_download.length} package(s).")
1028
+ else
1029
+ ids_to_download = [ids_to_download] unless ids_to_download.is_a?(Array)
1030
+ end
1031
+ ts_paths = transfer.ts_source_paths(default: ['.'])
1032
+ per_package_def = options.get_option(:package_folder).symbolize_keys
1033
+ save_metadata = per_package_def.delete(:inf)
1034
+ destination_folder = transfer.destination_folder(Transfer::Spec::DIRECTION_RECEIVE)
1035
+ result_transfer = []
1036
+ ids_to_download.each do |package_id|
1037
+ package_info = aoc_api.read("packages/#{package_id}")
1038
+ package_node_api = aoc_api.node_api_from(
1039
+ node_id: package_info['node_id'],
1040
+ package_info: package_info,
1041
+ **workspace_id_hash(name: true)
1042
+ )
1043
+ transfer_spec = package_node_api.transfer_spec_gen4(
1044
+ package_info['contents_file_id'],
1045
+ Transfer::Spec::DIRECTION_RECEIVE,
1046
+ {'paths'=> ts_paths}
1047
+ )
1048
+ transfer.user_transfer_spec['destination_root'] = self.class.unique_folder(package_info, destination_folder, **per_package_def) unless per_package_def.empty?
1049
+ dest_folder = transfer.user_transfer_spec['destination_root'] || destination_folder
1050
+ formatter.display_status(%Q{Downloading package: [#{package_info['id']}] "#{package_info['name']}" to [#{dest_folder}]})
1051
+ statuses = transfer.start(transfer_spec, rest_token: package_node_api)
1052
+ File.write(File.join(dest_folder, "#{package_id}.info.json"), package_info.to_json) if save_metadata
1053
+ result_transfer.push({'package' => package_id, Runner::STATUS_FIELD => statuses})
1054
+ if skip_ids_persistency && statuses.is_a?(Transfer::Result::Success)
1055
+ skip_ids_persistency.data.push(package_id)
1056
+ skip_ids_persistency.save
1057
+ end
1058
+ end
1059
+ return Runner.result_transfer_multiple(result_transfer)
1060
+ end
1061
+
1062
+ # packages > list
1063
+ def action_packages_list
1064
+ result = list_all_packages_with_query
1065
+ skip_ids_persistency = package_persistency
1066
+ reject_packages_from_persistency(result[:items], skip_ids_persistency)
1067
+ display_fields = PACKAGE_LIST_DEFAULT_FIELDS
1068
+ display_fields += ['workspace_id'] if aoc_api.workspace_info[:id].nil?
1069
+ Result::ObjectList.new(result[:items], fields: display_fields, total: result[:total])
1070
+ end
1071
+
1072
+ # packages > show
1073
+ def action_packages_show(package_id:, **)
1074
+ Result::SingleObject.new(aoc_api.read("packages/#{package_id}"))
1075
+ end
1076
+
1077
+ # packages > delete
1078
+ def action_packages_delete(package_id:, **)
1079
+ is_bulk = options.get_option(:bulk)
1080
+ items = package_id.is_a?(Array) ? package_id : [package_id]
1081
+ Result.bulk(items, is_bulk: is_bulk, command: :delete, bfail: options.get_option(:bfail)) do |one_id|
1082
+ Aspera.assert_type(one_id, String, Integer){'identifier'}
1083
+ aoc_api.delete("packages/#{one_id}")
1084
+ end
1085
+ end
1086
+
1087
+ # packages > modify
1088
+ def action_packages_modify(data:, package_id:, **)
1089
+ aoc_api.update("packages/#{package_id}", data)
1090
+ Result::Status.new('modified')
1091
+ end
1092
+
1093
+ # packages > bearer_token_node / node_info / browse / find
1094
+ # (NODE4_READ_ACTIONS dispatched by full path: action_packages_bearer_token_node, etc.)
1095
+ Node::NODE4_READ_ACTIONS.each do |action|
1096
+ define_action_method([:packages, action]) do |package_id:, **|
1097
+ package_info = aoc_api.read("packages/#{package_id}")
1098
+ execute_nodegen4_command(action, package_info['node_id'], file_id: package_info['contents_file_id'], scope: Api::Node::Scope::USER)
1099
+ end
1100
+ end
1101
+
1102
+ # setup: files > short_link
1103
+ # Resolves the target folder, consumes link_type argument, computes purposes.
1104
+ # @return [Hash] ctx keys: sl_shared_data, sl_link_type, sl_token_purpose, sl_short_link_purpose, sl_perm_block, sl_shared_apifid, sl_folder_dest
1105
+ def setup_files_short_link(folder_dest:, link_type:, **)
1106
+ home_node_api = aoc_api.node_api_from(
1107
+ node_id: aoc_api.home[:node_id],
1108
+ **workspace_id_hash(name: true)
1109
+ )
1110
+ shared_apifid = home_node_api.resolve_api_fid(aoc_api.home[:file_id], folder_dest)
1111
+ shared_data = {
1112
+ node_id: shared_apifid.node_api.app_info.node_info['id'],
1113
+ file_id: shared_apifid.file_id
1114
+ }
1115
+ token_purpose, short_link_purpose = short_link_purposes(shared_data, link_type)
1116
+ perm_block = lambda do |op, id, access_levels|
1117
+ case op
1118
+ when :create
1119
+ perm_data = {
1120
+ 'file_id' => shared_apifid.file_id,
1121
+ 'access_id' => id,
1122
+ 'access_type' => 'user',
1123
+ 'access_levels' => Api::AoC.expand_access_levels(access_levels),
1124
+ 'tags' => {
1125
+ 'url_token' => true,
1126
+ 'folder_name' => File.basename(folder_dest),
1127
+ 'created_by_name' => aoc_api.current_user_info['name'],
1128
+ 'created_by_email' => aoc_api.current_user_info['email'],
1129
+ 'access_key' => shared_apifid.node_api.app_info.node_info['access_key'],
1130
+ 'node' => shared_apifid.node_api.app_info.node_info['name'],
1131
+ **workspace_id_hash(string: true, name: true)
1132
+ }
1133
+ }
1134
+ created_data = shared_apifid.node_api.create('permissions', perm_data)
1135
+ aoc_api.permissions_send_event(event_data: created_data, app_info: shared_apifid.node_api.app_info)
1136
+ when :update
1137
+ found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).find{ |i| i['access_id'].eql?(id)}
1138
+ Aspera.assert(!found.nil?, type: Error){"Short link not found: #{id}"}
1139
+ shared_apifid.node_api.update("permissions/#{found['id']}", {access_levels: Api::AoC.expand_access_levels(access_levels)})
1140
+ when :delete
1141
+ found = shared_apifid.node_api.read('permissions', {file_id: shared_apifid.file_id, inherited: false, access_type: 'user', access_id: id}).first
1142
+ Aspera.assert(!found.nil?, type: Error){"Short link not found: #{id}"}
1143
+ shared_apifid.node_api.delete("permissions/#{found['id']}")
1144
+ else Aspera.error_unexpected_value(op)
1145
+ end
1146
+ end
1147
+ {
1148
+ sl_shared_data: shared_data,
1149
+ sl_link_type: link_type,
1150
+ sl_token_purpose: token_purpose,
1151
+ sl_short_link_purpose: short_link_purpose,
1152
+ sl_perm_block: perm_block
1153
+ }
1154
+ end
1155
+
1156
+ # setup: packages > shared_inboxes > short_link
1157
+ # Reads dropbox_id, consumes link_type argument, computes purposes.
1158
+ # @return [Hash] ctx keys: sl_shared_data, sl_link_type, sl_token_purpose, sl_short_link_purpose
1159
+ def setup_packages_short_link(link_type:, **)
1160
+ dropbox_id = get_resource_id_from_args('dropboxes')
1161
+ shared_data = {dropbox_id: dropbox_id, name: ''}
1162
+ token_purpose, short_link_purpose = short_link_purposes(shared_data, link_type)
1163
+ {
1164
+ sl_shared_data: shared_data,
1165
+ sl_link_type: link_type,
1166
+ sl_token_purpose: token_purpose,
1167
+ sl_short_link_purpose: short_link_purpose,
1168
+ sl_perm_block: nil
1169
+ }
1170
+ end
1171
+
1172
+ # Shared implementation for short_link > create
1173
+ def sl_exec_create(custom_data = {}, sl_shared_data:, sl_link_type:, sl_token_purpose:, sl_short_link_purpose:, sl_perm_block:, **)
1174
+ shared_data = sl_shared_data.dup
1175
+ workspace_id_hash(shared_data)
1176
+ create_payload = {purpose: sl_short_link_purpose, user_selected_name: nil}
1177
+ case sl_link_type
1178
+ when :private
1179
+ create_payload[:data] = shared_data
1180
+ when :public
1181
+ create_payload[:expires_at] = nil
1182
+ create_payload[:password_enabled] = false
1183
+ shared_data[:name] = ''
1184
+ create_payload[:data] = {
1185
+ aoc: true,
1186
+ url_token_data: {data: shared_data, purpose: sl_token_purpose}
1187
+ }
1188
+ end
1189
+ custom_data = {}
1190
+ access_levels = custom_data.delete('access_levels')
1191
+ if (pass = custom_data.delete('password'))
1192
+ create_payload[:data][:url_token_data][:password] = pass
1193
+ create_payload[:password_enabled] = true
1194
+ end
1195
+ create_payload.deep_merge!(custom_data)
1196
+ result_create_short_link = aoc_api.create('short_links', create_payload)
1197
+ sl_perm_block&.call(:create, result_create_short_link['resource_id'], access_levels) if sl_link_type.eql?(:public)
1198
+ Result::SingleObject.new(result_create_short_link)
1199
+ end
1200
+
1201
+ # Shared implementation for short_link > delete|list|show|modify: fetch the short_list
1202
+ def sl_fetch_list(sl_shared_data:, sl_link_type:, sl_token_purpose:, sl_short_link_purpose:, **)
1203
+ shared_data = sl_shared_data.dup
1204
+ workspace_id_hash(shared_data)
1205
+ list_params = short_link_list_params(
1206
+ shared_data: shared_data, link_type: sl_link_type,
1207
+ token_purpose: sl_token_purpose, short_link_purpose: sl_short_link_purpose
1208
+ )
1209
+ {
1210
+ sl_short_list: aoc_api.read_with_paging('short_links', list_params.merge(query_read_delete(default: {})).compact),
1211
+ sl_shared_data_ws: shared_data
1212
+ }
1213
+ end
1214
+
1215
+ # Shared implementation for short_link > delete
1216
+ def sl_exec_delete(sl_shared_data_ws:, sl_short_list:, sl_link_type:, sl_perm_block:, short_link_id: nil, **)
1217
+ one_id = short_link_id
1218
+ if sl_link_type.eql?(:public)
1219
+ found = sl_short_list[:items].find{ |item| item['id'].eql?(one_id)}
1220
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
1221
+ sl_perm_block&.call(:delete, found['resource_id'], nil)
1222
+ end
1223
+ aoc_api.delete("short_links/#{one_id}", {edit_access: true, json_query: sl_shared_data_ws.to_json})
1224
+ Result::Status.new('deleted')
1225
+ end
1226
+
1227
+ # Shared implementation for short_link > list
1228
+ def sl_exec_list(sl_short_list:, **)
1229
+ Result::ObjectList.new(sl_short_list[:items], fields: Formatter.all_but('data'), total: sl_short_list[:total])
1230
+ end
1231
+
1232
+ # Shared implementation for short_link > show
1233
+ def sl_exec_show(sl_short_list:, short_link_id: nil, **)
1234
+ one_id = short_link_id
1235
+ found = sl_short_list[:items].find{ |item| item['id'].eql?(one_id)}
1236
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
1237
+ Result::SingleObject.new(found, fields: Formatter.all_but('data'))
1238
+ end
1239
+
1240
+ # Shared implementation for short_link > modify
1241
+ def sl_exec_modify(custom_data = {}, sl_shared_data:, sl_short_list:, sl_link_type:, sl_perm_block:, short_link_id: nil, **)
1242
+ Aspera.assert_values(sl_link_type, [:public], type: Cli::BadArgument){'link_type'}
1243
+ one_id = short_link_id
1244
+ node_file = sl_shared_data.slice(:node_id, :file_id)
1245
+ modify_payload = {edit_access: true, json_query: node_file}
1246
+ custom_data = {}
1247
+ if (pass = custom_data.delete('password'))
1248
+ modify_payload[:password_enabled] = true
1249
+ modify_payload[:data] = {url_token_data: {password: pass, data: node_file}}
1250
+ else
1251
+ modify_payload[:password_enabled] = false
1252
+ end
1253
+ if custom_data.delete('access_levels')
1254
+ found = sl_short_list[:items].find{ |item| item['id'].eql?(one_id)}
1255
+ raise BadIdentifier.new('Short link', one_id) if found.nil?
1256
+ sl_perm_block&.call(:update, found['resource_id'], nil)
1257
+ end
1258
+ modify_payload.deep_merge!(custom_data)
1259
+ aoc_api.update("short_links/#{one_id}", modify_payload)
1260
+ Result::Status.new('modified')
1261
+ end
1262
+
1263
+ # files > transfer
1264
+ def action_files_transfer(direction:, source_folder:, **)
1265
+ execute_nodegen4_command(
1266
+ :transfer, aoc_api.home[:node_id],
1267
+ file_id: aoc_api.home[:file_id],
1268
+ scope: Api::Node::Scope::USER,
1269
+ transfer_direction: direction,
1270
+ transfer_source: source_folder
1271
+ )
1272
+ end
1273
+
1274
+ # files > FILES_COMMANDS (all Gen4 node commands except :transfer, handled above)
1275
+ FILES_COMMANDS.reject{ |a| a.eql?(:transfer)}.each do |action|
1276
+ define_action_method([:files, action]) do
1277
+ execute_nodegen4_command(action, aoc_api.home[:node_id], file_id: aoc_api.home[:file_id], scope: Api::Node::Scope::USER)
1278
+ end
1279
+ end
1280
+
1281
+ # admin > application > instance > <type> > show|modify
1282
+ APP_TYPES.each do |app_type|
1283
+ define_action_method([:admin, :application, :instance, app_type, :show]) do |res_id:, **|
1284
+ Result::SingleObject.new(aoc_api.read("admin/apps_new/#{app_type}/#{res_id}", query_read_delete))
1285
+ end
1286
+
1287
+ define_action_method([:admin, :application, :instance, app_type, :modify]) do |properties:, res_id:, **|
1288
+ aoc_api.update("admin/apps_new/#{app_type}/#{res_id}", properties)
1289
+ Result::Status.new('modified')
1290
+ end
1291
+ end
1292
+
1293
+ def action_admin_application_membership_create(membership:, **)
1294
+ data = membership.dup
1295
+ app_type = data.delete('app_type')
1296
+ Aspera.assert_type(app_type, String){'app_type'}
1297
+ Aspera.assert_values(app_type.to_sym, APP_TYPES){'app_type'}
1298
+ Result::SingleObject.new(aoc_api.create("apps/#{app_type}/app_memberships", data))
1299
+ end
1300
+
1301
+ # admin > application > membership > show|delete
1302
+ def action_admin_application_membership_show(res_id:, **)
1303
+ Result::SingleObject.new(aoc_api.read("apps/app_memberships/#{res_id}", query_read_delete))
1304
+ end
1305
+
1306
+ def action_admin_application_membership_delete(res_id:, **)
1307
+ aoc_api.delete("apps/app_memberships/#{res_id}")
1308
+ Result::Status.new('deleted')
1309
+ end
1310
+
1311
+ # admin - setup: change API scope to admin once
1312
+ def setup_admin_scope(**)
1313
+ change_api_scope(Api::AoC::Scope::ADMIN)
1314
+ {}
1315
+ end
1316
+
1317
+ # admin > subscription > account
1318
+ def action_admin_subscription_account
1319
+ org = aoc_api.read('organization')
1320
+ result = GraphQL.execute(api_from_options('bss/platform/graphql'), 'bss_subscription_account', {organization_id: org['id']})
1321
+ Result::SingleObject.new(result['aoc']['bssSubscription'])
1322
+ end
1323
+
1324
+ # admin > subscription > usage
1325
+ def action_admin_subscription_usage(aggregate:, start_date:, end_date:, **)
1326
+ today = Date.today
1327
+ aggregate = :ALL if aggregate.nil?
1328
+ start_date = today.prev_year.strftime('%Y-%m-%d') if start_date.nil?
1329
+ end_date = today.strftime('%Y-%m-%d') if end_date.nil?
1330
+ org = aoc_api.read('organization')
1331
+ result = GraphQL.execute(
1332
+ api_from_options('bss/platform/graphql'), 'bss_subscription_usage',
1333
+ {organization_id: org['id'], aggregate: aggregate, startDate: start_date, endDate: end_date}
1334
+ )
1335
+ Result::SingleObject.new(result['aoc'])
1336
+ end
1337
+
1338
+ # admin > analytics > application_events
1339
+ def action_admin_analytics_application_events
1340
+ analytics_api = build_analytics_api
1341
+ events = analytics_api.read("organizations/#{aoc_api.current_user_info['organization_id']}/application_events")['application_events']
1342
+ Result::ObjectList.new(events)
1343
+ end
1344
+
1345
+ # admin > analytics > transfers
1346
+ def action_admin_analytics_transfers(event_resource_type:, event_resource_id:, **)
1347
+ analytics_api = build_analytics_api
1348
+ event_resource_id ||=
1349
+ case event_resource_type
1350
+ when :organizations then aoc_api.current_user_info['organization_id']
1351
+ when :users then aoc_api.current_user_info['id']
1352
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1353
+ else Aspera.error_unreachable_line
1354
+ end
1355
+ filter = query_read_delete(default: {})
1356
+ filter['limit'] ||= 100
1357
+ if options.get_option(:once_only, mandatory: true)
1358
+ saved_date = []
1359
+ start_date_persistency = PersistencyActionOnce.new(
1360
+ manager: persistency,
1361
+ data: saved_date,
1362
+ id: IdGenerator.from_list('aoc_ana_date', options.get_option(:url, mandatory: true), aoc_api.workspace_info[:name], event_resource_type.to_s, event_resource_id)
1363
+ )
1364
+ start_date_time = saved_date.first
1365
+ stop_date_time = Time.now.utc.strftime('%FT%T.%LZ')
1366
+ saved_date[0] = stop_date_time
1367
+ filter['start_time'] = start_date_time unless start_date_time.nil?
1368
+ filter['stop_time'] = stop_date_time
1369
+ end
1370
+ events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers", filter)['transfers']
1371
+ start_date_persistency&.save
1372
+ events.each{ |tr_event| context.mailer.send_email_template(values: {ev: tr_event})} if !options.get_option(:notify_to).nil?
1373
+ Result::ObjectList.new(events)
1374
+ end
1375
+
1376
+ # admin > analytics > files
1377
+ def action_admin_analytics_files(event_resource_type:, event_resource_id:, event_uuid:, **)
1378
+ analytics_api = build_analytics_api
1379
+ event_resource_id =
1380
+ case event_resource_type
1381
+ when :organizations then aoc_api.current_user_info['organization_id']
1382
+ when :users then aoc_api.current_user_info['id']
1383
+ when :nodes then aoc_api.current_user_info['read_only_home_node_id']
1384
+ else Aspera.error_unreachable_line
1385
+ end if event_resource_id.to_s.empty?
1386
+ filter = query_read_delete(default: {})
1387
+ filter['limit'] ||= 100
1388
+ events = analytics_api.read("#{event_resource_type}/#{event_resource_id}/transfers/#{event_uuid}/files", filter)['files']
1389
+ Result::ObjectList.new(events)
1390
+ end
1391
+
1392
+ # Lookup methods for arguments:(:identifier) + lookup: on admin resources.
1393
+ # One method per non-singleton resource; each delegates to get_resource_id_from_args.
1394
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton)}.each do |res|
1395
+ define_method(:"lookup_aoc_#{res}_id") do |_field, value, **|
1396
+ aoc_api.lookup_with_q(aoc_res_path(res), value: value)['id']
1397
+ end
1398
+ end
1399
+
1400
+ # admin > <res> > list
1401
+ ADMIN_OBJECTS.each do |res|
1402
+ define_action_method([:admin, res, :list]) do
1403
+ c = aoc_res_cfg(res)
1404
+ result_list(c[:path], fields: c[:list_fields], query_component: c[:query_component])
1405
+ end
1406
+ end
1407
+
1408
+ # admin > <res> > show
1409
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton)}.each do |res|
1410
+ define_action_method([:admin, res, :show]) do |res_id:, **|
1411
+ c = aoc_res_cfg(res)
1412
+ Result::SingleObject.new(aoc_api.read("#{c[:path]}/#{res_id}", query_read_delete), fields: Formatter.all_but('certificate'))
1413
+ end
1414
+ end
1415
+
1416
+ # admin > organization|self > show (singleton)
1417
+ %i[organization self].each do |res|
1418
+ define_action_method([:admin, res, :show]) do
1419
+ Result::SingleObject.new(aoc_api.read(res.to_s, query_read_delete), fields: Formatter.all_but('certificate'))
1420
+ end
1421
+ end
1422
+
1423
+ # admin > <res> > create
1424
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton)}.each do |res|
1425
+ define_action_method([:admin, res, :create]) do |data:, **|
1426
+ c = aoc_res_cfg(res)
1427
+ path = c[:path]
1428
+ # Special case: client_registration_token has a different creation URL
1429
+ path = 'admin/client_registration/token' if path.eql?('admin/client_registration_tokens')
1430
+ workspace_id = aoc_api.workspace_info[:id] if c[:require_ws_id]
1431
+ is_bulk = options.get_option(:bulk)
1432
+ Result.bulk(data, is_bulk: is_bulk, command: :create, id_result: c[:id_result], bfail: options.get_option(:bfail)) do |params|
1433
+ params['workspace_id'] = workspace_id if c[:require_ws_id] && workspace_id && !params.key?('workspace_id')
1434
+ aoc_api.create(path, params)
1435
+ end
1436
+ end
1437
+ end
1438
+
1439
+ # admin > <res> > modify
1440
+ ADMIN_OBJECTS.reject{ |r| ADMIN_OBJECT_CONFIG.dig(r, :singleton) || ADMIN_OBJECT_CONFIG.dig(r, :ops)&.then{ |o| !o.include?(:modify)}}.each do |res|
1441
+ define_action_method([:admin, res, :modify]) do |data:, res_id:, **|
1442
+ c = aoc_res_cfg(res)
1443
+ aoc_api.update("#{c[:path]}/#{res_id}", data)
1444
+ Result::Status.new('modified')
1445
+ end
1446
+ end
1447
+
1448
+ # admin > <res> > delete
1449
+ ADMIN_OBJECTS.reject do |r|
1450
+ cfg = ADMIN_OBJECT_CONFIG.fetch(r, {})
1451
+ cfg[:singleton] || (cfg[:ops] && !cfg[:ops].include?(:delete))
1452
+ end.each do |res|
1453
+ define_action_method([:admin, res, :delete]) do |res_id:, **|
1454
+ c = aoc_res_cfg(res)
1455
+ is_bulk = options.get_option(:bulk)
1456
+ items = res_id.is_a?(Array) ? res_id : [res_id]
1457
+ Result.bulk(items, is_bulk: is_bulk, command: :delete, bfail: options.get_option(:bfail)) do |one_id|
1458
+ aoc_api.delete("#{c[:path]}/#{one_id}")
1459
+ {'id' => one_id}
1460
+ end
1461
+ end
1462
+ end
1463
+
1464
+ # user > contacts > list|show|create|modify|delete (same API path as admin > contact)
1465
+ Operations::ALL.each do |op|
1466
+ define_action_method([:user, :contacts, op]) do |**ctx|
1467
+ send(CommandSpec.action_method([:admin, :contact, op]), **ctx)
1468
+ end
1469
+ end
1470
+
1471
+ # admin > client > set_pub_key
1472
+ def action_admin_client_set_pub_key(private_key_pem:, res_id:, **)
1473
+ c = aoc_res_cfg(:client)
1474
+ the_public_key = OpenSSL::PKey::RSA.new(private_key_pem).public_key.to_s
1475
+ aoc_api.update("#{c[:path]}/#{res_id}", {jwt_grant_enabled: true, public_key: the_public_key})
1476
+ Result::Success.new
1477
+ end
1478
+
1479
+ # admin > node > do | bearer_token — setup reuses the generic instance setup
1480
+ # (setup_admin_node_instance is auto-generated above, providing res_id:)
1481
+
1482
+ # admin > node > do > <FILES_COMMAND>
1483
+ FILES_COMMANDS.each do |cmd|
1484
+ define_action_method([:admin, :node, :do, cmd]) do |res_id:, **|
1485
+ execute_nodegen4_command(cmd, res_id, scope: Api::Node::Scope::ADMIN)
1486
+ end
1487
+ end
1488
+
1489
+ # admin > node > bearer_token
1490
+ def action_admin_node_bearer_token(scope:, res_id:, **)
1491
+ scope ||= Api::Node::Scope::ADMIN
1492
+ node_api = aoc_api.node_api_from(node_id: res_id, scope: scope)
1493
+ Result::Text.new(node_api.oauth.authorization)
1494
+ end
1495
+
1496
+ # admin > workspace > dropbox — res_id: already in ctx via arguments:(:identifier)
1497
+ def setup_admin_workspace_dropbox(res_id:, **)
1498
+ {ws_res_id: res_id}
1499
+ end
1500
+
1501
+ # admin > workspace > dropbox > list
1502
+ def action_admin_workspace_dropbox_list(ws_res_id:, **)
1503
+ query = options.get_option(:query) || {}
1504
+ Result::ObjectList.new(aoc_api.read('dropboxes', query.merge({'workspace_id' => ws_res_id})), fields: %w[id name description])
1505
+ end
1506
+
1507
+ # admin > workspace > shared_folder — res_id: already in ctx via arguments:(:identifier)
1508
+ def setup_admin_workspace_shared_folder(res_id:, **)
1509
+ resource_instance_path = "#{aoc_res_path(:workspace)}/#{res_id}"
1510
+ query = options.get_option(:query) || Api::AoC.workspace_access(res_id).merge({'admin' => true})
1511
+ shared_folders = aoc_api.read_with_paging("#{resource_instance_path}/permissions", query)[:items]
1512
+ {ws_res_id: res_id, shared_folders: shared_folders}
1513
+ end
1514
+
1515
+ # admin > workspace > shared_folder > list
1516
+ def action_admin_workspace_shared_folder_list(shared_folders:, **)
1517
+ Result::ObjectList.new(shared_folders, fields: %w[id node_name node_id file_id file.path tags.aspera.files.workspace.share_as])
1518
+ end
1519
+
1520
+ # admin > workspace > shared_folder > node|member — sf_id: already in ctx via arguments:(:identifier)
1521
+ def resolve_sf_item(shared_folders:, sf_id:, **)
1522
+ sf_item = shared_folders.find{ |i| i['id'].eql?(sf_id)}
1523
+ Aspera.assert(sf_item, 'shared folder not found')
1524
+ {sf_item: sf_item}
1525
+ end
1526
+
1527
+ alias_method :setup_admin_workspace_shared_folder_node, :resolve_sf_item
1528
+ alias_method :setup_admin_workspace_shared_folder_member, :resolve_sf_item
1529
+
1530
+ # admin > workspace > shared_folder > node > <FILES_COMMAND>
1531
+ FILES_COMMANDS.each do |cmd|
1532
+ define_action_method([:admin, :workspace, :shared_folder, :node, cmd]) do |sf_item:, **|
1533
+ execute_nodegen4_command(cmd, sf_item['node_id'], file_id: sf_item['file_id'], scope: Api::Node::Scope::ADMIN)
1534
+ end
1535
+ end
1536
+
1537
+ # admin > workspace > shared_folder > member > list
1538
+ def action_admin_workspace_shared_folder_member_list(ws_res_id:, sf_item:, **)
1539
+ node_api = aoc_api.node_api_from(
1540
+ node_id: sf_item['node_id'],
1541
+ workspace_id: ws_res_id,
1542
+ workspace_name: nil,
1543
+ scope: Api::Node::Scope::USER
1544
+ )
1545
+ result = node_api.read('permissions', {'file_id' => sf_item['file_id'], 'tag' => "aspera.files.workspace.id=#{ws_res_id}"})
1546
+ result.each do |item|
1547
+ item['member'] = begin
1548
+ if Api::AoC.workspace_access?(item)
1549
+ {'name' => '[Internal permission]'}
1550
+ else
1551
+ aoc_api.read("admin/#{item['access_type']}s/#{item['access_id']}") rescue {'name' => 'not found'}
1260
1552
  end
1553
+ rescue => e
1554
+ {'name' => e.to_s}
1261
1555
  end
1262
- when :admin
1263
- return execute_admin_action
1264
- when :gateway
1265
- require 'aspera/faspex_gw'
1266
- parameters = value_create_modify(command: command, default: {}).symbolize_keys
1267
- uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1268
- server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1269
- Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?)
1270
- server.mount(uri.path, Faspex4GWServlet, aoc_api, aoc_api.workspace_info[:id])
1271
- server.start
1272
- return Main.result_status('Gateway terminated')
1273
- else Aspera.error_unreachable_line
1274
1556
  end
1275
- Aspera.error_unreachable_line
1557
+ # TODO : read users and group name and add, if query "include_members"
1558
+ Result::ObjectList.new(result, fields: %w[access_type access_id access_level last_updated_at member.name member.email member.system_group_type member.system_group])
1276
1559
  end
1277
1560
 
1278
- private :execute_admin_action
1561
+ # admin > user > preferences|notifications > show|modify
1562
+ # (setup_admin_user_instance is auto-generated, providing res_id:)
1563
+ %i[preferences notifications].each do |pref|
1564
+ pref_path = pref.eql?(:preferences) ? 'user_interaction_preferences' : 'notification_preferences'
1565
+ define_action_method([:admin, :user, pref, :show]) do |res_id:, **|
1566
+ Result::SingleObject.new(aoc_api.read("#{aoc_res_path(:user)}/#{res_id}/#{pref_path}"))
1567
+ end
1568
+ define_action_method([:admin, :user, pref, :modify]) do |properties:, res_id:, **|
1569
+ aoc_api.update("#{aoc_res_path(:user)}/#{res_id}/#{pref_path}", properties)
1570
+ Result::Status.new('modified')
1571
+ end
1572
+ end
1573
+
1574
+ # automation > workflows > CRUD operations
1575
+ Operations::ALL.each do |op|
1576
+ define_action_method([:automation, :workflows, op]) do
1577
+ entity_execute(api: @automation_api, entity: 'workflows', command: op)
1578
+ end
1579
+ end
1580
+
1581
+ # automation > workflows > action > * (TODO: not fully implemented)
1582
+ %i[list create show].each do |cmd|
1583
+ define_action_method([:automation, :workflows, :action, cmd]) do |wf_id:, **|
1584
+ Log.log.warn{"Not implemented: #{cmd}"}
1585
+ step = @automation_api.create('steps', {'workflow_id' => wf_id})
1586
+ @automation_api.update("workflows/#{wf_id}", {'step_order' => [step['id']]})
1587
+ action = @automation_api.create('actions', {'step_id' => step['id'], 'type' => 'manual'})
1588
+ @automation_api.update("steps/#{step['id']}", {'action_order' => [action['id']]})
1589
+ Result::SingleObject.new(@automation_api.read("workflows/#{wf_id}"))
1590
+ end
1591
+ end
1592
+
1593
+ def action_gateway(parameters: {}, **)
1594
+ require 'aspera/faspex_gw'
1595
+ parameters = parameters.symbolize_keys
1596
+ uri = URI.parse(parameters.delete(:url){WebServerSimple::DEFAULT_URL})
1597
+ server = WebServerSimple.new(uri, **parameters.slice(*WebServerSimple::PARAMS))
1598
+ Aspera.assert(parameters.except(*WebServerSimple::PARAMS).empty?){"unexpected parameters: #{parameters.except(*WebServerSimple::PARAMS).keys}"}
1599
+ server.mount(uri.path, Faspex4GWServlet, aoc_api, aoc_api.workspace_info[:id])
1600
+ server.start
1601
+ return Result::Status.new('Gateway terminated')
1602
+ end
1279
1603
  end
1280
1604
  end
1281
1605
  end