aspera-cli 4.27.0 → 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 (140) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +29 -0
  4. data/CONTRIBUTING.md +1 -7
  5. data/TODO.md +50 -0
  6. data/docs/README.md +708 -488
  7. data/lib/aspera/agent/base.rb +10 -2
  8. data/lib/aspera/agent/connect.rb +7 -7
  9. data/lib/aspera/agent/desktop.rb +3 -3
  10. data/lib/aspera/agent/direct.rb +48 -19
  11. data/lib/aspera/agent/factory.rb +3 -3
  12. data/lib/aspera/agent/httpgw.rb +55 -14
  13. data/lib/aspera/agent/node.rb +2 -2
  14. data/lib/aspera/agent/transferd.rb +12 -12
  15. data/lib/aspera/api/aoc.rb +28 -28
  16. data/lib/aspera/api/cos_node.rb +10 -9
  17. data/lib/aspera/api/faspex.rb +2 -2
  18. data/lib/aspera/api/httpgw.rb +20 -20
  19. data/lib/aspera/api/node.rb +31 -53
  20. data/lib/aspera/ascmd.rb +13 -13
  21. data/lib/aspera/ascp/installation.rb +43 -36
  22. data/lib/aspera/ascp/management.rb +6 -6
  23. data/lib/aspera/assert.rb +12 -13
  24. data/lib/aspera/cli/ascp_actions.rb +7 -7
  25. data/lib/aspera/cli/async_transfer_store.rb +49 -5
  26. data/lib/aspera/cli/bootstrapper.rb +9 -9
  27. data/lib/aspera/cli/command_registry.rb +1 -1
  28. data/lib/aspera/cli/command_spec.rb +6 -3
  29. data/lib/aspera/cli/context.rb +3 -3
  30. data/lib/aspera/cli/extended_value.rb +26 -26
  31. data/lib/aspera/cli/formatter.rb +76 -54
  32. data/lib/aspera/cli/gem_checker.rb +1 -1
  33. data/lib/aspera/cli/hints.rb +2 -2
  34. data/lib/aspera/cli/http.rb +22 -29
  35. data/lib/aspera/cli/mailer.rb +7 -7
  36. data/lib/aspera/cli/mcp_tool.rb +31 -4
  37. data/lib/aspera/cli/option_declarator.rb +77 -0
  38. data/lib/aspera/cli/options.schema.yaml +273 -3
  39. data/lib/aspera/cli/parser.rb +539 -280
  40. data/lib/aspera/cli/plugins/alee.rb +1 -1
  41. data/lib/aspera/cli/plugins/aoc.rb +297 -313
  42. data/lib/aspera/cli/plugins/ats.rb +25 -21
  43. data/lib/aspera/cli/plugins/base.rb +357 -186
  44. data/lib/aspera/cli/plugins/basic_auth.rb +0 -12
  45. data/lib/aspera/cli/plugins/config.rb +76 -73
  46. data/lib/aspera/cli/plugins/console.rb +8 -8
  47. data/lib/aspera/cli/plugins/cos.rb +4 -3
  48. data/lib/aspera/cli/plugins/factory.rb +2 -2
  49. data/lib/aspera/cli/plugins/faspex5.rb +259 -248
  50. data/lib/aspera/cli/plugins/faspio.rb +7 -2
  51. data/lib/aspera/cli/plugins/httpgw.rb +1 -1
  52. data/lib/aspera/cli/plugins/mcp.rb +11 -11
  53. data/lib/aspera/cli/plugins/node.rb +227 -283
  54. data/lib/aspera/cli/plugins/orchestrator.rb +23 -23
  55. data/lib/aspera/cli/plugins/preview.rb +61 -64
  56. data/lib/aspera/cli/plugins/server.rb +36 -31
  57. data/lib/aspera/cli/plugins/shares.rb +174 -135
  58. data/lib/aspera/cli/preset_actions.rb +39 -19
  59. data/lib/aspera/cli/preset_manager.rb +74 -32
  60. data/lib/aspera/cli/result.rb +18 -16
  61. data/lib/aspera/cli/runner.rb +75 -39
  62. data/lib/aspera/cli/secret_finder.rb +1 -1
  63. data/lib/aspera/cli/sync_actions.rb +36 -35
  64. data/lib/aspera/cli/terminal_formatter.rb +2 -2
  65. data/lib/aspera/cli/transfer_actions.rb +17 -10
  66. data/lib/aspera/cli/transfer_agent.rb +44 -44
  67. data/lib/aspera/cli/transfer_progress.rb +7 -7
  68. data/lib/aspera/cli/vault_manager.rb +34 -12
  69. data/lib/aspera/cli/version.rb +1 -1
  70. data/lib/aspera/cli/wizard.rb +17 -20
  71. data/lib/aspera/colors.rb +3 -3
  72. data/lib/aspera/command_line_builder.rb +16 -16
  73. data/lib/aspera/command_line_converter.rb +1 -1
  74. data/lib/aspera/coverage.rb +1 -2
  75. data/lib/aspera/data_repository.rb +1 -1
  76. data/lib/aspera/dot_container.rb +8 -8
  77. data/lib/aspera/environment.rb +12 -12
  78. data/lib/aspera/faspex_gw.rb +4 -4
  79. data/lib/aspera/faspex_postproc.rb +14 -9
  80. data/lib/aspera/graphql.rb +4 -2
  81. data/lib/aspera/hash_ext.rb +2 -2
  82. data/lib/aspera/json_rpc/client.rb +1 -1
  83. data/lib/aspera/keychain/base.rb +10 -3
  84. data/lib/aspera/keychain/encrypted_hash.rb +92 -17
  85. data/lib/aspera/keychain/factory.rb +17 -5
  86. data/lib/aspera/keychain/hashicorp_vault.rb +1 -1
  87. data/lib/aspera/keychain/macos_security.rb +23 -25
  88. data/lib/aspera/keychain/one_password_api.rb +86 -0
  89. data/lib/aspera/keychain/one_password_base.rb +34 -0
  90. data/lib/aspera/keychain/one_password_cli.rb +98 -0
  91. data/lib/aspera/link_header.rb +2 -2
  92. data/lib/aspera/log.rb +13 -13
  93. data/lib/aspera/markdown.rb +5 -5
  94. data/lib/aspera/nagios.rb +8 -8
  95. data/lib/aspera/node_simulator.rb +33 -27
  96. data/lib/aspera/oauth/base.rb +4 -5
  97. data/lib/aspera/oauth/boot.rb +3 -3
  98. data/lib/aspera/oauth/factory.rb +5 -5
  99. data/lib/aspera/oauth/jwt.rb +5 -7
  100. data/lib/aspera/oauth/web.rb +4 -4
  101. data/lib/aspera/persistency_action_once.rb +4 -4
  102. data/lib/aspera/persistency_folder.rb +8 -8
  103. data/lib/aspera/preview/file_types.rb +4 -4
  104. data/lib/aspera/preview/generator.rb +49 -46
  105. data/lib/aspera/preview/options.rb +3 -2
  106. data/lib/aspera/preview/terminal.rb +5 -5
  107. data/lib/aspera/preview/utils.rb +65 -18
  108. data/lib/aspera/products/connect.rb +2 -2
  109. data/lib/aspera/products/desktop.rb +1 -1
  110. data/lib/aspera/products/other.rb +2 -2
  111. data/lib/aspera/products/transferd.rb +3 -3
  112. data/lib/aspera/proxy_auto_config.rb +10 -10
  113. data/lib/aspera/rest.rb +149 -115
  114. data/lib/aspera/rest_error_analyzer.rb +3 -3
  115. data/lib/aspera/rest_list.rb +10 -9
  116. data/lib/aspera/schema/IBM Aspera Node API-4.4.6.yaml +6232 -0
  117. data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +249 -0
  118. data/lib/aspera/schema/IBM_Aspera_Shares.yaml +4653 -0
  119. data/lib/aspera/schema/documentation.rb +17 -11
  120. data/lib/aspera/schema/reader.rb +78 -4
  121. data/lib/aspera/schema/registry.rb +26 -7
  122. data/lib/aspera/secret_hider.rb +5 -5
  123. data/lib/aspera/ssh.rb +64 -31
  124. data/lib/aspera/ssl.rb +3 -3
  125. data/lib/aspera/sync/database.rb +2 -2
  126. data/lib/aspera/sync/operations.rb +13 -13
  127. data/lib/aspera/temp_file_manager.rb +24 -4
  128. data/lib/aspera/transfer/faux_file.rb +2 -2
  129. data/lib/aspera/transfer/parameters.rb +15 -15
  130. data/lib/aspera/transfer/resumer.rb +9 -9
  131. data/lib/aspera/transfer/spec.rb +1 -1
  132. data/lib/aspera/uri_reader.rb +52 -15
  133. data/lib/aspera/web_auth.rb +6 -5
  134. data/lib/aspera/web_server_simple.rb +9 -9
  135. data/lib/aspera/yaml.rb +4 -4
  136. data.tar.gz.sig +0 -0
  137. metadata +10 -4
  138. metadata.gz.sig +0 -0
  139. data/lib/aspera/cli/plugins/faspex.rb +0 -576
  140. data/lib/aspera/transfer/uri.rb +0 -56
