aspera-cli 4.27.2 → 4.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +133 -0
  4. data/CONTRIBUTING.md +5 -2
  5. data/bin/ascli +3 -1
  6. data/docs/README.md +1006 -766
  7. data/lib/aspera/agent/base.rb +7 -2
  8. data/lib/aspera/agent/connect.rb +6 -8
  9. data/lib/aspera/agent/desktop.rb +2 -6
  10. data/lib/aspera/agent/direct.rb +52 -22
  11. data/lib/aspera/agent/node.rb +9 -8
  12. data/lib/aspera/agent/transferd.rb +2 -2
  13. data/lib/aspera/api/alee.rb +1 -1
  14. data/lib/aspera/api/aoc.rb +14 -12
  15. data/lib/aspera/api/ats.rb +1 -1
  16. data/lib/aspera/api/cos_node.rb +2 -2
  17. data/lib/aspera/api/faspex.rb +11 -7
  18. data/lib/aspera/api/httpgw.rb +38 -35
  19. data/lib/aspera/api/node.rb +39 -34
  20. data/lib/aspera/ascmd.rb +3 -1
  21. data/lib/aspera/ascp/installation.rb +63 -28
  22. data/lib/aspera/ascp/management.rb +1 -0
  23. data/lib/aspera/assert.rb +4 -0
  24. data/lib/aspera/cli/ascp_actions.rb +20 -41
  25. data/lib/aspera/cli/async_transfer_store.rb +12 -11
  26. data/lib/aspera/cli/bootstrapper.rb +14 -16
  27. data/lib/aspera/cli/command_line.rb +252 -0
  28. data/lib/aspera/cli/command_registry.rb +215 -37
  29. data/lib/aspera/cli/command_spec.rb +104 -15
  30. data/lib/aspera/cli/completion/ascli.bash +12 -0
  31. data/lib/aspera/cli/completion/ascli.fish +16 -0
  32. data/lib/aspera/cli/completion/ascli.zsh +19 -0
  33. data/lib/aspera/cli/context.rb +3 -0
  34. data/lib/aspera/cli/deprecation.rb +37 -0
  35. data/lib/aspera/cli/extended_value.rb +6 -3
  36. data/lib/aspera/cli/formatter.rb +94 -80
  37. data/lib/aspera/cli/gem_checker.rb +1 -1
  38. data/lib/aspera/cli/hints.rb +7 -6
  39. data/lib/aspera/cli/http.rb +22 -34
  40. data/lib/aspera/cli/info.rb +3 -0
  41. data/lib/aspera/cli/mcp_tool.rb +47 -83
  42. data/lib/aspera/cli/option_declarator.rb +33 -42
  43. data/lib/aspera/cli/option_registry.rb +69 -0
  44. data/lib/aspera/cli/option_types.rb +105 -0
  45. data/lib/aspera/cli/option_value.rb +278 -0
  46. data/lib/aspera/cli/options.schema.yaml +124 -15
  47. data/lib/aspera/cli/parser.rb +333 -862
  48. data/lib/aspera/cli/plugins/alee.rb +7 -4
  49. data/lib/aspera/cli/plugins/aoc.rb +545 -518
  50. data/lib/aspera/cli/plugins/ats.rb +59 -80
  51. data/lib/aspera/cli/plugins/base.rb +221 -265
  52. data/lib/aspera/cli/plugins/basic_auth.rb +2 -10
  53. data/lib/aspera/cli/plugins/config.rb +263 -184
  54. data/lib/aspera/cli/plugins/console.rb +103 -39
  55. data/lib/aspera/cli/plugins/cos.rb +6 -23
  56. data/lib/aspera/cli/plugins/factory.rb +3 -0
  57. data/lib/aspera/cli/plugins/faspex5.rb +204 -182
  58. data/lib/aspera/cli/plugins/faspio.rb +6 -11
  59. data/lib/aspera/cli/plugins/httpgw.rb +8 -11
  60. data/lib/aspera/cli/plugins/mcp.rb +20 -55
  61. data/lib/aspera/cli/plugins/node.rb +300 -327
  62. data/lib/aspera/cli/plugins/orchestrator.rb +152 -110
  63. data/lib/aspera/cli/plugins/preview.rb +96 -105
  64. data/lib/aspera/cli/plugins/server.rb +78 -53
  65. data/lib/aspera/cli/plugins/shares.rb +80 -131
  66. data/lib/aspera/cli/preset_actions.rb +44 -27
  67. data/lib/aspera/cli/preset_manager.rb +44 -19
  68. data/lib/aspera/cli/prompt.rb +36 -0
  69. data/lib/aspera/cli/result.rb +42 -36
  70. data/lib/aspera/cli/runner.rb +32 -59
  71. data/lib/aspera/cli/special_values.rb +5 -0
  72. data/lib/aspera/cli/sync_actions.rb +51 -46
  73. data/lib/aspera/cli/terminal_formatter.rb +9 -3
  74. data/lib/aspera/cli/transfer_actions.rb +14 -9
  75. data/lib/aspera/cli/transfer_agent.rb +34 -38
  76. data/lib/aspera/cli/transfer_progress.rb +290 -55
  77. data/lib/aspera/cli/vault_manager.rb +0 -17
  78. data/lib/aspera/cli/version.rb +1 -1
  79. data/lib/aspera/cli/wizard.rb +5 -3
  80. data/lib/aspera/coverage.rb +1 -1
  81. data/lib/aspera/environment.rb +35 -5
  82. data/lib/aspera/faspex_gw.rb +2 -1
  83. data/lib/aspera/faspex_postproc.rb +1 -0
  84. data/lib/aspera/graphql.rb +5 -5
  85. data/lib/aspera/json_rpc/client.rb +5 -5
  86. data/lib/aspera/keychain/encrypted_hash.rb +2 -2
  87. data/lib/aspera/keychain/factory.rb +2 -1
  88. data/lib/aspera/keychain/one_password_api.rb +1 -1
  89. data/lib/aspera/link_header.rb +2 -2
  90. data/lib/aspera/log.rb +47 -27
  91. data/lib/aspera/markdown.rb +2 -0
  92. data/lib/aspera/mime.rb +25 -0
  93. data/lib/aspera/node_emulator.rb +759 -0
  94. data/lib/aspera/oauth/base.rb +37 -26
  95. data/lib/aspera/oauth/factory.rb +7 -3
  96. data/lib/aspera/oauth/generic.rb +1 -1
  97. data/lib/aspera/oauth/json_credentials.rb +34 -0
  98. data/lib/aspera/oauth/jwt.rb +4 -5
  99. data/lib/aspera/oauth/web.rb +9 -8
  100. data/lib/aspera/oauth.rb +1 -0
  101. data/lib/aspera/persistency_folder.rb +1 -3
  102. data/lib/aspera/preview/file_types.rb +4 -4
  103. data/lib/aspera/preview/generator.rb +11 -1
  104. data/lib/aspera/preview/options.schema.yaml +119 -0
  105. data/lib/aspera/preview/terminal.rb +4 -3
  106. data/lib/aspera/preview/utils.rb +9 -6
  107. data/lib/aspera/products/connect.rb +1 -1
  108. data/lib/aspera/rainbow.rb +7 -0
  109. data/lib/aspera/rest/aspera_errors.rb +72 -0
  110. data/lib/aspera/rest/call_error.rb +27 -0
  111. data/lib/aspera/rest/client.rb +523 -0
  112. data/lib/aspera/rest/error_analyzer.rb +113 -0
  113. data/lib/aspera/rest/list.rb +149 -0
  114. data/lib/aspera/rest/parameters.rb +55 -0
  115. data/lib/aspera/rest/util.rb +176 -0
  116. data/lib/aspera/rest.rb +7 -621
  117. data/lib/aspera/schema/IBM Aspera Console-enhanced.yaml +1125 -0
  118. data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -20
  119. data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +1784 -0
  120. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1230 -137
  121. data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +2395 -0
  122. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +14 -13
  123. data/lib/aspera/schema/documentation.rb +13 -3
  124. data/lib/aspera/schema/reader.rb +12 -18
  125. data/lib/aspera/schema/registry.rb +23 -1
  126. data/lib/aspera/schema/validator.rb +92 -0
  127. data/lib/aspera/secret_hider.rb +36 -25
  128. data/lib/aspera/string_ext.rb +15 -0
  129. data/lib/aspera/temp_file_manager.rb +6 -5
  130. data/lib/aspera/transfer/parameters.rb +2 -0
  131. data/lib/aspera/transfer/spec.rb +1 -0
  132. data/lib/aspera/uri_reader.rb +11 -11
  133. data/lib/aspera/web_auth/index.html +147 -0
  134. data/lib/aspera/web_auth/server.rb +81 -0
  135. data.tar.gz.sig +0 -0
  136. metadata +43 -9
  137. metadata.gz.sig +0 -0
  138. data/lib/aspera/colors.rb +0 -79
  139. data/lib/aspera/node_simulator.rb +0 -344
  140. data/lib/aspera/preview/options.rb +0 -45
  141. data/lib/aspera/rest_call_error.rb +0 -25
  142. data/lib/aspera/rest_error_analyzer.rb +0 -111
  143. data/lib/aspera/rest_errors_aspera.rb +0 -58
  144. data/lib/aspera/rest_list.rb +0 -136
  145. data/lib/aspera/web_auth.rb +0 -211
