aspera-cli 4.26.2 → 4.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +281 -21
  4. data/CONTRIBUTING.md +98 -7
  5. data/bin/ascli +3 -3
  6. data/docs/README.md +10757 -0
  7. data/docs/test-mcp-with-ai.md +202 -0
  8. data/lib/aspera/agent/base.rb +20 -6
  9. data/lib/aspera/agent/connect.rb +53 -16
  10. data/lib/aspera/agent/desktop.rb +52 -17
  11. data/lib/aspera/agent/direct.rb +26 -24
  12. data/lib/aspera/agent/httpgw.rb +1 -1
  13. data/lib/aspera/agent/node.rb +32 -0
  14. data/lib/aspera/agent/transferd.rb +39 -3
  15. data/lib/aspera/api/alee.rb +1 -1
  16. data/lib/aspera/api/aoc.rb +14 -16
  17. data/lib/aspera/api/cos_node.rb +1 -1
  18. data/lib/aspera/api/faspex.rb +15 -14
  19. data/lib/aspera/api/httpgw.rb +4 -4
  20. data/lib/aspera/api/node.rb +26 -23
  21. data/lib/aspera/api/queries/bss_subscription_account.graphql +18 -0
  22. data/lib/aspera/api/queries/bss_subscription_usage.graphql +18 -0
  23. data/lib/aspera/ascmd.rb +21 -19
  24. data/lib/aspera/ascp/installation.rb +37 -38
  25. data/lib/aspera/ascp/management.rb +2 -3
  26. data/lib/aspera/cli/ascp_actions.rb +155 -0
  27. data/lib/aspera/cli/async_transfer_store.rb +81 -0
  28. data/lib/aspera/cli/bootstrapper.rb +197 -0
  29. data/lib/aspera/cli/command_registry.rb +145 -0
  30. data/lib/aspera/cli/command_spec.rb +138 -0
  31. data/lib/aspera/cli/context.rb +16 -3
  32. data/lib/aspera/cli/error.rb +16 -0
  33. data/lib/aspera/cli/extended_value.rb +2 -1
  34. data/lib/aspera/cli/formatter.rb +41 -27
  35. data/lib/aspera/cli/gem_checker.rb +65 -0
  36. data/lib/aspera/cli/hints.rb +2 -2
  37. data/lib/aspera/cli/http.rb +70 -19
  38. data/lib/aspera/cli/info.rb +2 -0
  39. data/lib/aspera/cli/mailer.rb +97 -0
  40. data/lib/aspera/cli/mcp_tool.rb +198 -0
  41. data/lib/aspera/cli/options.schema.yaml +364 -7
  42. data/lib/aspera/cli/{manager.rb → parser.rb} +334 -142
  43. data/lib/aspera/cli/plugins/alee.rb +20 -22
  44. data/lib/aspera/cli/plugins/aoc.rb +1166 -853
  45. data/lib/aspera/cli/plugins/ats.rb +200 -161
  46. data/lib/aspera/cli/plugins/base.rb +457 -100
  47. data/lib/aspera/cli/plugins/basic_auth.rb +14 -4
  48. data/lib/aspera/cli/plugins/config.rb +428 -754
  49. data/lib/aspera/cli/plugins/console.rb +106 -64
  50. data/lib/aspera/cli/plugins/cos.rb +44 -32
  51. data/lib/aspera/cli/plugins/factory.rb +4 -4
  52. data/lib/aspera/cli/plugins/faspex.rb +294 -257
  53. data/lib/aspera/cli/plugins/faspex5.rb +585 -357
  54. data/lib/aspera/cli/plugins/faspio.rb +49 -51
  55. data/lib/aspera/cli/plugins/httpgw.rb +18 -25
  56. data/lib/aspera/cli/plugins/mcp.rb +279 -0
  57. data/lib/aspera/cli/plugins/node.rb +998 -794
  58. data/lib/aspera/cli/plugins/oauth.rb +7 -10
  59. data/lib/aspera/cli/plugins/orchestrator.rb +111 -134
  60. data/lib/aspera/cli/plugins/preview.rb +180 -139
  61. data/lib/aspera/cli/plugins/server.rb +124 -79
  62. data/lib/aspera/cli/plugins/shares.rb +301 -107
  63. data/lib/aspera/cli/preset_actions.rb +139 -0
  64. data/lib/aspera/cli/preset_manager.rb +13 -12
  65. data/lib/aspera/cli/result.rb +61 -11
  66. data/lib/aspera/cli/runner.rb +275 -141
  67. data/lib/aspera/cli/secret_finder.rb +40 -0
  68. data/lib/aspera/cli/special_values.rb +1 -0
  69. data/lib/aspera/cli/sync_actions.rb +83 -59
  70. data/lib/aspera/cli/terminal_formatter.rb +1 -1
  71. data/lib/aspera/cli/transfer_actions.rb +83 -0
  72. data/lib/aspera/cli/transfer_agent.rb +116 -51
  73. data/lib/aspera/cli/transfer_progress.rb +3 -3
  74. data/lib/aspera/cli/vault_manager.rb +57 -0
  75. data/lib/aspera/cli/version.rb +1 -1
  76. data/lib/aspera/cli/wizard.rb +21 -20
  77. data/lib/aspera/command_line_builder.rb +22 -22
  78. data/lib/aspera/data_repository.rb +2 -1
  79. data/lib/aspera/dot_container.rb +4 -4
  80. data/lib/aspera/environment.rb +18 -11
  81. data/lib/aspera/exec_spec.rb +13 -0
  82. data/lib/aspera/faspex_gw.rb +1 -1
  83. data/lib/aspera/faspex_postproc.rb +4 -3
  84. data/lib/aspera/graphql.rb +35 -0
  85. data/lib/aspera/hash_ext.rb +6 -0
  86. data/lib/aspera/json_rpc/client.rb +62 -0
  87. data/lib/aspera/json_rpc/version.rb +7 -0
  88. data/lib/aspera/keychain/encrypted_hash.rb +1 -1
  89. data/lib/aspera/keychain/factory.rb +3 -3
  90. data/lib/aspera/keychain/macos_security.rb +3 -3
  91. data/lib/aspera/link_header.rb +82 -0
  92. data/lib/aspera/log.rb +17 -3
  93. data/lib/aspera/markdown.rb +85 -2
  94. data/lib/aspera/node_simulator.rb +3 -1
  95. data/lib/aspera/oauth/base.rb +5 -5
  96. data/lib/aspera/oauth/boot.rb +2 -2
  97. data/lib/aspera/oauth/factory.rb +7 -5
  98. data/lib/aspera/oauth/jwt.rb +4 -4
  99. data/lib/aspera/oauth/web.rb +2 -2
  100. data/lib/aspera/persistency_action_once.rb +8 -8
  101. data/lib/aspera/persistency_folder.rb +10 -3
  102. data/lib/aspera/preview/file_types.rb +3 -3
  103. data/lib/aspera/preview/generator.rb +3 -3
  104. data/lib/aspera/preview/utils.rb +1 -1
  105. data/lib/aspera/products/connect.rb +1 -1
  106. data/lib/aspera/products/other.rb +1 -1
  107. data/lib/aspera/products/transferd.rb +1 -1
  108. data/lib/aspera/proxy_auto_config.rb +7 -6
  109. data/lib/aspera/rest.rb +10 -36
  110. data/lib/aspera/rest_error_analyzer.rb +3 -3
  111. data/lib/aspera/rest_errors_aspera.rb +0 -10
  112. data/lib/aspera/rest_list.rb +3 -3
  113. data/lib/aspera/schema/IBM Aspera on Cloud API-0.2.6-enhanced.yaml +1853 -510
  114. data/lib/aspera/schema/documentation.rb +57 -26
  115. data/lib/aspera/schema/reader.rb +59 -10
  116. data/lib/aspera/schema/registry.rb +39 -6
  117. data/lib/aspera/secret_hider.rb +7 -2
  118. data/lib/aspera/ssl.rb +4 -4
  119. data/lib/aspera/sync/conf.schema.yaml +2 -2
  120. data/lib/aspera/sync/operations.rb +10 -13
  121. data/lib/aspera/temp_file_manager.rb +1 -1
  122. data/lib/aspera/timer_limiter.rb +1 -1
  123. data/lib/aspera/transfer/faux_file.rb +24 -11
  124. data/lib/aspera/transfer/parameters.rb +23 -21
  125. data/lib/aspera/transfer/result.rb +74 -0
  126. data/lib/aspera/transfer/resumer.rb +6 -6
  127. data/lib/aspera/transfer/spec.rb +18 -1
  128. data/lib/aspera/transfer/spec.schema.yaml +12 -3
  129. data/lib/aspera/uri_reader.rb +2 -2
  130. data/lib/aspera/web_auth.rb +2 -2
  131. data/lib/aspera/web_server_simple.rb +8 -5
  132. data/lib/aspera/yaml.rb +2 -1
  133. data.tar.gz.sig +0 -0
  134. metadata +25 -3
  135. metadata.gz.sig +0 -0
  136. data/lib/aspera/json_rpc.rb +0 -52