@@ -26,7 +26,7 @@ module Aspera
26
26
  login_page = Rest
27
27
  .new(base_url: url, redirect_max: 2)
28
28
  .read('', headers: {'Accept'=>'text/html'})
29
- Aspera.assert(login_page.include?('aspera-Shares')){'not Shares'}
29
+ Aspera.assert(login_page.include?('aspera-Shares')) { 'not Shares' }
30
30
  if (m = login_page.match(/\(v([0-9a-f\.]+)\)/))
31
31
  version = m[1]
32
32
  if (m = login_page.match(/Patch level ([0-9]+)/))
@@ -95,107 +95,118 @@ module Aspera
95
95
  end
96
96
 
97
97
  SAML_IMPORT_MANDATORY = %w[id name_id].freeze
98
- SAML_IMPORT_ALLOWED = %w[email given_name surname].concat(SAML_IMPORT_MANDATORY).freeze
98
+ SAML_IMPORT_ALLOWED = (%w[email given_name surname] + SAML_IMPORT_MANDATORY).freeze
99
99
 
100
100
  # common to users and groups
101
101
  USR_GRP_SETTINGS = %i[transfer_settings app_authorizations share_permissions].freeze
102
+ # share_permissions is read-only (Rails only exposes index+show)
103
+ SHARE_PERMISSIONS_OPS = %i[list show].freeze
104
+ # group users: Rails only exposes index+show+update+destroy (no create)
105
+ GROUP_USERS_OPS = (Operations::ALL - %i[create]).freeze
102
106
 
