aspera-cli 4.26.2 → 4.27.1

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