@@ -3,7 +3,7 @@
3
3
  require 'aspera/cli/plugins/basic_auth'
4
4
  require 'aspera/cli/plugins/node'
5
5
  require 'aspera/assert'
6
- require 'aspera/rest_list'
6
+ require 'aspera/rest/list'
7
7
 
8
8
  module Aspera
9
9
  module Cli
@@ -23,7 +23,7 @@ module Aspera
23
23
  result[:version] =
24
24
  begin
25
25
  version = nil
26
- login_page = Rest
26
+ login_page = Rest::Client
27
27
  .new(base_url: url, redirect_max: 2)
28
28
  .read('', headers: {'Accept'=>'text/html'})
29
29
  Aspera.assert(login_page.include?('aspera-Shares')) { 'not Shares' }
@@ -39,16 +39,16 @@ module Aspera
39
39
  end
40
40
  result[:ping] =
41
41
  begin
42
- Rest
42
+ Rest::Client
43
43
  .new(base_url: "#{url}/#{NODE_API_PATH}")
44
- .read('ping', headers: {'Content-Type'=>'application/json'})
44
+ .read('ping', headers: {'Content-Type' => Mime::JSON})
45
45
  'ping ok'
46
46
  rescue => e
47
47
  e
48
48
  end
49
49
  result[:api] =