103
107
  # --- DSL ---
104
108
 
105
109
  command :health, description: 'Check Shares health'
106
- command :info, description: 'Show server information', action: ->{Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type'=>'application/json'}))}
110
+ command :info, description: 'Show server information', action: -> { Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type'=>'application/json'})) }
107
111
  command :files, description: 'Browse and transfer files on Shares', aliases: [:repository], setup: :setup_shares_node
108
112
  command :admin, description: 'Administer Shares', setup: :setup_admin
109
113
 
110
- commands_under(:admin) do
111
- entity_command :node, api: :@api_shares_admin, entity: 'data/nodes'
114
+ commands_under :admin do
115
+ commands_under :node do
116
+ crud_commands entity: 'data/nodes', api: :@api_shares_admin
117
+ end
118
+ commands_under :ldap_config, description: 'LDAP configuration' do
119
+ crud_commands entity: 'data/ldap_configs', api: :@api_shares_admin
120
+ end
112
121
  command :share, description: 'Manage shares'
113
122
  command :transfer_settings, description: 'Manage transfer settings'
114
123
  command :user, description: 'Manage users'
115
124
  command :group, description: 'Manage groups'
116
- end
117
-
118
- # admin > user / group: sub-commands per location, generated for both entity types
119
- %i[user group].each do |entity_type|
120
- commands_under([:admin, entity_type]) do
121
- command :all, description: "#{entity_type.capitalize}s from all sources"
122
- command :local, description: "Local #{entity_type}s"
123
- command :ldap, description: "LDAP #{entity_type}s"
124
- command :saml, description: "SAML #{entity_type}s"
125
+ commands_under :smtp_server, description: 'SMTP server' do
126
+ crud_commands entity: 'data/smtp_server',
127
+ api: :@api_shares_admin,
128
+ is_singleton: true,
129
+ body_component: Schema::Registry::SHARES,
130
+ operations: %i[show create modify]
125
131
  end
126
-
127
- # all: list/show/delete + USR_GRP_SETTINGS [+ users for group]
128
- commands_under([:admin, entity_type, :all]) do
129
- (Operations::ALL - [:create]).each do |op|
130
- command(op, description: "#{op.capitalize} #{entity_type}(s)")
131
- end
132
- USR_GRP_SETTINGS.each do |setting|
133
- setting_ops = setting.eql?(:share_permissions) ? Operations::ALL : %i[show modify]
134
- lookup_method = :"lookup_shares_#{entity_type}_all_id"
135
- command(
136
- setting, description: "Manage #{setting} for a #{entity_type}",
137
- arguments: [{name: :entity_id, type: :identifier, lookup: lookup_method}]
138
- )
139
- commands_under([:admin, entity_type, :all, setting]) do
140
- setting_ops.each do |op|
141
- command(op, description: "#{op.capitalize} #{setting} for a #{entity_type}")
132
+ # user / group: sub-commands per location, generated for both entity types
133
+ %i[user group].each do |entity_type|
134
+ commands_under entity_type do
135
+ command :all, description: "#{entity_type.capitalize}s from all sources"
136
+ command :local, description: "Local #{entity_type}s"
137
+ command :ldap, description: "LDAP #{entity_type}s"
138
+ command :saml, description: "SAML #{entity_type}s"
139
+
140
+ # all: list/show/delete only (no create, no modify — users/groups/:all has no update route)
141
+ lookup_method_all = :"lookup_shares_#{entity_type}_all_id"
142
+ 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
142
146
  end
143
- end
144
- end
145
- if entity_type.eql?(:group)
146
- command(
147
- :users, description: 'Manage users of a group',
148
- arguments: [{name: :group_id, type: :identifier}]
149
- )
150
- commands_under([:admin, entity_type, :all, :users]) do
151
- Operations::ALL.each do |op|
152
- command(op, description: "#{op.capitalize} users of a group")
147
+ USR_GRP_SETTINGS.each do |setting|
148
+ # share_permissions: Rails only exposes index+show (read-only)
149
+ setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
150
+ command setting,
151
+ description: "Manage #{setting} for a #{entity_type}",
152
+ arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_all}]
153
+ commands_under setting do
154
+ 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
161
+ command op,
162
+ description: "#{op.capitalize} #{setting} for a #{entity_type}",
163
+ arguments: perm_id_arg.empty? ? nil : perm_id_arg
164
+ end
165
+ end
153
166
  end