@@ -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
- raise 'not Shares' unless login_page.include?('aspera-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]+)/))
@@ -91,129 +91,323 @@ module Aspera
91
91
 
92
92
  def initialize(**_)
93
93
  super
94
+ @api_shares_admin = nil
94
95
  end
95
96
 
96
97
  SAML_IMPORT_MANDATORY = %w[id name_id].freeze
97
98
  SAML_IMPORT_ALLOWED = %w[email given_name surname].concat(SAML_IMPORT_MANDATORY).freeze
98
99
 
99
- ACTIONS = %i[health info files admin].freeze
100
100
  # common to users and groups
101
101
  USR_GRP_SETTINGS = %i[transfer_settings app_authorizations share_permissions].freeze
102
102
 
103
- def execute_action
104
- command = options.get_next_command(ACTIONS, aliases: {repository: :files})
105
- case command
106
- when :health
107
- nagios = Nagios.new
108
- shares_url = options.get_option(:url, mandatory: true)
109
- health = self.class.health_check(shares_url)
110
- nagios.add_ok('version', health[:version]) if health[:version].is_a?(String)
111
- if health[:ping].is_a?(String)
112
- nagios.add_ok('ping', health[:ping])
113
- else
114
- nagios.add_critical('ping', health[:ping].to_s)
103
+ # --- DSL ---
104
+
105
+ 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'}))}
107
+ command :files, description: 'Browse and transfer files on Shares', aliases: [:repository], setup: :setup_shares_node
108
+ command :admin, description: 'Administer Shares', setup: :setup_admin
109
+
110
+ commands_under(:admin) do
111
+ entity_command :node, api: :@api_shares_admin, entity: 'data/nodes'
112
+ command :share, description: 'Manage shares'
113
+ command :transfer_settings, description: 'Manage transfer settings'
114
+ command :user, description: 'Manage users'
115
+ 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
+ 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)")
115
131
  end