50
50
  begin
51
- data, resp = Rest
51
+ data, resp = Rest::Client
52
52
  .new(base_url: "#{url}/#{NODE_API_PATH}", redirect_max: 1)
53
53
  .read('info', exception: false, ret: :both)
54
54
  # shall fail: shares requires auth, but we check error message
@@ -103,12 +103,19 @@ module Aspera
103
103
  SHARE_PERMISSIONS_OPS = %i[list show].freeze
104
104
  # group users: Rails only exposes index+show+update+destroy (no create)
105
105
  GROUP_USERS_OPS = (Operations::ALL - %i[create]).freeze
106
+ # Arguments of group users commands, by operation
107
+ GROUP_USERS_ARGS = {
108
+ show: [{name: :user_id, type: :identifier}],
109
+ modify: [{name: :user_id, type: :identifier}, {name: :user, type: Hash}],
110
+ delete: [{name: :user_id, type: :identifier}]
111
+ }.freeze
106
112
 
107
113
  # --- DSL ---
108
114
 
109
115
  command :health, description: 'Check Shares health'
110
- command :info, description: 'Show server information', action: -> { Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type'=>'application/json'})) }
111
- command :files, description: 'Browse and transfer files on Shares', aliases: [:repository], setup: :setup_shares_node
116
+ command :info, description: 'Show server information', action: ->(**) { Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type' => Mime::JSON})) }
117
+ command :files, description: 'Browse and transfer files on Shares', aliases: [:repository],
118
+ mount: {plugin: Node, instance: :shares_node_plugin, only: Node::COMMANDS_SHARES}
112
119
  command :admin, description: 'Administer Shares', setup: :setup_admin
113
120
 
114
121
  commands_under :admin do
@@ -137,30 +144,33 @@ module Aspera
137
144
  command :ldap, description: "LDAP #{entity_type}s"
138
145
  command :saml, description: "SAML #{entity_type}s"
139
146
 
147
+ user_fields = %w[id user_id username first_name last_name email] if entity_type.eql?(:user)
140
148
  # all: list/show/delete only (no create, no modify — users/groups/:all has no update route)
141
149
  lookup_method_all = :"lookup_shares_#{entity_type}_all_id"
142
150
  commands_under :all do
143
- (Operations::ALL - %i[create modify]).each do |op|
144
- id_args = Operations::GLOBAL.include?(op) ? {} : {arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_all}]}
145
- command op, description: "#{op.capitalize} #{entity_type}s", **id_args
146
- end
151
+ crud_commands entity: "data/#{entity_type}s",
152
+ api: :@api_shares_admin,
153
+ name: entity_type.to_s,
154
+ operations: Operations::ALL - %i[create modify],
155
+ lookup: lookup_method_all,
156
+ display_fields: user_fields && (user_fields + %w[directory_user])
147
157
  USR_GRP_SETTINGS.each do |setting|
148
158
  # share_permissions: Rails only exposes index+show (read-only)
149
159
  setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
150
160
  command setting,
151
- description: "Manage #{setting} for a #{entity_type}",
161
+ description: "Manage #{entity_noun(setting, singular: false)} for a #{entity_type}",
152
162
  arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_all}]
153
163
  commands_under setting do
154
164
  setting_ops.each do |op|
155
- # share_permissions > show needs a permission identifier (list does not)
156
- perm_id_arg = if setting.eql?(:share_permissions) && op.eql?(:show)
157
- [{name: :permission_id, type: :identifier, lookup: :lookup_share_id}]
158
- else
159
- []
160
- end
165
+ # share_permissions > show needs a permission identifier (list does not), modify needs data
166
+ op_args =
167
+ case op
168
+ when :show then setting.eql?(:share_permissions) ? [{name: :permission_id, type: :identifier, lookup: :lookup_share_id}] : nil
169
+ when :modify then [{name: setting, type: Hash}]
170
+ end
161
171
  command op,