154
- end
155
- end
156
- end
157
-
158
- # local: list/show/delete/create/modify [+ users for group]
159
- commands_under([:admin, entity_type, :local]) do
160
- Operations::ALL.each do |op|
161
- command(op, description: "#{op.capitalize} #{entity_type}(s)")
162
- end
163
- USR_GRP_SETTINGS.each do |setting|
164
- setting_ops = setting.eql?(:share_permissions) ? Operations::ALL : %i[show modify]
165
- lookup_method = :"lookup_shares_#{entity_type}_local_id"
166
- command(
167
- setting, description: "Manage #{setting} for a #{entity_type}",
168
- arguments: [{name: :entity_id, type: :identifier, lookup: lookup_method}]
169
- )
170
- commands_under([:admin, entity_type, :local, setting]) do
171
- setting_ops.each do |op|
172
- command(op, description: "#{op.capitalize} #{setting} for a #{entity_type}")
167
+ if entity_type.eql?(:group)
168
+ command :users,
169
+ description: 'Manage users of a group',
170
+ arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_all_id}]
171
+ commands_under :users do
172
+ GROUP_USERS_OPS.each do |op|
173
+ command op, description: "#{op.capitalize} users of a group"
174
+ end
175
+ end
173
176
  end
174
177
  end
175
- end
176
- if entity_type.eql?(:group)
177
- command(
178
- :users, description: 'Manage users of a group',
179
- arguments: [{name: :group_id, type: :identifier}]
180
- )
181
- commands_under([:admin, entity_type, :local, :users]) do
178
+
179
+ # local: full CRUD only — no nested settings (Rails does not nest transfer_settings/app_authorizations/share_permissions under local_users/local_groups)
180
+ commands_under :local do
181
+ lookup_method_local = :"lookup_shares_#{entity_type}_local_id"
182
182
  Operations::ALL.each do |op|
183
- command(op, description: "#{op.capitalize} users of a group")
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
186
+ if entity_type.eql?(:group)
187
+ command :users,
188
+ description: 'Manage users of a group',
189
+ arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_local_id}]
190
+ commands_under :users do
191
+ GROUP_USERS_OPS.each do |op|
192
+ command op, description: "#{op.capitalize} users of a group"
193
+ end
194
+ end
184
195
  end
185
196
  end
186
- end
187
- end
188
197
 