116
- if health[:api].is_a?(String)
117
- nagios.add_ok('API', health[:api])
118
- else
119
- nagios.add_critical('API', health[:api].to_s)
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}")
142
+ end
143
+ end
120
144
  end
121
- Result::ObjectList.new(nagios.status_list)
122
- when :info
123
- return Result::SingleObject.new(basic_auth_api(NODE_API_PATH).read('info', headers: {'Content-Type'=>'application/json'}))
124
- when :files
125
- api_shares_node = basic_auth_api(NODE_API_PATH)
126
- repo_command = options.get_next_command(Node::COMMANDS_SHARES)
127
- return Node
128
- .new(context: context, api: api_shares_node)
129
- .execute_action(repo_command)
130
- when :admin
131
- api_shares_admin = basic_auth_api(ADMIN_API_PATH)
132
- admin_command = options.get_next_command(%i[node share transfer_settings user group].freeze)
133
- lookup_share = ->(field, value){RestList.lookup_entity_generic(entity: 'share', field: field, value: value){api_shares_admin.read('data/shares')}['id']}
134
- case admin_command
135
- when :node
136
- return entity_execute(api: api_shares_admin, entity: 'data/nodes')
137
- when :share
138
- share_command = options.get_next_command(%i[user_permissions group_permissions].concat(Operations::ALL))
139
- case share_command
140
- when *Operations::ALL
141
- return entity_execute(
142
- api: api_shares_admin,
143
- entity: 'data/shares',
144
- command: share_command,
145
- display_fields: %w[id name node_id directory percent_free],
146
- &lookup_share
147
- )
148
- when :user_permissions, :group_permissions
149
- share_id = options.instance_identifier(&lookup_share)
150
- return entity_execute(api: api_shares_admin, entity: "data/shares/#{share_id}/#{share_command}")
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")
153
+ end
151
154
  end