162
- description: "#{op.capitalize} #{setting} for a #{entity_type}",
163
- arguments: perm_id_arg.empty? ? nil : perm_id_arg
172
+ description: "#{op.capitalize} #{entity_noun(setting, singular: false)} for a #{entity_type}",
173
+ arguments: op_args
164
174
  end
165
175
  end
166
176
  end
@@ -170,7 +180,7 @@ module Aspera
170
180
  arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_all_id}]
171
181
  commands_under :users do
172
182
  GROUP_USERS_OPS.each do |op|
173
- command op, description: "#{op.capitalize} users of a group"
183
+ command op, description: "#{op.capitalize} users of a group", arguments: GROUP_USERS_ARGS[op]
174
184
  end
175
185
  end
176
186
  end
@@ -179,17 +189,19 @@ module Aspera
179
189
  # local: full CRUD only — no nested settings (Rails does not nest transfer_settings/app_authorizations/share_permissions under local_users/local_groups)
180
190
  commands_under :local do
181
191
  lookup_method_local = :"lookup_shares_#{entity_type}_local_id"
182
- Operations::ALL.each do |op|
183
- id_args = Operations::GLOBAL.include?(op) ? {} : {arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_local}]}
184
- command op, description: "#{op.capitalize} #{entity_type}s", **id_args
185
- end
192
+ crud_commands entity: "data/local_#{entity_type}s",
193
+ api: :@api_shares_admin,
194
+ name: "local #{entity_type}",
195
+ lookup: lookup_method_local,
196
+ display_fields: user_fields,
197
+ body_component: Schema::Registry::SHARES
186
198
  if entity_type.eql?(:group)
187
199
  command :users,
188
200
  description: 'Manage users of a group',
189
201
  arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_local_id}]
190
202
  commands_under :users do
191
203
  GROUP_USERS_OPS.each do |op|
192
- command op, description: "#{op.capitalize} users of a group"
204
+ command op, description: "#{op.capitalize} users of a group", arguments: GROUP_USERS_ARGS[op]
193
205
  end
194
206
  end
195
207
  end
@@ -198,13 +210,13 @@ module Aspera
198
210
  # ldap: add only
199
211
  commands_under :ldap do
200
212
  command :add, description: "Add a LDAP #{entity_type}",
201
- arguments: [{name: :entity_name, type: String, bulk: true}]
213
+ arguments: [{name: entity_type, bulk: true}]
202
214
  end
203
215
 
204
216
  # saml: import only
205
217
  commands_under :saml do
206
218
  command :import, description: "Import a SAML #{entity_type}",
207
- arguments: [{name: :entity_parameters, type: Hash, bulk: true}]
219
+ arguments: [{name: entity_type, type: Hash, bulk: true}]
208
220
  end
209
221
  end
210
222
  end
@@ -221,7 +233,7 @@ module Aspera
221
233
  lookup_method = :"lookup_shares_#{entity_type}_#{location}_id"
222
234
  define_method(lookup_method) do |field, value, **|
223
235
  path = admin_entity_path(entity_type, location)
224
- RestList.lookup_entity_generic(entity: entity_type, field: field, value: value) { @api_shares_admin.read(path) }['id']
236
+ Rest::List.lookup_entity_generic(entity: entity_type, field: field, value: value) { @api_shares_admin.read(path) }['id']
225
237
  end
226
238
  private lookup_method
227
239
  end
@@ -235,18 +247,22 @@ module Aspera
235
247
  lookup: :lookup_share_id
236
248
  command :user_permissions,
237
249
  description: 'Manage user permissions on a share',
238
- arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
250
+ arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}],
251
+ setup: :setup_share_permissions
239
252
  command :group_permissions,
240
253
  description: 'Manage group permissions on a share',
241
- arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
254
+ arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}],
255
+ setup: :setup_share_permissions
242
256
  end
243
257
 
244
- # share user_permissions and group_permissions: Rails only exposes index+show (read-only)
245
- %i[user_permissions group_permissions].each do |perm_type|
258
+ # share user_permissions and group_permissions: full CRUD, identified by the user or group id
259
+ {user_permissions: :user, group_permissions: :group}.each do |perm_type, entity_type|
246
260
  commands_under [:admin, :share, perm_type] do
247
- SHARE_PERMISSIONS_OPS.each do |op|
248
- command op, description: "#{op.capitalize} #{perm_type}"
249
- end
261
+ crud_commands entity: :"share_#{perm_type}",
262
+ api: :@api_shares_admin,
263
+ name: entity_noun(perm_type),
264
+ id_name: :"#{entity_type}_id",
265
+ lookup: :"lookup_shares_#{entity_type}_all_id"
250
266
  end
251
267
  end
252
268
 
@@ -259,11 +275,11 @@ module Aspera
259
275
 
260
276
  # --- setup ---