189
- # ldap: add only
190
- commands_under([:admin, entity_type, :ldap]) do
191
- command :add, description: "Add a LDAP #{entity_type}",
192
- arguments: [{name: :entity_name, type: String, bulk: true}]
193
- end
198
+ # ldap: add only
199
+ commands_under :ldap do
200
+ command :add, description: "Add a LDAP #{entity_type}",
201
+ arguments: [{name: :entity_name, type: String, bulk: true}]
202
+ end
194
203
 
195
- # saml: import only
196
- commands_under([:admin, entity_type, :saml]) do
197
- command :import, description: "Import a SAML #{entity_type}",
198
- arguments: [{name: :entity_parameters, type: Hash, bulk: true}]
204
+ # saml: import only
205
+ commands_under :saml do
206
+ command :import, description: "Import a SAML #{entity_type}",
207
+ arguments: [{name: :entity_parameters, type: Hash, bulk: true}]
208
+ end
209
+ end
199
210
  end
200
211
  end
201
212
 
@@ -210,42 +221,40 @@ module Aspera
210
221
  lookup_method = :"lookup_shares_#{entity_type}_#{location}_id"
211
222
  define_method(lookup_method) do |field, value, **|
212
223
  path = admin_entity_path(entity_type, location)
213
- RestList.lookup_entity_generic(entity: entity_type, field: field, value: value){@api_shares_admin.read(path)}['id']
224
+ RestList.lookup_entity_generic(entity: entity_type, field: field, value: value) { @api_shares_admin.read(path) }['id']
214
225
  end
215
226
  private lookup_method
216
227
  end
217
228
  end
218
229
 
219
- commands_under(%i[admin share]) do
220
- Operations::ALL.each do |op|
221
- command(op, description: "#{op.capitalize} share(s)", action: lambda do
222
- entity_execute(
223
- api: @api_shares_admin, entity: 'data/shares', command: op,
224
- display_fields: SHARE_DISPLAY_FIELDS
225
- ){ |f, v| lookup_share_id(f, v)}
226
- end)
227
- end
228
- command(
229
- :user_permissions, description: 'Manage user permissions on a share',
230
+ commands_under %i[admin share] do
231
+ crud_commands entity: 'data/shares',
232
+ api: :@api_shares_admin,
233
+ display_fields: SHARE_DISPLAY_FIELDS,
234
+ body_component: Schema::Registry::SHARES,
235
+ lookup: :lookup_share_id
236
+ command :user_permissions,
237
+ description: 'Manage user permissions on a share',
230
238
  arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
231
- )
232
- command(
233
- :group_permissions, description: 'Manage group permissions on a share',
239
+ command :group_permissions,
240
+ description: 'Manage group permissions on a share',
234
241
  arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
235
- )
236
242
  end
237
243
 
244
+ # share user_permissions and group_permissions: Rails only exposes index+show (read-only)
238
245
  %i[user_permissions group_permissions].each do |perm_type|
239
- commands_under([:admin, :share, perm_type]) do
240
- Operations::ALL.each do |op|
241
- command(op, description: "#{op.capitalize} #{perm_type}")
246
+ commands_under [:admin, :share, perm_type] do
247
+ SHARE_PERMISSIONS_OPS.each do |op|
248
+ command op, description: "#{op.capitalize} #{perm_type}"
242
249
  end
243
250
  end
244
251
  end
245
252
 
246
- commands_under(%i[admin transfer_settings]) do
247
- entity_command :show, description: 'Show transfer settings', api: :@api_shares_admin, entity: 'data/transfer_settings', command: :show, is_singleton: true
248
- entity_command :modify, description: 'Modify transfer settings', api: :@api_shares_admin, entity: 'data/transfer_settings', command: :modify, is_singleton: true
253
+ commands_under %i[admin transfer_settings] do
254
+ crud_commands entity: 'data/transfer_settings',
255
+ api: :@api_shares_admin,
256
+ is_singleton: true,
257
+ operations: %i[show modify]
249
258
  end
250
259
 
251
260
  # --- setup ---
@@ -266,7 +275,7 @@ module Aspera
266
275
 
267
276
  # Lookup a share id by field/value using the admin API.
268
277
  def lookup_share_id(field, value, **)
269
- RestList.lookup_entity_generic(entity: 'share', field: field, value: value){@api_shares_admin.read('data/shares')}['id']
278
+ RestList.lookup_entity_generic(entity: 'share', field: field, value: value) { @api_shares_admin.read('data/shares') }['id']
270
279
  end