152
- when :transfer_settings
153
- xfer_settings_command = options.get_next_command(%i[show modify])
154
- return entity_execute(
155
- api: api_shares_admin,
156
- entity: 'data/transfer_settings',
157
- command: xfer_settings_command,
158
- is_singleton: true
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}]
159
169
  )
160
- when :user, :group
161
- entity_type = admin_command
162
- entities_location = options.get_next_command(%i[all local ldap saml])
163
- entities_prefix = entities_location.eql?(:all) ? '' : "#{entities_location}_"
164
- entities_path = "data/#{entities_prefix}#{entity_type}s"
165
- entity_commands = nil
166
- case entities_location
167
- when :all
168
- entity_commands = %i[list show delete]
169
- entity_commands.concat(USR_GRP_SETTINGS)
170
- entity_commands.push(:users) if entity_type.eql?(:group)
171
- entity_commands.freeze
172
- when :local
173
- entity_commands = %i[list show delete create modify]
174
- entity_commands.push(:users) if entity_type.eql?(:group)
175
- entity_commands.freeze
176
- when :ldap
177
- entity_commands = %i[add].freeze
178
- when :saml
179
- entity_commands = %i[import].freeze
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}")
173
+ end
180
174
  end
181
- entity_verb = options.get_next_command(entity_commands)
182
- lookup_block = ->(field, value){RestList.lookup_entity_generic(entity: entity_type, field: field, value: value){api_shares_admin.read(entities_path)}['id']}
183
- case entity_verb
184
- when *Operations::ALL
185
- display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
186
- display_fields.push('directory_user') if entity_type.eql?(:user) && entities_location.eql?(:all)
187
- return entity_execute(
188
- api: api_shares_admin,
189
- entity: entities_path,
190
- command: entity_verb,
191
- display_fields: display_fields,
192
- &lookup_block
193
- )
194
- when *USR_GRP_SETTINGS # transfer_settings, app_authorizations, share_permissions
195
- group_id = options.instance_identifier(&lookup_block)
196
- entities_path = "#{entities_path}/#{group_id}/#{entity_verb}"
197
- return entity_execute(api: api_shares_admin, entity: entities_path, is_singleton: !entity_verb.eql?(:share_permissions), &lookup_share)
198
- when :import # saml
199
- return do_bulk_operation(command: entity_verb, descr: 'user information') do |entity_parameters|
200
- entity_parameters = entity_parameters.transform_keys{ |k| k.gsub(/\s+/, '_').downcase}
201
- Aspera.assert_type(entity_parameters, Hash)
202
- SAML_IMPORT_MANDATORY.each{ |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil?}
203
- entity_parameters.each_key do |p|
204
- raise "unsupported field: #{p}, use: #{SAML_IMPORT_ALLOWED.join(',')}" unless SAML_IMPORT_ALLOWED.include?(p)
205
- end
206
- api_shares_admin.create("#{entities_path}/import", entity_parameters)
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
182
+ Operations::ALL.each do |op|
183
+ command(op, description: "#{op.capitalize} users of a group")
207
184
  end