261
277
 
262
- # Build the Shares node API plugin and inject into ctx.
263
- # @return [Hash] context hash containing :shares_node_plugin
264
- def setup_shares_node(**)
265
- api_shares_node = basic_auth_api(NODE_API_PATH)
266
- {shares_node_plugin: Node.new(context: context, api: api_shares_node)}
278
+ # files - mount target: Node plugin on the Shares node API.
279
+ # Api::Node (not plain Rest::Client) so that Node commands using its helpers work (e.g. spec, transport).
280
+ # @return [Node]
281
+ def shares_node_plugin(**)
282
+ Node.new(context: context, api: Api::Node.new(**basic_auth_params(NODE_API_PATH)))
267
283
  end
268
284
 
269
285
  # Build the admin REST API and store in ivar.
@@ -273,14 +289,20 @@ module Aspera
273
289
  {}
274
290
  end
275
291
 
292
+ # share_id: resolved by Phase A via arguments:(:identifier, lookup: :lookup_share_id) on the perm_type node
293
+ # @return [Hash] ctx with API path of user and group permissions of the share
294
+ def setup_share_permissions(share_id:, **)
295
+ %i[user_permissions group_permissions].to_h { |perm_type| [:"share_#{perm_type}", "data/shares/#{share_id}/#{perm_type}"] }
296
+ end
297
+
276
298
  # Lookup a share id by field/value using the admin API.
277
299
  def lookup_share_id(field, value, **)
278
- RestList.lookup_entity_generic(entity: 'share', field: field, value: value) { @api_shares_admin.read('data/shares') }['id']
300
+ Rest::List.lookup_entity_generic(entity: 'share', field: field, value: value) { @api_shares_admin.read('data/shares') }['id']
279
301
  end
280
302
 
281
303
  # --- health ---
282
304
 
283
- def action_health
305
+ def action_health(**)
284
306
  nagios = Nagios.new
285
307
  shares_url = options.get_option(:url, mandatory: true)
286
308
  health = self.class.health_check(shares_url)
@@ -298,44 +320,6 @@ module Aspera
298
320
  Result::ObjectList.new(nagios.status_list)
299
321
  end
300
322
 
301
- # --- files sub-commands (restricted to COMMANDS_SHARES, delegated to Node) ---
302
-
303
- commands_under :files do
304
- Node::COMMANDS_SHARES.each do |cmd|
305
- next if cmd.eql?(:sync) # intermediate node with sub-commands, declared separately below
306
- spec = Node::COMMANDS_GEN3_SPEC[cmd] || {description: "Node #{cmd} command"}
307
- command cmd, **spec
308
- end
309
- command :sync, description: 'Synchronize folders (Gen3)'
310
- commands_under :sync do
311
- Sync::Operations::DIRECTIONS.each do |dir|
312
- command dir, description: "#{dir.capitalize}-sync (Gen3)", transfer_paths: :send, arguments: SyncActions::PATH_AND_INFO_ARGS
313
- end
314
- command :admin, description: 'Manage sync database (admin operations)'
315
- SyncActions.register_sync_admin_commands(self, :admin)
316
- end
317
- end
318
-
319
- # One handler per COMMANDS_SHARES command (except :sync which is an intermediate node).
320
- Node::COMMANDS_SHARES.reject { |cmd| cmd.eql?(:sync) }.each do |cmd|
321
- define_action_method([:files, cmd]) do |shares_node_plugin:, **ctx|
322
- shares_node_plugin.dispatch_v3_command(cmd, **ctx)
323
- end
324
- end
325
-
326
- # Handlers for files > sync > <direction>: delegate to Node's [:sync, dir] leaf.
327
- Sync::Operations::DIRECTIONS.each do |dir|
328
- define_action_method([:files, :sync, dir]) do |shares_node_plugin:, **ctx|
329
- shares_node_plugin.dispatch_from_registry([:sync, dir], ctx, skip_setup: true)
330
- end
331
- end
332
- # Handlers for files > sync > admin > <op>: delegate to Node's [:sync, :admin, op] leaf.
333
- SyncActions::ADMIN_COMMANDS.each do |op|
334
- define_action_method([:files, :sync, :admin, op]) do |shares_node_plugin:, **ctx|
335
- shares_node_plugin.dispatch_from_registry([:sync, :admin, op], ctx, skip_setup: true)
336
- end
337
- end
338
-
339
323
  private
340
324
 
341
325
  # @return [String] admin API path for the given entity_type and location
@@ -346,42 +330,25 @@ module Aspera
346
330
 
347
331
  # --- admin > user|group handlers ---
348
332
 