271
280
 
272
281
  # --- health ---
@@ -291,16 +300,39 @@ module Aspera
291
300
 
292
301
  # --- files sub-commands (restricted to COMMANDS_SHARES, delegated to Node) ---
293
302
 
294
- commands_under(:files) do
303
+ commands_under :files do
295
304
  Node::COMMANDS_SHARES.each do |cmd|
296
- command(cmd, description: "Node #{cmd} command")
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)
297
316
  end
298
317
  end
299
318
 
300
- # One handler per COMMANDS_SHARES command.
301
- Node::COMMANDS_SHARES.each do |cmd|
302
- define_action_method([:files, cmd]) do |shares_node_plugin:|
303
- shares_node_plugin.dispatch_v3_command(cmd)
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)
304
336
  end
305
337
  end
306
338
 
@@ -320,10 +352,13 @@ module Aspera
320
352
  # @param op [Symbol] CRUD operation
321
353
  def action_admin_entity_crud(entity_type, location, op)
322
354
  path = admin_entity_path(entity_type, location)
323
- lookup = ->(f, v){RestList.lookup_entity_generic(entity: entity_type, field: f, value: v){@api_shares_admin.read(path)}['id']}
355
+ lookup = ->(f, v) { RestList.lookup_entity_generic(entity: entity_type, field: f, value: v) { @api_shares_admin.read(path) }['id'] }
324
356
  display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
325
357
  display_fields&.push('directory_user') if entity_type.eql?(:user) && location.eql?(:all)
326
- entity_execute(api: @api_shares_admin, entity: path, command: op, display_fields: display_fields, &lookup)
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)
327
362
  end
328
363
 
329
364
  # Shared handler for USR_GRP_SETTINGS (transfer_settings, app_authorizations, share_permissions)
@@ -331,15 +366,13 @@ module Aspera
331
366
  # @param location [Symbol] :all or :local
332
367
  # @param setting [Symbol] one of USR_GRP_SETTINGS
333
368
  # @param op [Symbol] CRUD operation
334
- # entity_id: resolved by Phase A via arguments:(:identifier) on the setting node
335
- def action_admin_entity_setting(entity_type, location, setting, op, entity_id:, **)
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, **)
336
371
  path = admin_entity_path(entity_type, location)
337
- entity_execute(
338
- api: @api_shares_admin,
339
- entity: "#{path}/#{entity_id}/#{setting}",
340
- command: op,
341
- is_singleton: !setting.eql?(:share_permissions)
342
- ){ |f, v| lookup_share_id(f, v)}
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)
343
376
  end
344
377
 
345
378
  # Shared handler for :users (group only)
@@ -347,29 +380,35 @@ module Aspera
347
380
  def action_admin_entity_users(entity_type, location, op, group_id:, **)
348
381
  path = admin_entity_path(entity_type, location)
349
382
  prefix = location.eql?(:all) ? '' : "#{location}_"
350
- entity_execute(api: @api_shares_admin, entity: "#{path}/#{group_id}/#{prefix}users", command: op)
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
385
  end
352
386
 
353
387
  # Generate action_admin_<user|group>_<location>_<verb> for all combinations
354
388
  %i[user group].each do |entity_type|
355
- # all + local: CRUD + USR_GRP_SETTINGS [+ users for group]
389
+ # all: list/show/delete only (no create, no modify — no update route on /users and /groups)
390
+ # local: full CRUD only — no nested settings under local_users/local_groups
356
391
  ENTITY_LOCATIONS.each do |location|
357
- ops = location.eql?(:all) ? (Operations::ALL - [:create]) : Operations::ALL
392
+ ops = location.eql?(:all) ? (Operations::ALL - %i[create modify]) : Operations::ALL
358
393
  ops.each do |op|
359
394
  define_action_method([:admin, entity_type, location, op]) do
360
395
  action_admin_entity_crud(entity_type, location, op)
361
396
  end
362
397
  end
363
- USR_GRP_SETTINGS.each do |setting|
364
- setting_ops = setting.eql?(:share_permissions) ? Operations::ALL : %i[show modify]
365
- setting_ops.each do |op|
366
- define_action_method([:admin, entity_type, location, setting, op]) do |entity_id:, **|
367
- action_admin_entity_setting(entity_type, location, setting, op, entity_id: entity_id)
398
+ unless location.eql?(:local)
399
+ USR_GRP_SETTINGS.each do |setting|
400
+ # share_permissions: Rails only exposes index+show (read-only)
401
+ setting_ops = setting.eql?(:share_permissions) ? SHARE_PERMISSIONS_OPS : %i[show modify]
402
+ setting_ops.each do |op|
403
+ 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])
405
+ end
368
406
  end