208
- when :add # ldap
209
- return do_bulk_operation(command: entity_verb, descr: "#{entity_type} name", values: String) do |entity_name|
210
- api_shares_admin.create(entities_path, {entity_type=>entity_name})
185
+ end
186
+ end
187
+ end
188
+
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
194
+
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}]
199
+ end
200
+ end
201
+
202
+ ENTITY_LOCATIONS = %i[all local].freeze
203
+ SHARE_DISPLAY_FIELDS = %w[id name node_id directory percent_free].freeze
204
+ private_constant :ENTITY_LOCATIONS, :SHARE_DISPLAY_FIELDS
205
+
206
+ # Lookup methods used by arguments:(:identifier, lookup:) on setting nodes.
207
+ # One method per entity_type/location combination; each returns the entity id.
208
+ %i[user group].each do |entity_type|
209
+ ENTITY_LOCATIONS.each do |location|
210
+ lookup_method = :"lookup_shares_#{entity_type}_#{location}_id"
211
+ define_method(lookup_method) do |field, value, **|
212
+ 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']
214
+ end
215
+ private lookup_method
216
+ end
217
+ end
218
+
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
+ arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
231
+ )
232
+ command(
233
+ :group_permissions, description: 'Manage group permissions on a share',
234
+ arguments: [{name: :share_id, type: :identifier, lookup: :lookup_share_id}]
235
+ )
236
+ end
237
+
238
+ %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}")
242
+ end
243
+ end
244
+ end
245
+
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
249
+ end
250
+
251
+ # --- setup ---
252
+
253
+ # Build the Shares node API plugin and inject into ctx.
254
+ # @return [Hash] context hash containing :shares_node_plugin
255
+ def setup_shares_node(**)
256
+ api_shares_node = basic_auth_api(NODE_API_PATH)
257
+ {shares_node_plugin: Node.new(context: context, api: api_shares_node)}
258
+ end
259
+
260
+ # Build the admin REST API and store in ivar.
261
+ # @return [Hash] empty ctx (state stored in @api_shares_admin)
262
+ def setup_admin(**)
263
+ @api_shares_admin = basic_auth_api(ADMIN_API_PATH)
264
+ {}
265
+ end
266
+
267
+ # Lookup a share id by field/value using the admin API.
268
+ def lookup_share_id(field, value, **)
269
+ RestList.lookup_entity_generic(entity: 'share', field: field, value: value){@api_shares_admin.read('data/shares')}['id']
270
+ end
271
+
272
+ # --- health ---
273
+
274
+ def action_health
275
+ nagios = Nagios.new
276
+ shares_url = options.get_option(:url, mandatory: true)
277
+ health = self.class.health_check(shares_url)
278
+ nagios.add_ok('version', health[:version]) if health[:version].is_a?(String)
279
+ if health[:ping].is_a?(String)
280
+ nagios.add_ok('ping', health[:ping])
281
+ else
282
+ nagios.add_critical('ping', health[:ping].to_s)
283
+ end
284
+ if health[:api].is_a?(String)
285
+ nagios.add_ok('API', health[:api])
286
+ else
287
+ nagios.add_critical('API', health[:api].to_s)
288
+ end
289
+ Result::ObjectList.new(nagios.status_list)
290
+ end
291
+
292
+ # --- files sub-commands (restricted to COMMANDS_SHARES, delegated to Node) ---
293
+
294
+ commands_under(:files) do
295
+ Node::COMMANDS_SHARES.each do |cmd|
296
+ command(cmd, description: "Node #{cmd} command")
297
+ end
298
+ end
299
+
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)
304
+ end
305
+ end
306
+
307
+ private
308
+
309
+ # @return [String] admin API path for the given entity_type and location
310
+ def admin_entity_path(entity_type, location)
311
+ prefix = location.eql?(:all) ? '' : "#{location}_"
312
+ "data/#{prefix}#{entity_type}s"
313
+ end
314
+
315
+ # --- admin > user|group handlers ---
316
+
317
+ # Shared handler for list/show/delete/create/modify on user or group.
318
+ # @param entity_type [Symbol] :user or :group
319
+ # @param location [Symbol] :all or :local
320
+ # @param op [Symbol] CRUD operation
321
+ def action_admin_entity_crud(entity_type, location, op)
322
+ 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']}
324
+ display_fields = entity_type.eql?(:user) ? %w[id user_id username first_name last_name email] : nil
325
+ 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)
327
+ end
328
+
329
+ # Shared handler for USR_GRP_SETTINGS (transfer_settings, app_authorizations, share_permissions)
330
+ # @param entity_type [Symbol] :user or :group
331
+ # @param location [Symbol] :all or :local
332
+ # @param setting [Symbol] one of USR_GRP_SETTINGS
333
+ # @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:, **)
336
+ 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)}
343
+ end
344
+
345
+ # Shared handler for :users (group only)
346
+ # group_id: resolved by Phase A via arguments:(:identifier) on the :users node
347
+ def action_admin_entity_users(entity_type, location, op, group_id:, **)
348
+ path = admin_entity_path(entity_type, location)
349
+ prefix = location.eql?(:all) ? '' : "#{location}_"
350
+ entity_execute(api: @api_shares_admin, entity: "#{path}/#{group_id}/#{prefix}users", command: op)
351
+ end
352
+
353
+ # Generate action_admin_<user|group>_<location>_<verb> for all combinations
354
+ %i[user group].each do |entity_type|
355
+ # all + local: CRUD + USR_GRP_SETTINGS [+ users for group]
356
+ ENTITY_LOCATIONS.each do |location|
357
+ ops = location.eql?(:all) ? (Operations::ALL - [:create]) : Operations::ALL
358
+ ops.each do |op|
359
+ define_action_method([:admin, entity_type, location, op]) do
360
+ action_admin_entity_crud(entity_type, location, op)
361
+ end
362
+ 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)
211
368
  end