349
- # Shared handler for list/show/delete/create/modify on user or group.
350
- # @param entity_type [Symbol] :user or :group
351
- # @param location [Symbol] :all or :local
352
- # @param op [Symbol] CRUD operation
353
- def action_admin_entity_crud(entity_type, location, op)
354
- path = admin_entity_path(entity_type, location)
355
- lookup = ->(f, v) { RestList.lookup_entity_generic(entity: entity_type, field: f, value: v) { @api_shares_admin.read(path) }['id'] }
356
- display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
357
- display_fields&.push('directory_user') if entity_type.eql?(:user) && location.eql?(:all)
358
- # :all excludes :create; :local has a documented POST+PUT requestBody for both users and groups
359
- body_component = location.eql?(:local) ? Schema::Registry::SHARES : nil
360
- id = Operations::INSTANCE.include?(op) ? options.instance_identifier(&lookup) : nil
361
- send(:"entity_#{op}", api: @api_shares_admin, entity: path, id: id, display_fields: display_fields, body_component: body_component)
362
- end
363
-
364
333
  # Shared handler for USR_GRP_SETTINGS (transfer_settings, app_authorizations, share_permissions)
365
334
  # @param entity_type [Symbol] :user or :group
366
335
  # @param location [Symbol] :all or :local
367
336
  # @param setting [Symbol] one of USR_GRP_SETTINGS
368
337
  # @param op [Symbol] CRUD operation
369
- # entity_id: resolved via arguments:(:identifier) on each setting leaf command
370
- def action_admin_entity_setting(entity_type, location, setting, op, entity_id:, permission_id: nil, **)
371
- path = admin_entity_path(entity_type, location)
372
- entity_path = "#{path}/#{entity_id}/#{setting}"
373
- is_singleton = !setting.eql?(:share_permissions)
374
- id = permission_id || (Operations::INSTANCE.include?(op) && !is_singleton ? options.instance_identifier { |f, v| lookup_share_id(f, v) } : nil)
375
- send(:"entity_#{op}", api: @api_shares_admin, entity: entity_path, id: id, is_singleton: is_singleton)
338
+ # entity_id: resolved via arguments:(:identifier) on the setting node
339
+ # permission_id: (share_permissions show) and data: (modify) resolved via arguments: on the leaf
340
+ def admin_entity_setting(entity_type, location, setting, op, entity_id:, permission_id: nil, data: nil)
341
+ entity_path = "#{admin_entity_path(entity_type, location)}/#{entity_id}/#{setting}"
342
+ send(:"entity_#{op}", api: @api_shares_admin, entity: entity_path, id: permission_id, is_singleton: !setting.eql?(:share_permissions), data: data)
376
343
  end
377
344
 
378
345
  # Shared handler for :users (group only)
379
346
  # group_id: resolved by Phase A via arguments:(:identifier) on the :users node
380
- def action_admin_entity_users(entity_type, location, op, group_id:, **)
347
+ # user_id: and data: resolved via arguments: on the leaf (GROUP_USERS_ARGS)
348
+ def admin_entity_users(entity_type, location, op, group_id:, user_id: nil, data: nil)
381
349
  path = admin_entity_path(entity_type, location)
382
350
  prefix = location.eql?(:all) ? '' : "#{location}_"
383
- id = Operations::INSTANCE.include?(op) ? options.instance_identifier : nil
384
- send(:"entity_#{op}", api: @api_shares_admin, entity: "#{path}/#{group_id}/#{prefix}users", id: id)
351
+ send(:"entity_#{op}", api: @api_shares_admin, entity: "#{path}/#{group_id}/#{prefix}users", id: user_id, data: data)
385
352
  end
386
353
 
387
354
  # Generate action_admin_<user|group>_<location>_<verb> for all combinations
@@ -389,19 +356,13 @@ module Aspera
389
356
  # all: list/show/delete only (no create, no modify — no update route on /users and /groups)
390
357
  # local: full CRUD only — no nested settings under local_users/local_groups
391
358
  ENTITY_LOCATIONS.each do |location|
392
- ops = location.eql?(:all) ? (Operations::ALL - %i[create modify]) : Operations::ALL
393
- ops.each do |op|
394
- define_action_method([:admin, entity_type, location, op]) do
395
- action_admin_entity_crud(entity_type, location, op)
396
- end
397
- end
398
359
  unless location.eql?(:local)
399
360
  USR_GRP_SETTINGS.each do |setting|
400
361
  # share_permissions: Rails only exposes index+show (read-only)
401
362
  setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
402
363
  setting_ops.each do |op|
403
364
  define_action_method([:admin, entity_type, location, setting, op]) do |**kwargs|
404
- action_admin_entity_setting(entity_type, location, setting, op, entity_id: kwargs[:"#{entity_type}_id"], permission_id: kwargs[:permission_id])
365
+ admin_entity_setting(entity_type, location, setting, op, entity_id: kwargs[:"#{entity_type}_id"], permission_id: kwargs[:permission_id], data: kwargs[setting])
405
366
  end
406
367
  end
407
368
  end
@@ -409,24 +370,24 @@ module Aspera
409
370
  next unless entity_type.eql?(:group)