369
407
  end
370
408
  end
371
409
  next unless entity_type.eql?(:group)
372
- Operations::ALL.each do |op|
410
+ # group users: no create route (Rails only exposes index+show+update+destroy)
411
+ GROUP_USERS_OPS.each do |op|
373
412
  define_action_method([:admin, entity_type, location, :users, op]) do |group_id:, **|
374
413
  action_admin_entity_users(entity_type, location, op, group_id: group_id)
375
414
  end
@@ -379,8 +418,7 @@ module Aspera
379
418
  # ldap: add
380
419
  define_action_method([:admin, entity_type, :ldap, :add]) do |entity_name:, **|
381
420
  path = admin_entity_path(entity_type, :ldap)
382
- is_bulk = options.get_option(:bulk)
383
- Result.bulk(entity_name, is_bulk: is_bulk, command: :add, id_result: entity_type.to_s, bfail: options.get_option(:bfail)) do |entity_name|
421
+ bulk_result(entity_name, command: :add, id_result: entity_type.to_s) do |entity_name|
384
422
  @api_shares_admin.create(path, {entity_type => entity_name})
385
423
  end
386
424
  end
@@ -388,13 +426,12 @@ module Aspera
388
426
  # saml: import
389
427
  define_action_method([:admin, entity_type, :saml, :import]) do |entity_parameters:, **|
390
428
  path = admin_entity_path(entity_type, :saml)
391
- is_bulk = options.get_option(:bulk)
392
- Result.bulk(entity_parameters, is_bulk: is_bulk, command: :import, bfail: options.get_option(:bfail)) do |entity_parameters|
393
- entity_parameters = entity_parameters.transform_keys{ |k| k.gsub(/\s+/, '_').downcase}
429
+ bulk_result(entity_parameters, command: :import) do |entity_parameters|
430
+ entity_parameters = entity_parameters.transform_keys { |k| k.gsub(/\s+/, '_').downcase }
394
431
  Aspera.assert_type(entity_parameters, Hash)
395
- SAML_IMPORT_MANDATORY.each{ |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil?}
432
+ SAML_IMPORT_MANDATORY.each { |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil? }
396
433
  entity_parameters.each_key do |p|
397
- Aspera.assert_values(p, SAML_IMPORT_ALLOWED){'SAML field'}
434
+ Aspera.assert_values(p, SAML_IMPORT_ALLOWED) { 'SAML field' }
398
435
  end
399
436
  @api_shares_admin.create("#{path}/import", entity_parameters)
400
437
  end
@@ -403,10 +440,12 @@ module Aspera
403
440
 
404
441
  # Handlers for admin > share > user_permissions|group_permissions > op
405
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)
406
444
  %i[user_permissions group_permissions].each do |perm_type|
407
- Operations::ALL.each do |op|
445
+ SHARE_PERMISSIONS_OPS.each do |op|
408
446
  define_action_method([:admin, :share, perm_type, op]) do |share_id:, **|
409
- entity_execute(api: @api_shares_admin, entity: "data/shares/#{share_id}/#{perm_type}", command: op)
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)
410
449
  end
411
450
  end
412
451
  end
@@ -30,40 +30,57 @@ module Aspera
30
30
  end
31
31
 
32
32
  def action_preset_lookup(**)
33
- Plugins::BasicAuth.declare_options(options)
33
+ Plugins::BasicAuth.declare_options(options, parse: true)
34
34
  url = options.get_option(:url, mandatory: true)
35
35
  user = options.get_option(:username, mandatory: true)
36
36
  result = presets.lookup_preset(url: url, username: user)
37
- Aspera.assert(!result.nil?, type: Error){'no such config found'}
37
+ Aspera.assert(!result.nil?, type: Error) { 'no such config found' }
38
38
  Result::SingleObject.new(result)
39
39
  end
40
40
 
41
41
  def action_preset_secure(config_name: nil, **)
42
42
  cp = presets.config_presets
43
43
  preset_names = config_name.nil? ? cp.keys : [config_name]
44
- secret_keywords = %w[password secret].freeze
45
44
  preset_names.each do |preset_name|
46
45
  preset = cp[preset_name]