212
- when :users # group
213
- return entity_execute(api: api_shares_admin, entity: "#{entities_path}/#{options.instance_identifier(&lookup_block)}/#{entities_prefix}users")
214
- else Aspera.error_unexpected_value(entity_verb)
215
369
  end
216
370
  end
371
+ next unless entity_type.eql?(:group)
372
+ Operations::ALL.each do |op|
373
+ define_action_method([:admin, entity_type, location, :users, op]) do |group_id:, **|
374
+ action_admin_entity_users(entity_type, location, op, group_id: group_id)
375
+ end
376
+ end
377
+ end
378
+
379
+ # ldap: add
380
+ define_action_method([:admin, entity_type, :ldap, :add]) do |entity_name:, **|
381
+ 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|
384
+ @api_shares_admin.create(path, {entity_type => entity_name})
385
+ end
386
+ end
387
+
388
+ # saml: import
389
+ define_action_method([:admin, entity_type, :saml, :import]) do |entity_parameters:, **|
390
+ 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}
394
+ Aspera.assert_type(entity_parameters, Hash)
395
+ SAML_IMPORT_MANDATORY.each{ |p| raise "missing mandatory field: #{p}" if entity_parameters[p].nil?}
396
+ entity_parameters.each_key do |p|
397
+ Aspera.assert_values(p, SAML_IMPORT_ALLOWED){'SAML field'}
398
+ end
399
+ @api_shares_admin.create("#{path}/import", entity_parameters)
400
+ end
401
+ end
402
+ end
403
+
404
+ # Handlers for admin > share > user_permissions|group_permissions > op
405
+ # share_id: resolved by Phase A via arguments:(:identifier, lookup: :lookup_share_id) on the perm_type node
406
+ %i[user_permissions group_permissions].each do |perm_type|
407
+ Operations::ALL.each do |op|
408
+ 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)
410
+ end
217
411
  end
218
412
  end