410
371
  # group users: no create route (Rails only exposes index+show+update+destroy)
411
372
  GROUP_USERS_OPS.each do |op|
412
- define_action_method([:admin, entity_type, location, :users, op]) do |group_id:, **|
413
- action_admin_entity_users(entity_type, location, op, group_id: group_id)
373
+ define_action_method([:admin, entity_type, location, :users, op]) do |group_id:, user_id: nil, user: nil, **|
374
+ admin_entity_users(entity_type, location, op, group_id: group_id, user_id: user_id, data: user)
414
375
  end
415
376
  end
416
377
  end
417
378
 
418
379
  # ldap: add
419
- define_action_method([:admin, entity_type, :ldap, :add]) do |entity_name:, **|
380
+ define_action_method([:admin, entity_type, :ldap, :add]) do |**kwargs|
420
381
  path = admin_entity_path(entity_type, :ldap)
421
- bulk_result(entity_name, command: :add, id_result: entity_type.to_s) do |entity_name|
382
+ bulk_result(kwargs.fetch(entity_type), command: :add, id_result: entity_type.to_s) do |entity_name|
422
383
  @api_shares_admin.create(path, {entity_type => entity_name})
423
384
  end
424
385
  end
425
386
 
426
387
  # saml: import
427
- define_action_method([:admin, entity_type, :saml, :import]) do |entity_parameters:, **|
388
+ define_action_method([:admin, entity_type, :saml, :import]) do |**kwargs|
428
389
  path = admin_entity_path(entity_type, :saml)
429
- bulk_result(entity_parameters, command: :import) do |entity_parameters|
390
+ bulk_result(kwargs.fetch(entity_type), command: :import) do |entity_parameters|
430
391
  entity_parameters = entity_parameters.transform_keys { |k| k.gsub(/\s+/, '_').downcase }
431
392
  Aspera.assert_type(entity_parameters, Hash)
432
393
  SAML_IMPORT_MANDATORY.each { |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil? }
@@ -437,18 +398,6 @@ module Aspera
437
398
  end
438
399
  end
439
400
  end
440
-
441
- # Handlers for admin > share > user_permissions|group_permissions > op
442
- # share_id: resolved by Phase A via arguments:(:identifier, lookup: :lookup_share_id) on the perm_type node
443
- # Rails only exposes index+show for share permissions (read-only)
444
- %i[user_permissions group_permissions].each do |perm_type|
445
- SHARE_PERMISSIONS_OPS.each do |op|
446
- define_action_method([:admin, :share, perm_type, op]) do |share_id:, **|
447
- id = Operations::INSTANCE.include?(op) ? options.instance_identifier : nil
448
- send(:"entity_#{op}", api: @api_shares_admin, entity: "data/shares/#{share_id}/#{perm_type}", id: id)
449
- end
450
- end
451
- end
452
401
  end
453
402
  end
454
403
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'aspera/assert'
4
+ require 'aspera/cli/preset_manager'
4
5
 
5
6
  module Aspera
6
7
  module Cli
@@ -11,10 +12,6 @@ module Aspera
11
12
  # Display columns for preset overview
12
13
  CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
13
14
 
14
- def action_preset_list(**)
15
- Result::ValueList.new(presets.config_presets.keys, name: 'name')
16
- end
17
-
18
15
  def action_preset_overview(**)
19
16
  cp = presets.config_presets
20
17
  # Display process modifies the value (hide secrets): we do not want to save removed secrets
@@ -55,18 +52,29 @@ module Aspera
55
52
 
56
53
  SECRET_KEYWORDS = %w[password secret].freeze
57
54
 
58
- # If +option_name+ ends with a secret keyword and the vault is configured,
55
+ # @param name [String] preset name, or `GLOBAL` for the global default preset
56
+ # @param create [Boolean] `true` for an action writing in the preset: the global default preset is declared and created if needed
57
+ # @return [String] preset name
58
+ def preset_name(name, create: false)
59
+ return name unless name.eql?(GLOBAL_DEFAULT_KEYWORD)
60
+ presets.global_default_preset(create: create)
61
+ end
62
+
63
+ # If `option_name` ends with a secret keyword and the vault is configured,
59
64
  # move the clear-text value into the vault and replace it with a @vault: reference.
60
65
  # @param preset [Hash] the preset hash (modified in place)
61
66
  # @param preset_name [String] name used as base for the vault label
62
67
  # @param option_name [String] option key to inspect
63
68
  def secure_preset_option(preset, preset_name, option_name)
64
69
  return unless SECRET_KEYWORDS.any? { |kw| option_name.end_with?(kw) }
70
+ # Vault password is not saved in vault
71
+ return if option_name.eql?('vault_password')
65
72
  # Never auto-secure the global preset: it holds vault credentials themselves