47
46
  next unless preset.is_a?(Hash)
48
47
  preset.each_key do |option_name|
49
- secret_keywords.each do |keyword|
50
- next unless option_name.end_with?(keyword)
51
- vault_label = preset_name
52
- incr = 0
53
- until vault.get(label: vault_label, exception: false).nil?
54
- vault_label = "#{preset_name}#{incr}"
55
- incr += 1
56
- end
57
- to_set = {label: vault_label, password: preset[option_name]}
58
- puts "need to encode #{preset_name}.#{option_name} -> #{vault_label} -> #{to_set}"
59
- vault.set(to_set)
60
- preset[option_name] = "@vault:#{vault_label}.password"
61
- end
48
+ secure_preset_option(preset, preset_name, option_name)
62
49
  end
63
50
  end
64
51
  Result::Status.new('Secrets secured in vault: Make sure to save the vault password securely.')
65
52
  end
66
53
 
54
+ private
55
+
56
+ SECRET_KEYWORDS = %w[password secret].freeze
57
+
58
+ # If +option_name+ ends with a secret keyword and the vault is configured,
59
+ # move the clear-text value into the vault and replace it with a @vault: reference.
60
+ # @param preset [Hash] the preset hash (modified in place)
61
+ # @param preset_name [String] name used as base for the vault label
62
+ # @param option_name [String] option key to inspect
63
+ def secure_preset_option(preset, preset_name, option_name)
64
+ return unless SECRET_KEYWORDS.any? { |kw| option_name.end_with?(kw) }
65
+ # Never auto-secure the global preset: it holds vault credentials themselves
66
+ return if preset_name.eql?(presets.global_default_preset)
67
+ return if vault.nil?
68
+ value = preset[option_name]
69
+ # Already a vault reference or nil — nothing to do
70
+ return if value.nil? || value.to_s.start_with?('@vault:')
71
+ vault_label = preset_name
72
+ incr = 0
73
+ until vault.get(label: vault_label, exception: false).nil?
74
+ vault_label = "#{preset_name}#{incr}"
75
+ incr += 1
76
+ end
77
+ Log.log.info { "Securing #{preset_name}.#{option_name} -> vault label: #{vault_label}" }
78
+ vault.set({label: vault_label, password: value})
79
+ preset[option_name] = "@vault:#{vault_label}.password"
80
+ end
81
+
82
+ public
83
+
67
84
  def action_preset_show(name:, **)
68
85
  name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
69
86
  cp = presets.config_presets
@@ -102,14 +119,15 @@ module Aspera
102
119
  def action_preset_set(name:, param_name:, param_value:, **)
103
120
  name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
104
121
  param_name = Parser.option_line_to_name(param_name)
105
- presets.set_key(name, param_name, param_value)
122
+ presets.set_key(name, param_name, Parser.smart_convert(param_value))
123
+ secure_preset_option(presets.config_presets[name], name, param_name)
106
124
  Result::Nothing.new
107
125
  end
108
126
 
109
127
  def action_preset_initialize(name:, config_value:, **)
110
128
  name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
111
129
  cp = presets.config_presets
112
- Log.log.warn{"configuration already exists: #{name}, overwriting"} if cp.key?(name)
130
+ Log.log.warn { "configuration already exists: #{name}, overwriting" } if cp.key?(name)
113
131
  cp[name] = config_value
114
132
  Result::Status.new("Modified: #{@option_config_file}")
115
133
  end
@@ -117,10 +135,11 @@ module Aspera
117
135
  def action_preset_update(name:, **)
118
136
  name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
119
137
  unprocessed_options = options.unprocessed_options_with_value
120
- Log.log.debug{"opts=#{unprocessed_options}"}
138
+ Log.dump(:opts, unprocessed_options)
121
139
  cp = presets.config_presets
122
140
  cp[name] ||= {}
123
141
  cp[name].merge!(unprocessed_options)
142
+ unprocessed_options.each_key { |k| secure_preset_option(cp[name], name, k) }
124
143
  Result::Status.new("Updated: #{name}")
125
144
  end
126
145
 
@@ -131,6 +150,7 @@ module Aspera
131
150
  option_names.each do |option_name|
132
151
  option_value = options.get_interactive(option_name, check_option: true)
133
152
  cp[name][option_name] = option_value
153
+ secure_preset_option(cp[name], name, option_name)
134
154
  end
135
155
  Result::Status.new("Updated: #{name}")
136
156
  end