aspera-cli 4.27.5 → 4.28.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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +160 -4
- data/CONTRIBUTING.md +4 -2
- data/README.md +1 -1
- data/bin/asession +1 -1
- data/docs/README.md +1086 -331
- data/docs/test-mcp-with-ai.md +5 -3
- data/lib/aspera/agent/direct.rb +15 -4
- data/lib/aspera/agent/factory.rb +2 -1
- data/lib/aspera/agent/node.rb +1 -1
- data/lib/aspera/api/cos_node.rb +1 -1
- data/lib/aspera/api/faspex.rb +1 -1
- data/lib/aspera/api/node.rb +6 -2
- data/lib/aspera/ascp/installation.rb +47 -11
- data/lib/aspera/ascp/management.rb +89 -43
- data/lib/aspera/cli/ascp_actions.rb +5 -19
- data/lib/aspera/cli/bootstrapper.rb +1 -2
- data/lib/aspera/cli/command_registry.rb +43 -3
- data/lib/aspera/cli/command_spec.rb +3 -0
- data/lib/aspera/cli/completion/ascli.bash +39 -5
- data/lib/aspera/cli/completion/ascli.fish +16 -8
- data/lib/aspera/cli/completion/ascli.ps1 +41 -0
- data/lib/aspera/cli/completion/ascli.zsh +20 -4
- data/lib/aspera/cli/completion_actions.rb +360 -0
- data/lib/aspera/cli/error.rb +42 -0
- data/lib/aspera/cli/extended_value.rb +30 -5
- data/lib/aspera/cli/formatter.rb +118 -56
- data/lib/aspera/cli/hints.rb +0 -1
- data/lib/aspera/cli/http.rb +6 -5
- data/lib/aspera/cli/info.rb +10 -4
- data/lib/aspera/cli/mailer.rb +1 -1
- data/lib/aspera/cli/mcp/http_servlet.rb +156 -0
- data/lib/aspera/cli/mcp/secrets.rb +143 -0
- data/lib/aspera/cli/mcp/tool.rb +231 -0
- data/lib/aspera/cli/option_value.rb +9 -1
- data/lib/aspera/cli/options.schema.yaml +390 -39
- data/lib/aspera/cli/parser.rb +14 -5
- data/lib/aspera/cli/plugins/aoc.rb +8 -11
- data/lib/aspera/cli/plugins/base.rb +36 -15
- data/lib/aspera/cli/plugins/config.rb +15 -44
- data/lib/aspera/cli/plugins/console.rb +3 -1
- data/lib/aspera/cli/plugins/factory.rb +1 -1
- data/lib/aspera/cli/plugins/faspex5.rb +9 -19
- data/lib/aspera/cli/plugins/faspio.rb +36 -5
- data/lib/aspera/cli/plugins/httpgw.rb +1 -0
- data/lib/aspera/cli/plugins/mcp.rb +177 -118
- data/lib/aspera/cli/plugins/node.rb +14 -8
- data/lib/aspera/cli/plugins/orchestrator.rb +351 -38
- data/lib/aspera/cli/plugins/server.rb +3 -1
- data/lib/aspera/cli/plugins/shares.rb +205 -35
- data/lib/aspera/cli/preset_actions.rb +15 -7
- data/lib/aspera/cli/preset_manager.rb +24 -2
- data/lib/aspera/cli/prompt.rb +3 -1
- data/lib/aspera/cli/result.rb +17 -4
- data/lib/aspera/cli/runner.rb +133 -43
- data/lib/aspera/cli/security.rb +134 -0
- data/lib/aspera/cli/table_fitter.rb +116 -0
- data/lib/aspera/cli/terminal_formatter.rb +23 -34
- data/lib/aspera/cli/transfer_agent.rb +4 -1
- data/lib/aspera/cli/vault_manager.rb +6 -5
- data/lib/aspera/cli/version.rb +1 -1
- data/lib/aspera/cli/web_secret.html.erb +61 -0
- data/lib/aspera/cli/web_secret.rb +220 -0
- data/lib/aspera/cli/wizard.rb +19 -9
- data/lib/aspera/command_line_builder.rb +26 -0
- data/lib/aspera/data_repository.rb +38 -24
- data/lib/aspera/duration.rb +42 -0
- data/lib/aspera/environment.rb +88 -24
- data/lib/aspera/exec_spec.rb +6 -5
- data/lib/aspera/faspex_gw.rb +98 -32
- data/lib/aspera/faspex_postproc.rb +187 -44
- data/lib/aspera/formatter_interface.rb +0 -1
- data/lib/aspera/id_generator.rb +2 -1
- data/lib/aspera/json_rpc/client.rb +51 -32
- data/lib/aspera/json_rpc/version.rb +1 -0
- data/lib/aspera/keychain/base.rb +5 -0
- data/lib/aspera/keychain/encrypted_hash.rb +6 -2
- data/lib/aspera/keychain/factory.rb +2 -3
- data/lib/aspera/keychain/hashicorp_vault.rb +7 -4
- data/lib/aspera/keychain/macos_security.rb +119 -13
- data/lib/aspera/keychain/one_password_api.rb +8 -3
- data/lib/aspera/keychain/one_password_base.rb +14 -9
- data/lib/aspera/keychain/one_password_cli.rb +4 -2
- data/lib/aspera/log.rb +3 -6
- data/lib/aspera/markdown.rb +152 -115
- data/lib/aspera/mime.rb +6 -0
- data/lib/aspera/nagios.rb +3 -1
- data/lib/aspera/node_emulator.rb +98 -101
- data/lib/aspera/oauth/{base.rb → client/base.rb} +5 -4
- data/lib/aspera/oauth/{boot.rb → client/boot.rb} +3 -2
- data/lib/aspera/oauth/{factory.rb → client/factory.rb} +7 -0
- data/lib/aspera/oauth/{generic.rb → client/generic.rb} +1 -1
- data/lib/aspera/oauth/{json_credentials.rb → client/json_credentials.rb} +1 -1
- data/lib/aspera/oauth/{jwt.rb → client/jwt.rb} +1 -1
- data/lib/aspera/oauth/{url_json.rb → client/url_json.rb} +1 -1
- data/lib/aspera/oauth/{web.rb → client/web.rb} +2 -2
- data/lib/aspera/oauth/server/authorize.html.erb +34 -0
- data/lib/aspera/oauth/server/pkce.rb +426 -0
- data/lib/aspera/orchestrator_workflow_checker.rb +561 -0
- data/lib/aspera/preview/terminal.rb +3 -2
- data/lib/aspera/rainbow.rb +27 -1
- data/lib/aspera/rest/aspera_errors.rb +51 -59
- data/lib/aspera/rest/client.rb +21 -14
- data/lib/aspera/rest/error_analyzer.rb +9 -0
- data/lib/aspera/rest/list.rb +6 -6
- data/lib/aspera/rest/util.rb +7 -1
- data/lib/aspera/rest.rb +0 -1
- data/lib/aspera/schema/IBM Aspera Faspex API-5.0-enhanced.yaml +0 -2
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1-enhanced.yaml +4001 -0
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0-enhanced.yaml +1097 -0
- data/lib/aspera/schema/IBM Aspera on Cloud Automation API-1.0.5-enhanced.yaml +35 -35
- data/lib/aspera/schema/IBM_Aspera_Shares.yaml +819 -7
- data/lib/aspera/schema/documentation.rb +50 -78
- data/lib/aspera/schema/reader.rb +39 -11
- data/lib/aspera/schema/registry.rb +8 -2
- data/lib/aspera/secret_hider.rb +94 -15
- data/lib/aspera/secret_strength.rb +114 -0
- data/lib/aspera/servlet_helpers.rb +111 -0
- data/lib/aspera/sync/args.schema.yaml +3 -1
- data/lib/aspera/sync/conf.schema.yaml +16 -6
- data/lib/aspera/sync/operations.rb +21 -1
- data/lib/aspera/temp_file_manager.rb +79 -20
- data/lib/aspera/transfer/parameters.rb +5 -0
- data/lib/aspera/transfer/spec.rb +52 -12
- data/lib/aspera/transfer/spec.schema.yaml +8 -5
- data/lib/aspera/web_auth/index.html.erb +16 -0
- data/lib/aspera/web_auth/server.rb +18 -10
- data/lib/aspera/web_page.html.erb +262 -0
- data/lib/aspera/web_page.rb +61 -0
- data/lib/aspera/web_server_simple.rb +76 -10
- data.tar.gz.sig +0 -0
- metadata +47 -32
- metadata.gz.sig +0 -0
- data/lib/aspera/cli/mcp_tool.rb +0 -189
- data/lib/aspera/oauth.rb +0 -9
- data/lib/aspera/schema/IBM Aspera Orchestrator API-v1.yaml +0 -1784
- data/lib/aspera/schema/IBM Aspera faspio Gateway API-1.0.0.yaml +0 -249
- data/lib/aspera/web_auth/index.html +0 -147
|
@@ -60,6 +60,15 @@ module Aspera
|
|
|
60
60
|
rescue => e
|
|
61
61
|
e
|
|
62
62
|
end
|
|
63
|
+
# Native JSON health check endpoint (no auth required)
|
|
64
|
+
result[:system] =
|
|
65
|
+
begin
|
|
66
|
+
Rest::Client
|
|
67
|
+
.new(base_url: url)
|
|
68
|
+
.read('health_check')
|
|
69
|
+
rescue => e
|
|
70
|
+
e
|
|
71
|
+
end
|
|
63
72
|
result
|
|
64
73
|
end
|
|
65
74
|
|
|
@@ -79,10 +88,12 @@ module Aspera
|
|
|
79
88
|
# @param app_url [String] Tested URL
|
|
80
89
|
# @return [Hash] :preset_value, :test_args
|
|
81
90
|
def wizard(wizard, app_url)
|
|
91
|
+
username = options.get_option(:username, mandatory: true)
|
|
82
92
|
return {
|
|
93
|
+
preset_ids: [username],
|
|
83
94
|
preset_value: {
|
|
84
95
|
url: app_url,
|
|
85
|
-
username:
|
|
96
|
+
username: username,
|
|
86
97
|
password: options.get_option(:password, mandatory: true)
|
|
87
98
|
},
|
|
88
99
|
test_args: 'files browse /'
|
|
@@ -99,10 +110,14 @@ module Aspera
|
|
|
99
110
|
|
|
100
111
|
# common to users and groups
|
|
101
112
|
USR_GRP_SETTINGS = %i[transfer_settings app_authorizations share_permissions].freeze
|
|
113
|
+
# transfer_settings supports delete (resets to global inheritance); app_authorizations and share_permissions do not
|
|
114
|
+
TRANSFER_SETTINGS_OPS = %i[show modify delete].freeze
|
|
102
115
|
# share_permissions is read-only (Rails only exposes index+show)
|
|
103
116
|
SHARE_PERMISSIONS_OPS = %i[list show].freeze
|
|
104
|
-
# group users: Rails only exposes index+show+update+destroy (no create)
|
|
117
|
+
# group users (all): Rails only exposes index+show+update+destroy (no create)
|
|
105
118
|
GROUP_USERS_OPS = (Operations::ALL - %i[create]).freeze
|
|
119
|
+
# local group users: same operations (no create), via data/local_groups/{id}/local_users
|
|
120
|
+
LOCAL_GROUP_USERS_OPS = GROUP_USERS_OPS
|
|
106
121
|
# Arguments of group users commands, by operation
|
|
107
122
|
GROUP_USERS_ARGS = {
|
|
108
123
|
show: [{name: :user_id, type: :identifier}],
|
|
@@ -117,10 +132,25 @@ module Aspera
|
|
|
117
132
|
command :files, description: 'Browse and transfer files on Shares', aliases: [:repository],
|
|
118
133
|
mount: {plugin: Node, instance: :shares_node_plugin, only: Node::COMMANDS_SHARES}
|
|
119
134
|
command :admin, description: 'Administer Shares', setup: :setup_admin
|
|
135
|
+
command :share, description: 'Shares accessible to current user'
|
|
136
|
+
|
|
137
|
+
commands_under :share do
|
|
138
|
+
command :list, description: 'List shares accessible to the current user',
|
|
139
|
+
action: ->(**) { entity_list(api: basic_auth_api(ADMIN_API_PATH), entity: 'shares', items_key: 'shares') }
|
|
140
|
+
command :event, description: 'Transfer notification events on a share'
|
|
141
|
+
commands_under :event do
|
|
142
|
+
command :create,
|
|
143
|
+
description: 'Notify Shares of a completed transfer (triggers email alerts)',
|
|
144
|
+
arguments: [
|
|
145
|
+
{name: :share_id, type: :identifier, lookup: :lookup_share_id_user},
|
|
146
|
+
{name: :event, type: Hash}
|
|
147
|
+
]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
120
150
|
|
|
121
151
|
commands_under :admin do
|
|
122
152
|
commands_under :node do
|
|
123
|
-
crud_commands entity: 'data/nodes', api: :@api_shares_admin
|
|
153
|
+
crud_commands entity: 'data/nodes', api: :@api_shares_admin, lookup: %w[name]
|
|
124
154
|
end
|
|
125
155
|
commands_under :ldap_config, description: 'LDAP configuration' do
|
|
126
156
|
crud_commands entity: 'data/ldap_configs', api: :@api_shares_admin
|
|
@@ -136,13 +166,30 @@ module Aspera
|
|
|
136
166
|
body_component: Schema::Registry::SHARES,
|
|
137
167
|
operations: %i[show create modify]
|
|
138
168
|
end
|
|
169
|
+
commands_under :directory do
|
|
170
|
+
command :list, description: 'List configured authentication directories',
|
|
171
|
+
action: ->(**) { entity_list(api: @api_shares_admin, entity: 'directories') }
|
|
172
|
+
command :show,
|
|
173
|
+
description: 'Get details of an authentication directory',
|
|
174
|
+
arguments: [{name: :directory_id, type: :identifier}]
|
|
175
|
+
command :users,
|
|
176
|
+
description: 'Search users in a directory',
|
|
177
|
+
arguments: [{name: :directory_id, type: :identifier}]
|
|
178
|
+
command :groups,
|
|
179
|
+
description: 'Search groups in a directory',
|
|
180
|
+
arguments: [{name: :directory_id, type: :identifier}]
|
|
181
|
+
end
|
|
182
|
+
command :authenticate,
|
|
183
|
+
description: 'Validate credentials and optionally check a permission',
|
|
184
|
+
arguments: [{name: :credentials, type: Hash}]
|
|
139
185
|
# user / group: sub-commands per location, generated for both entity types
|
|
140
186
|
%i[user group].each do |entity_type|
|
|
141
187
|
commands_under entity_type do
|
|
142
|
-
command :all,
|
|
143
|
-
command :local,
|
|
144
|
-
command :ldap,
|
|
145
|
-
command :saml,
|
|
188
|
+
command :all, description: "#{entity_type.capitalize}s from all sources"
|
|
189
|
+
command :local, description: "Local #{entity_type}s"
|
|
190
|
+
command :ldap, description: "LDAP #{entity_type}s"
|
|
191
|
+
command :saml, description: "SAML #{entity_type}s"
|
|
192
|
+
command :search, description: "Search #{entity_type}s across all directories"
|
|
146
193
|
|
|
147
194
|
user_fields = %w[id user_id username first_name last_name email] if entity_type.eql?(:user)
|
|
148
195
|
# all: list/show/delete only (no create, no modify — users/groups/:all has no update route)
|
|
@@ -154,23 +201,34 @@ module Aspera
|
|
|
154
201
|
operations: Operations::ALL - %i[create modify],
|
|
155
202
|
lookup: lookup_method_all,
|
|
156
203
|
display_fields: user_fields && (user_fields + %w[directory_user])
|
|
204
|
+
# share_permissions are read-only: tell where they are granted
|
|
205
|
+
grant_hint = "read-only, grant with: admin share #{entity_type}_permissions"
|
|
157
206
|
USR_GRP_SETTINGS.each do |setting|
|
|
158
|
-
#
|
|
159
|
-
setting_ops =
|
|
207
|
+
# choose the operations set for this setting
|
|
208
|
+
setting_ops =
|
|
209
|
+
case setting
|
|
210
|
+
when :share_permissions then SHARE_PERMISSIONS_OPS
|
|
211
|
+
when :transfer_settings then TRANSFER_SETTINGS_OPS
|
|
212
|
+
else %i[show modify]
|
|
213
|
+
end
|
|
160
214
|
command setting,
|
|
161
|
-
description: "Manage #{entity_noun(setting, singular: false)} for a #{entity_type}",
|
|
215
|
+
description: setting.eql?(:share_permissions) ? "Permissions of a #{entity_type} on shares (#{grant_hint})" : "Manage #{entity_noun(setting, singular: false)} for a #{entity_type}",
|
|
162
216
|
arguments: [{name: :"#{entity_type}_id", type: :identifier, lookup: lookup_method_all}]
|
|
163
217
|
commands_under setting do
|
|
164
218
|
setting_ops.each do |op|
|
|
165
|
-
# share_permissions > show needs a
|
|
219
|
+
# share_permissions > show needs a share identifier (list does not), modify needs data
|
|
166
220
|
op_args =
|
|
167
221
|
case op
|
|
168
|
-
when :show
|
|
222
|
+
when :show then setting.eql?(:share_permissions) ? [{name: :share_id, type: :identifier, lookup: :lookup_share_id}] : nil
|
|
169
223
|
when :modify then [{name: setting, type: Hash}]
|
|
170
224
|
end
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
225
|
+
op_description =
|
|
226
|
+
case [setting, op]
|
|
227
|
+
when %i[share_permissions list] then "List permissions of a #{entity_type} on shares (#{grant_hint})"
|
|
228
|
+
when %i[share_permissions show] then "Show permission of a #{entity_type} on a share (#{grant_hint})"
|
|
229
|
+
else "#{op.capitalize} #{entity_noun(setting, singular: false)} for a #{entity_type}"
|
|
230
|
+
end
|
|
231
|
+
command op, description: op_description, arguments: op_args
|
|
174
232
|
end
|
|
175
233
|
end
|
|
176
234
|
end
|
|
@@ -197,11 +255,16 @@ module Aspera
|
|
|
197
255
|
body_component: Schema::Registry::SHARES
|
|
198
256
|
if entity_type.eql?(:group)
|
|
199
257
|
command :users,
|
|
200
|
-
description: 'Manage users of a group',
|
|
258
|
+
description: 'Manage users of a local group',
|
|
201
259
|
arguments: [{name: :group_id, type: :identifier, lookup: :lookup_shares_group_local_id}]
|
|
202
260
|
commands_under :users do
|
|
203
|
-
|
|
204
|
-
|
|
261
|
+
LOCAL_GROUP_USERS_OPS.each do |op|
|
|
262
|
+
local_op_args =
|
|
263
|
+
case op
|
|
264
|
+
when :show, :delete then [{name: :user_id, type: :identifier}]
|
|
265
|
+
when :modify then [{name: :user_id, type: :identifier}, {name: :user, type: Hash}]
|
|
266
|
+
end
|
|
267
|
+
command op, description: "#{op.capitalize} users of a local group", arguments: local_op_args
|
|
205
268
|
end
|
|
206
269
|
end
|
|
207
270
|
end
|
|
@@ -218,6 +281,24 @@ module Aspera
|
|
|
218
281
|
command :import, description: "Import a SAML #{entity_type}",
|
|
219
282
|
arguments: [{name: entity_type, type: Hash, bulk: true}]
|
|
220
283
|
end
|
|
284
|
+
|
|
285
|
+
# search: list + show by URN + (groups only) members
|
|
286
|
+
# The cross-directory search endpoints return a collection object {"resource_type":"collection","members":[...]}
|
|
287
|
+
# entity_plural captured at class-load time in the closure
|
|
288
|
+
entity_plural_search = entity_type.eql?(:user) ? 'users' : 'groups'
|
|
289
|
+
commands_under :search do
|
|
290
|
+
command :list,
|
|
291
|
+
description: "Search #{entity_type}s across all directories (use --query={q: '...'} to filter)",
|
|
292
|
+
action: ->(**) { entity_list(api: @api_shares_admin, entity: entity_plural_search, items_key: 'members') }
|
|
293
|
+
command :show,
|
|
294
|
+
description: "Get directory #{entity_type} details by URN",
|
|
295
|
+
arguments: [{name: :"#{entity_type}_urn", type: :identifier}]
|
|
296
|
+
if entity_type.eql?(:group)
|
|
297
|
+
command :members,
|
|
298
|
+
description: 'Search members in a directory group',
|
|
299
|
+
arguments: [{name: :group_urn, type: :identifier}]
|
|
300
|
+
end
|
|
301
|
+
end
|
|
221
302
|
end
|
|
222
303
|
end
|
|
223
304
|
end
|
|
@@ -233,7 +314,7 @@ module Aspera
|
|
|
233
314
|
lookup_method = :"lookup_shares_#{entity_type}_#{location}_id"
|
|
234
315
|
define_method(lookup_method) do |field, value, **|
|
|
235
316
|
path = admin_entity_path(entity_type, location)
|
|
236
|
-
Rest::List.lookup_entity_generic(
|
|
317
|
+
Rest::List.lookup_entity_generic(field: field, value: value, entity: entity_type) { @api_shares_admin.read(path) }['id']
|
|
237
318
|
end
|
|
238
319
|
private lookup_method
|
|
239
320
|
end
|
|
@@ -259,10 +340,12 @@ module Aspera
|
|
|
259
340
|
{user_permissions: :user, group_permissions: :group}.each do |perm_type, entity_type|
|
|
260
341
|
commands_under [:admin, :share, perm_type] do
|
|
261
342
|
crud_commands entity: :"share_#{perm_type}",
|
|
262
|
-
api:
|
|
263
|
-
name:
|
|
264
|
-
id_name:
|
|
265
|
-
lookup:
|
|
343
|
+
api: :@api_shares_admin,
|
|
344
|
+
name: entity_noun(perm_type),
|
|
345
|
+
id_name: :"#{entity_type}_id",
|
|
346
|
+
lookup: :"lookup_shares_#{entity_type}_all_id",
|
|
347
|
+
body_component: Schema::Registry::SHARES,
|
|
348
|
+
schema_entity: "data/shares/{share_id}/#{perm_type}"
|
|
266
349
|
end
|
|
267
350
|
end
|
|
268
351
|
|
|
@@ -297,7 +380,14 @@ module Aspera
|
|
|
297
380
|
|
|
298
381
|
# Lookup a share id by field/value using the admin API.
|
|
299
382
|
def lookup_share_id(field, value, **)
|
|
300
|
-
Rest::List.lookup_entity_generic(
|
|
383
|
+
Rest::List.lookup_entity_generic(field: field, value: value, entity: 'share') { @api_shares_admin.read('data/shares') }['id']
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Lookup a share id by field/value using the user-level API (for share event create).
|
|
387
|
+
def lookup_share_id_user(field, value, **)
|
|
388
|
+
data = basic_auth_api(ADMIN_API_PATH).read('shares')
|
|
389
|
+
data = data['shares'] if data.is_a?(Hash)
|
|
390
|
+
Rest::List.lookup_entity_generic(field: field, value: value, entity: 'share') { data }['id']
|
|
301
391
|
end
|
|
302
392
|
|
|
303
393
|
# --- health ---
|
|
@@ -317,6 +407,25 @@ module Aspera
|
|
|
317
407
|
else
|
|
318
408
|
nagios.add_critical('API', health[:api].to_s)
|
|
319
409
|
end
|
|
410
|
+
if health[:system].is_a?(Hash)
|
|
411
|
+
sys = health[:system]
|
|
412
|
+
# one status item for the license (standard or entitlement)
|
|
413
|
+
if sys['valid_license'] || sys['entitled']
|
|
414
|
+
nagios.add_ok('license', sys['entitled'] ? 'entitled' : 'valid')
|
|
415
|
+
else
|
|
416
|
+
nagios.add_critical('license', 'invalid')
|
|
417
|
+
end
|
|
418
|
+
# one status item per configured node
|
|
419
|
+
(sys['nodes'] || []).each do |node|
|
|
420
|
+
if node['status'].to_s.casecmp('active').zero?
|
|
421
|
+
nagios.add_ok("node #{node['id']}", node['status'])
|
|
422
|
+
else
|
|
423
|
+
nagios.add_critical("node #{node['id']}", node['status'])
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
else
|
|
427
|
+
nagios.add_critical('system', health[:system].to_s)
|
|
428
|
+
end
|
|
320
429
|
Result::ObjectList.new(nagios.status_list)
|
|
321
430
|
end
|
|
322
431
|
|
|
@@ -336,10 +445,10 @@ module Aspera
|
|
|
336
445
|
# @param setting [Symbol] one of USR_GRP_SETTINGS
|
|
337
446
|
# @param op [Symbol] CRUD operation
|
|
338
447
|
# entity_id: resolved via arguments:(:identifier) on the setting node
|
|
339
|
-
#
|
|
340
|
-
def admin_entity_setting(entity_type, location, setting, op, entity_id:,
|
|
448
|
+
# share_id: (share_permissions show) and data: (modify) resolved via arguments: on the leaf
|
|
449
|
+
def admin_entity_setting(entity_type, location, setting, op, entity_id:, share_id: nil, data: nil)
|
|
341
450
|
entity_path = "#{admin_entity_path(entity_type, location)}/#{entity_id}/#{setting}"
|
|
342
|
-
send(:"entity_#{op}", api: @api_shares_admin, entity: entity_path, id:
|
|
451
|
+
send(:"entity_#{op}", api: @api_shares_admin, entity: entity_path, id: share_id, is_singleton: !setting.eql?(:share_permissions), data: data)
|
|
343
452
|
end
|
|
344
453
|
|
|
345
454
|
# Shared handler for :users (group only)
|
|
@@ -353,25 +462,45 @@ module Aspera
|
|
|
353
462
|
|
|
354
463
|
# Generate action_admin_<user|group>_<location>_<verb> for all combinations
|
|
355
464
|
%i[user group].each do |entity_type|
|
|
356
|
-
# all: list/show/delete only
|
|
465
|
+
# all: list/show/delete only — nested settings (transfer_settings, app_authorizations, share_permissions) only under :all
|
|
357
466
|
# local: full CRUD only — no nested settings under local_users/local_groups
|
|
358
467
|
ENTITY_LOCATIONS.each do |location|
|
|
359
468
|
unless location.eql?(:local)
|
|
360
469
|
USR_GRP_SETTINGS.each do |setting|
|
|
361
|
-
#
|
|
362
|
-
setting_ops =
|
|
470
|
+
# choose the operations set for this setting
|
|
471
|
+
setting_ops =
|
|
472
|
+
case setting
|
|
473
|
+
when :share_permissions then SHARE_PERMISSIONS_OPS
|
|
474
|
+
when :transfer_settings then TRANSFER_SETTINGS_OPS
|
|
475
|
+
else %i[show modify]
|
|
476
|
+
end
|
|
363
477
|
setting_ops.each do |op|
|
|
364
478
|
define_action_method([:admin, entity_type, location, setting, op]) do |**kwargs|
|
|
365
|
-
admin_entity_setting(entity_type, location, setting, op, entity_id: kwargs[:"#{entity_type}_id"],
|
|
479
|
+
admin_entity_setting(entity_type, location, setting, op, entity_id: kwargs[:"#{entity_type}_id"], share_id: kwargs[:share_id], data: kwargs[setting])
|
|
366
480
|
end
|
|
367
481
|
end
|
|
368
482
|
end
|
|
369
483
|
end
|
|
370
484
|
next unless entity_type.eql?(:group)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
485
|
+
if location.eql?(:all)
|
|
486
|
+
# all group users: no create route (Rails only exposes index+show+update+destroy)
|
|
487
|
+
GROUP_USERS_OPS.each do |op|
|
|
488
|
+
define_action_method([:admin, entity_type, :all, :users, op]) do |group_id:, user_id: nil, user: nil, **|
|
|
489
|
+
admin_entity_users(entity_type, :all, op, group_id: group_id, user_id: user_id, data: user)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
else
|
|
493
|
+
# local group local_users: list/show/modify/delete (via data/local_groups/{id}/local_users)
|
|
494
|
+
LOCAL_GROUP_USERS_OPS.each do |op|
|
|
495
|
+
define_action_method([:admin, entity_type, :local, :users, op]) do |group_id:, user_id: nil, user: nil, **|
|
|
496
|
+
send(
|
|
497
|
+
:"entity_#{op}",
|
|
498
|
+
api: @api_shares_admin,
|
|
499
|
+
entity: "data/local_groups/#{group_id}/local_users",
|
|
500
|
+
id: user_id,
|
|
501
|
+
data: user
|
|
502
|
+
)
|
|
503
|
+
end
|
|
375
504
|
end
|
|
376
505
|
end
|
|
377
506
|
end
|
|
@@ -397,6 +526,47 @@ module Aspera
|
|
|
397
526
|
@api_shares_admin.create("#{path}/import", entity_parameters)
|
|
398
527
|
end
|
|
399
528
|
end
|
|
529
|
+
|
|
530
|
+
# search: list + show + (groups) members — cross-directory endpoints under /api/v1/users|groups
|
|
531
|
+
# URNs contain ':' and '=' and must be percent-encoded in the URL path segment.
|
|
532
|
+
entity_plural = entity_type.eql?(:user) ? 'users' : 'groups'
|
|
533
|
+
define_action_method(%i[admin].push(entity_type, :search, :show)) do |**kwargs|
|
|
534
|
+
urn = URI.encode_www_form_component(kwargs[:"#{entity_type}_urn"])
|
|
535
|
+
Result::SingleObject.new(@api_shares_admin.read("#{entity_plural}/#{urn}"))
|
|
536
|
+
end
|
|
537
|
+
next unless entity_type.eql?(:group)
|
|
538
|
+
define_action_method(%i[admin group search members]) do |group_urn:, **|
|
|
539
|
+
encoded = URI.encode_www_form_component(group_urn)
|
|
540
|
+
entity_list(api: @api_shares_admin, entity: "groups/#{encoded}/members", items_key: 'members')
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# admin > directory > show / users / groups
|
|
545
|
+
# directory_id is a URN — must be percent-encoded in the URL path segment.
|
|
546
|
+
define_action_method(%i[admin directory show]) do |directory_id:, **|
|
|
547
|
+
enc = URI.encode_www_form_component(directory_id)
|
|
548
|
+
Result::SingleObject.new(@api_shares_admin.read("directories/#{enc}"))
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
define_action_method(%i[admin directory users]) do |directory_id:, **|
|
|
552
|
+
enc = URI.encode_www_form_component(directory_id)
|
|
553
|
+
entity_list(api: @api_shares_admin, entity: "directories/#{enc}/users", items_key: 'members')
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
define_action_method(%i[admin directory groups]) do |directory_id:, **|
|
|
557
|
+
enc = URI.encode_www_form_component(directory_id)
|
|
558
|
+
entity_list(api: @api_shares_admin, entity: "directories/#{enc}/groups", items_key: 'members')
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# admin > authenticate
|
|
562
|
+
define_action_method(%i[admin authenticate]) do |credentials:, **|
|
|
563
|
+
Result::SingleObject.new(@api_shares_admin.create('authenticate', credentials))
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# share > event > create
|
|
567
|
+
define_action_method(%i[share event create]) do |share_id:, event:, **|
|
|
568
|
+
basic_auth_api(ADMIN_API_PATH).create("shares/#{share_id}/events", event)
|
|
569
|
+
Result::Status.new('event created')
|
|
400
570
|
end
|
|
401
571
|
end
|
|
402
572
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'aspera/assert'
|
|
4
|
+
require 'aspera/secret_hider'
|
|
5
|
+
require 'aspera/cli/security'
|
|
4
6
|
require 'aspera/cli/preset_manager'
|
|
5
7
|
|
|
6
8
|
module Aspera
|
|
@@ -13,10 +15,8 @@ module Aspera
|
|
|
13
15
|
CONF_OVERVIEW_KEYS = %w[preset parameter value].freeze
|
|
14
16
|
|
|
15
17
|
def action_preset_overview(**)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
data = PresetManager.deep_clone(cp)
|
|
19
|
-
formatter.hide_secrets(data)
|
|
18
|
+
# Secrets hidden by key name here: the key becomes the value of column `parameter`
|
|
19
|
+
data = formatter.hide_secrets(presets.config_presets)
|
|
20
20
|
result = []
|
|
21
21
|
data.each do |config, preset|
|
|
22
22
|
preset.each do |parameter, value|
|
|
@@ -83,7 +83,7 @@ module Aspera
|
|
|
83
83
|
incr += 1
|
|
84
84
|
end
|
|
85
85
|
Log.log.info { "Securing #{preset_name}.#{option_name} -> vault label: #{vault_label}" }
|
|
86
|
-
vault.set({label: vault_label, password: value})
|
|
86
|
+
vault.set({label: vault_label, password: value}) # HashiCorpIgnore
|
|
87
87
|
preset[option_name] = "@vault:#{vault_label}.password"
|
|
88
88
|
end
|
|
89
89
|
|
|
@@ -93,7 +93,7 @@ module Aspera
|
|
|
93
93
|
name = preset_name(name)
|
|
94
94
|
cp = presets.config_presets
|
|
95
95
|
raise Cli::BadArgument, "no such preset: #{name}" unless cp.key?(name)
|
|
96
|
-
Result::SingleObject.new(
|
|
96
|
+
Result::SingleObject.new(cp[name])
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
def action_preset_delete(name:, **)
|
|
@@ -117,7 +117,11 @@ module Aspera
|
|
|
117
117
|
value = cp[name][param_name]
|
|
118
118
|
raise Cli::BadArgument, "no such option in preset #{name}: #{param_name}" if value.nil?
|
|
119
119
|
case value
|
|
120
|
-
when Numeric, String
|
|
120
|
+
when Numeric, String
|
|
121
|
+
value = ExtendedValue.instance.evaluate(value.to_s, context: 'preset')
|
|
122
|
+
# A secret has no key name in a text result: declared, so that it is hidden
|
|
123
|
+
SecretHider.instance.add_secret_values(value) if SecretHider.instance.secret?(param_name, value)
|
|
124
|
+
return Result::Text.new(value)
|
|
121
125
|
end
|
|
122
126
|
Result::SingleObject.new(value)
|
|
123
127
|
end
|
|
@@ -132,6 +136,10 @@ module Aspera
|
|
|
132
136
|
|
|
133
137
|
def action_preset_set(name:, param_name:, param_value:, **)
|
|
134
138
|
name = preset_name(name, create: true)
|
|
139
|
+
# Key names are not secrets: converted, or split with dot-notation, a secret would not be hidden in results
|
|
140
|
+
Security.instance.check_secret_processing('preset parameter name', hint: 'give a name that is not a secret') do
|
|
141
|
+
SecretHider.instance.contains_secret?(param_name)
|
|
142
|
+
end
|
|
135
143
|
param_name = Parser.option_line_to_name(param_name)
|
|
136
144
|
presets.set_key(name, param_name, Parser.smart_convert(param_value))
|
|
137
145
|
secure_preset_option(presets.config_presets[name], name, param_name)
|
|
@@ -6,6 +6,7 @@ require 'aspera/cli/version'
|
|
|
6
6
|
require 'aspera/cli/info'
|
|
7
7
|
require 'aspera/log'
|
|
8
8
|
require 'aspera/assert'
|
|
9
|
+
require 'aspera/secret_hider'
|
|
9
10
|
require 'aspera/yaml'
|
|
10
11
|
require 'digest'
|
|
11
12
|
require 'yaml'
|
|
@@ -66,7 +67,8 @@ module Aspera
|
|
|
66
67
|
if File.exist?(@config_file)
|
|
67
68
|
Log.log.debug { "loading #{@config_file}" }
|
|
68
69
|
begin
|
|
69
|
-
|
|
70
|
+
# Before checksum: the conversion alone does not save the file
|
|
71
|
+
@config_presets = self.class.stringify_secrets(nil, Yaml.safe_load(File.read(@config_file)))
|
|
70
72
|
rescue Psych::SyntaxError
|
|
71
73
|
Log.log.error { "YAML error in config file: #{@config_file}" }
|
|
72
74
|
raise
|
|
@@ -91,6 +93,8 @@ module Aspera
|
|
|
91
93
|
end
|
|
92
94
|
raise Cli::Error, e.to_s
|
|
93
95
|
end
|
|
96
|
+
# Hidden wherever they appear (logs, results), also when copied under another key name
|
|
97
|
+
SecretHider.instance.add_secrets_in(@config_presets)
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
# Save to disk only if content changed since last load/save.
|
|
@@ -152,7 +156,10 @@ module Aspera
|
|
|
152
156
|
end
|
|
153
157
|
current = self.class.deep_clone(current) unless current.is_a?(String)
|
|
154
158
|
current.delete_if { |k, _| k.to_s.start_with?(Key::META_PREFIX) } if current.is_a?(Hash)
|
|
155
|
-
ExtendedValue.instance.evaluate(current, context: 'preset')
|
|
159
|
+
current = ExtendedValue.instance.evaluate(current, context: 'preset')
|
|
160
|
+
# A secret read alone has no key name: declared, so that it is hidden wherever it appears
|
|
161
|
+
SecretHider.instance.add_secret_values(current) if SecretHider.instance.secret?(names.last, current)
|
|
162
|
+
current
|
|
156
163
|
end
|
|
157
164
|
|
|
158
165
|
# Set a single key in a preset hash (creates the preset if absent).
|
|
@@ -161,6 +168,7 @@ module Aspera
|
|
|
161
168
|
def set_key(preset, param_name, param_value)
|
|
162
169
|
Aspera.assert_type(param_name, String, Symbol) { 'parameter' }
|
|
163
170
|
param_name = param_name.to_s
|
|
171
|
+
param_value = self.class.stringify_secrets(param_name.split(DotContainer::SEPARATOR).last, param_value)
|
|
164
172
|
selected = @config_presets[preset]
|
|
165
173
|
if selected.nil?
|
|
166
174
|
Log.log.debug { "Unknown preset name: #{preset}, initializing" }
|
|
@@ -276,6 +284,20 @@ module Aspera
|
|
|
276
284
|
end
|
|
277
285
|
dst
|
|
278
286
|
end
|
|
287
|
+
|
|
288
|
+
# A secret is a String: a number under a secret key name (e.g. password `12345678`, unquoted in YAML or set on command line)
|
|
289
|
+
# is converted, else it is not identified as a secret, and is shown in results
|
|
290
|
+
# @param key [String, nil] Key name of value
|
|
291
|
+
# @param value [Object] Value: containers are modified in place
|
|
292
|
+
# @return [Object] Value, or its String if it is a secret
|
|
293
|
+
def stringify_secrets(key, value)
|
|
294
|
+
case value
|
|
295
|
+
when Hash then value.each { |k, v| value[k] = stringify_secrets(k, v) }
|
|
296
|
+
when Array then value.map! { |v| stringify_secrets(nil, v) }
|
|
297
|
+
when Numeric then value = value.to_s if SecretHider.instance.secret?(key, value.to_s)
|
|
298
|
+
end
|
|
299
|
+
value
|
|
300
|
+
end
|
|
279
301
|
end
|
|
280
302
|
|
|
281
303
|
private
|
data/lib/aspera/cli/prompt.rb
CHANGED
|
@@ -7,13 +7,15 @@ module Aspera
|
|
|
7
7
|
module Cli
|
|
8
8
|
# Console input
|
|
9
9
|
module Prompt
|
|
10
|
+
extend self
|
|
11
|
+
|
|
10
12
|
# Prompt user for console input
|
|
11
13
|
# The prompt is displayed on `stderr`, like for sensitive input, so that `stdout` contains only results.
|
|
12
14
|
# @param prompt [String] prompt string to display
|
|
13
15
|
# @param sensitive [Boolean] whether to hide typed input
|
|
14
16
|
# @return [String] user input stripped of trailing newline
|
|
15
17
|
def prompt_user_input(prompt, sensitive: false)
|
|
16
|
-
return $stdin.getpass("#{prompt}> ") if sensitive
|
|
18
|
+
return $stdin.getpass("#{prompt}> ") if sensitive && $stdin.tty?
|
|
17
19
|
$stderr.print("#{prompt}> ")
|
|
18
20
|
line = $stdin.gets
|
|
19
21
|
Aspera.assert_type(line, String) { 'Unexpected end of standard input' }
|
data/lib/aspera/cli/result.rb
CHANGED
|
@@ -29,6 +29,12 @@ module Aspera
|
|
|
29
29
|
@fields = fields
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
# @param data [Object] New data
|
|
33
|
+
# @return [Result] Copy of this result, with other data (e.g. with secrets hidden)
|
|
34
|
+
def with_data(data)
|
|
35
|
+
dup.tap { |r| r.data = data }
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
# Format this result using the provided formatter
|
|
33
39
|
# This method implements the Visitor pattern, allowing each Result subclass
|
|
34
40
|
# to define how it should be formatted without the Formatter needing to know
|
|
@@ -81,6 +87,13 @@ module Aspera
|
|
|
81
87
|
|
|
82
88
|
# Formats rendering the data structure itself
|
|
83
89
|
STRUCTURED_FORMATS = %i[ruby json jsonpp yaml].freeze
|
|
90
|
+
# Types of a single value result (Text), or of items of a value list
|
|
91
|
+
SCALAR_TYPES = [String, Integer, Float, Symbol, TrueClass, FalseClass].freeze
|
|
92
|
+
|
|
93
|
+
protected
|
|
94
|
+
|
|
95
|
+
# Used by `with_data`
|
|
96
|
+
attr_writer :data
|
|
84
97
|
|
|
85
98
|
private
|
|
86
99
|
|
|
@@ -174,8 +187,9 @@ module Aspera
|
|
|
174
187
|
|
|
175
188
|
# Text result
|
|
176
189
|
class Text < Result
|
|
190
|
+
# @param data [String, Numeric, Symbol, Boolean] The value, hidden if it is a secret declared to `SecretHider`
|
|
177
191
|
def initialize(data)
|
|
178
|
-
Aspera.assert_type(data,
|
|
192
|
+
Aspera.assert_type(data, *SCALAR_TYPES, type: ArgumentError) { 'text result data' }
|
|
179
193
|
super(data: data)
|
|
180
194
|
end
|
|
181
195
|
|
|
@@ -351,12 +365,11 @@ module Aspera
|
|
|
351
365
|
all_types = data.map(&:class).uniq
|
|
352
366
|
return ObjectList.new(data) if all_types.eql?([Hash])
|
|
353
367
|
|
|
354
|
-
|
|
355
|
-
unsupported_types = all_types - scalar_types
|
|
368
|
+
unsupported_types = all_types - SCALAR_TYPES
|
|
356
369
|
return ValueList.new(data, name: 'list') if unsupported_types.empty?
|
|
357
370
|
|
|
358
371
|
Aspera.error_unexpected_value(unsupported_types) { 'list item types' }
|
|
359
|
-
when
|
|
372
|
+
when *SCALAR_TYPES
|
|
360
373
|
Text.new(data)
|
|
361
374
|
else
|
|
362
375
|
Aspera.error_unexpected_value(data.class.name) { 'result type' }
|