219
413
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aspera/assert'
4
+
5
+ module Aspera
6
+ module Cli
7
+ # Mixin for Config plugin: preset CRUD actions
8
+ module PresetActions
9
+ # Used to identify the global default preset keyword
10
+ GLOBAL_DEFAULT_KEYWORD = 'GLOBAL'
11
+ # Display columns for preset overview
12
+ CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
13
+
14
+ def action_preset_list(**)
15
+ Result::ValueList.new(presets.config_presets.keys, name: 'name')
16
+ end
17
+
18
+ def action_preset_overview(**)
19
+ cp = presets.config_presets
20
+ # Display process modifies the value (hide secrets): we do not want to save removed secrets
21
+ data = PresetManager.deep_clone(cp)
22
+ formatter.hide_secrets(data)
23
+ result = []
24
+ data.each do |config, preset|
25
+ preset.each do |parameter, value|
26
+ result.push(CONF_OVERVIEW_KEYS.zip([config, parameter, value]).to_h)
27
+ end
28
+ end
29
+ Result::ObjectList.new(result, fields: CONF_OVERVIEW_KEYS)
30
+ end
31
+
32
+ def action_preset_lookup(**)
33
+ Plugins::BasicAuth.declare_options(options)
34
+ url = options.get_option(:url, mandatory: true)
35
+ user = options.get_option(:username, mandatory: true)
36
+ result = presets.lookup_preset(url: url, username: user)
37
+ Aspera.assert(!result.nil?, type: Error){'no such config found'}
38
+ Result::SingleObject.new(result)
39
+ end
40
+
41
+ def action_preset_secure(config_name: nil, **)
42
+ cp = presets.config_presets
43
+ preset_names = config_name.nil? ? cp.keys : [config_name]
44
+ secret_keywords = %w[password secret].freeze
45
+ preset_names.each do |preset_name|
46
+ preset = cp[preset_name]
47
+ next unless preset.is_a?(Hash)
48
+ 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
62
+ end
63
+ end
64
+ Result::Status.new('Secrets secured in vault: Make sure to save the vault password securely.')
65
+ end
66
+
67
+ def action_preset_show(name:, **)
68
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
69
+ cp = presets.config_presets
70
+ raise "no such preset: #{name}" unless cp.key?(name)
71
+ Result::SingleObject.new(PresetManager.deep_clone(cp[name]))
72
+ end
73
+
74
+ def action_preset_delete(name:, **)
75
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
76
+ cp = presets.config_presets
77
+ raise "no such preset: #{name}" unless cp.key?(name)
78
+ cp.delete(name)
79
+ Result::Status.new("Deleted: #{name}")
80
+ end
81
+
82
+ def action_preset_get(name:, param_name:, **)
83
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
84
+ cp = presets.config_presets
85
+ raise "no such preset: #{name}" unless cp.key?(name)
86
+ value = cp[name][param_name]
87
+ raise "no such option in preset #{name} : #{param_name}" if value.nil?
88
+ case value
89
+ when Numeric, String then return Result::Text.new(ExtendedValue.instance.evaluate(value.to_s, context: 'preset'))
90
+ end
91
+ Result::SingleObject.new(value)
92
+ end
93
+
94
+ def action_preset_unset(name:, param_name:, **)
95
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
96
+ cp = presets.config_presets
97
+ raise "no such preset: #{name}" unless cp.key?(name)
98
+ cp[name].delete(param_name)
99
+ Result::Status.new("Removed: #{name}: #{param_name}")
100
+ end
101
+
102
+ def action_preset_set(name:, param_name:, param_value:, **)
103
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
104
+ param_name = Parser.option_line_to_name(param_name)
105
+ presets.set_key(name, param_name, param_value)
106
+ Result::Nothing.new
107
+ end
108
+
109
+ def action_preset_initialize(name:, config_value:, **)
110
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
111
+ cp = presets.config_presets
112
+ Log.log.warn{"configuration already exists: #{name}, overwriting"} if cp.key?(name)
113
+ cp[name] = config_value
114
+ Result::Status.new("Modified: #{@option_config_file}")
115
+ end
116
+
117
+ def action_preset_update(name:, **)
118
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
119
+ unprocessed_options = options.unprocessed_options_with_value
120
+ Log.log.debug{"opts=#{unprocessed_options}"}
121
+ cp = presets.config_presets
122
+ cp[name] ||= {}
123
+ cp[name].merge!(unprocessed_options)
124
+ Result::Status.new("Updated: #{name}")
125
+ end
126
+
127
+ def action_preset_ask(name:, option_names:, **)
128
+ name = presets.global_default_preset if name.eql?(GLOBAL_DEFAULT_KEYWORD)
129
+ cp = presets.config_presets
130
+ cp[name] ||= {}
131
+ option_names.each do |option_name|
132
+ option_value = options.get_interactive(option_name, check_option: true)
133
+ cp[name][option_name] = option_value
134
+ end
135
+ Result::Status.new("Updated: #{name}")
136
+ end
137
+ end
138
+ end
139
+ end