66
- return if preset_name.eql?(presets.global_default_preset)
73
+ # Checked before opening the vault, which may not be usable yet (e.g. vault_password not set)
74
+ return if presets.global_preset?(preset_name)
67
75
  return if vault.nil?
68
76
  value = preset[option_name]
69
- # Already a vault reference or nil — nothing to do
77
+ # Already a vault reference or nil - nothing to do
70
78
  return if value.nil? || value.to_s.start_with?('@vault:')
71
79
  vault_label = preset_name
72
80
  incr = 0
@@ -82,26 +90,32 @@ module Aspera
82
90
  public
83
91
 
84
92
  def action_preset_show(name:, **)
85
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
93
+ name = preset_name(name)
86
94
  cp = presets.config_presets
87
- raise "no such preset: #{name}" unless cp.key?(name)
95
+ raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
88
96
  Result::SingleObject.new(PresetManager.deep_clone(cp[name]))
89
97
  end
90
98
 
91
99
  def action_preset_delete(name:, **)
92
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
100
+ name = preset_name(name)
93
101
  cp = presets.config_presets
94
- raise "no such preset: #{name}" unless cp.key?(name)
102
+ raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
95
103
  cp.delete(name)
104
+ # Remove default declarations referring to the deleted preset
105
+ defaults = cp[PresetManager::Key::DEFAULTS]
106
+ defaults&.select { |_, preset_name| preset_name.eql?(name) }&.each_key do |plugin|
107
+ defaults.delete(plugin)
108
+ Log.log.info { "Removed: #{PresetManager::Key::DEFAULTS}: #{plugin}" }
109
+ end
96
110
  Result::Status.new("Deleted: #{name}")
97
111
  end
98
112
 
99
113
  def action_preset_get(name:, param_name:, **)
100
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
114
+ name = preset_name(name)
101
115
  cp = presets.config_presets
102
- raise "no such preset: #{name}" unless cp.key?(name)
116
+ raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
103
117
  value = cp[name][param_name]
104
- raise "no such option in preset #{name} : #{param_name}" if value.nil?
118
+ raise Cli::BadArgument, "no such option in preset #{name}: #{param_name}" if value.nil?
105
119
  case value
106
120
  when Numeric, String then return Result::Text.new(ExtendedValue.instance.evaluate(value.to_s, context: 'preset'))
107
121
  end
@@ -109,31 +123,31 @@ module Aspera
109
123
  end
110
124
 
111
125
  def action_preset_unset(name:, param_name:, **)
112
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
126
+ name = preset_name(name)
113
127
  cp = presets.config_presets
114
- raise "no such preset: #{name}" unless cp.key?(name)
128
+ raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
115
129
  cp[name].delete(param_name)
116
130
  Result::Status.new("Removed: #{name}: #{param_name}")
117
131
  end
118
132
 
119
133
  def action_preset_set(name:, param_name:, param_value:, **)
120
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
134
+ name = preset_name(name, create: true)
121
135
  param_name = Parser.option_line_to_name(param_name)
122
136
  presets.set_key(name, param_name, Parser.smart_convert(param_value))
123
137
  secure_preset_option(presets.config_presets[name], name, param_name)
124
138
  Result::Nothing.new
125
139
  end
126
140
 
127
- def action_preset_initialize(name:, config_value:, **)
128
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
141
+ def action_preset_initialize(name:, preset:, **)
142
+ name = preset_name(name, create: true)
129
143
  cp = presets.config_presets
130
144
  Log.log.warn { "configuration already exists: #{name}, overwriting" } if cp.key?(name)
131
- cp[name] = config_value
132
- Result::Status.new("Modified: #{@option_config_file}")
145
+ cp[name] = preset
146
+ Result::Status.new("Initialized: #{name}")
133
147
  end
134
148
 
135
149
  def action_preset_update(name:, **)
136
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
150
+ name = preset_name(name, create: true)
137
151
  unprocessed_options = options.unprocessed_options_with_value
138
152
  Log.dump(:opts, unprocessed_options)
139
153
  cp = presets.config_presets
@@ -144,13 +158,16 @@ module Aspera
144
158
  end
145
159
 
146
160
  def action_preset_ask(name:, option_names:, **)
147
- name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
161
+ name = preset_name(name, create: true)
148
162
  cp = presets.config_presets
149
163
  cp[name] ||= {}
150
- option_names.each do |option_name|
151
- option_value = options.get_interactive(option_name, check_option: true)
152
- cp[name][option_name] = option_value
153
- secure_preset_option(cp[name], name, option_name)
164
+ # Option names are only known at runtime: cannot be declared as interactive arguments
165
+ options.with_interactive do
166
+ option_names.each do |option_name|
167
+ option_value = options.get_interactive(option_name)
168
+ cp[name][option_name] = option_value
169
+ secure_preset_option(cp[name], name, option_name)
170
+ end
154
171
  end
155
172
  Result::Status.new("Updated: #{name}")
156
173
  end