btps_client 1.0.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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +444 -0
  3. data/lib/btps_client/api_operations/actionable.rb +22 -0
  4. data/lib/btps_client/api_operations/creatable.rb +16 -0
  5. data/lib/btps_client/api_operations/deletable.rb +15 -0
  6. data/lib/btps_client/api_operations/listable.rb +16 -0
  7. data/lib/btps_client/api_operations/movable.rb +17 -0
  8. data/lib/btps_client/api_operations/nested_listable.rb +23 -0
  9. data/lib/btps_client/api_operations/retrievable.rb +15 -0
  10. data/lib/btps_client/api_operations/scalar_creatable.rb +31 -0
  11. data/lib/btps_client/api_operations/searchable.rb +16 -0
  12. data/lib/btps_client/api_operations/updatable.rb +16 -0
  13. data/lib/btps_client/api_operations/uploadable.rb +29 -0
  14. data/lib/btps_client/api_requestor.rb +143 -0
  15. data/lib/btps_client/api_resource.rb +45 -0
  16. data/lib/btps_client/btps_object.rb +58 -0
  17. data/lib/btps_client/client.rb +37 -0
  18. data/lib/btps_client/configuration.rb +71 -0
  19. data/lib/btps_client/errors.rb +39 -0
  20. data/lib/btps_client/resources/credential.rb +98 -0
  21. data/lib/btps_client/resources/folder.rb +36 -0
  22. data/lib/btps_client/resources/managed_account.rb +97 -0
  23. data/lib/btps_client/resources/request.rb +107 -0
  24. data/lib/btps_client/resources/safe.rb +77 -0
  25. data/lib/btps_client/resources/secret.rb +179 -0
  26. data/lib/btps_client/schema_builder.rb +50 -0
  27. data/lib/btps_client/services/auth_service.rb +57 -0
  28. data/lib/btps_client/services/credentials_service.rb +36 -0
  29. data/lib/btps_client/services/folders_service.rb +37 -0
  30. data/lib/btps_client/services/managed_accounts_service.rb +48 -0
  31. data/lib/btps_client/services/requests_service.rb +44 -0
  32. data/lib/btps_client/services/safes_service.rb +48 -0
  33. data/lib/btps_client/services/secrets_safe_service.rb +25 -0
  34. data/lib/btps_client/services/secrets_service.rb +96 -0
  35. data/lib/btps_client/validator.rb +129 -0
  36. data/lib/btps_client/version.rb +5 -0
  37. data/lib/btps_client.rb +64 -0
  38. metadata +183 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2c928eefcf3f75bb95eb5501a1c8915da41edc9fe5849575f2da6b166125c4e1
4
+ data.tar.gz: 7cfc23cbc1f50fd49c40db1bec8823c736af4c442f4b611dc1a92d4394671a27
5
+ SHA512:
6
+ metadata.gz: bf9c40e02d072459c7e45c709bef4dd0ca654fc84ceba272b753cb8c112ea38b674b0c934c7a374f80a741220f5fdec5013015e143b98e0c5ffbbc443a26ce93
7
+ data.tar.gz: f7a2cc18bef4662f46684e579fa135d2f782613797087e6968eb7b98d36e7ba64341263d45cccbb355ee53b238f1914f0f6aa5f73e0998e278f6b0f398e42d6b
data/README.md ADDED
@@ -0,0 +1,444 @@
1
+ # btps_client — BeyondTrust Password Safe Ruby SDK
2
+
3
+ Ruby SDK for the [BeyondTrust Password Safe REST API v3](https://www.beyondtrust.com).
4
+ Covers **Auth**, **Secrets Safe Folders**, **Safes**, and **Secrets** (credential, text, and file types).
5
+
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.7-red)](https://www.ruby-lang.org)
7
+
8
+ ---
9
+
10
+ ## Installation
11
+
12
+ Add to your `Gemfile`:
13
+
14
+ ```ruby
15
+ gem 'btps_client'
16
+ ```
17
+
18
+ Or install directly:
19
+
20
+ ```sh
21
+ gem install btps_client
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Quick start
27
+
28
+ ```ruby
29
+ require 'btps_client'
30
+
31
+ # 1. Configure once (usually in an initializer)
32
+ BtpsClient.configure do |c|
33
+ c.host = 'vault.example.com'
34
+ c.base_path = 'BeyondTrust/api/public/v3'
35
+ c.client_id = ENV['BTPS_CLIENT_ID']
36
+ c.client_secret = ENV['BTPS_CLIENT_SECRET']
37
+ c.verify_ssl = true
38
+ end
39
+
40
+ # 2. Acquire an OAuth2 access token (client credentials grant)
41
+ client = BtpsClient::Client.new
42
+ token_response = client.auth.acquire_token
43
+ access_token = token_response.access_token # String
44
+
45
+ # 3. Sign in to open a Password Safe session
46
+ authed_client = BtpsClient::Client.new(access_token: access_token)
47
+ authed_client.auth.sign_in
48
+
49
+ # 4. Use the authenticated client for all API calls
50
+ ss = authed_client.secrets_safe
51
+
52
+ # List folders
53
+ folders = ss.folders.list
54
+ folders.each { |f| puts f.name }
55
+
56
+ # Create a credential secret
57
+ secret = ss.secrets.create_in_folder(folder_id,
58
+ Title: 'My App DB',
59
+ Username: 'dbadmin',
60
+ Password: 's3cr3t'
61
+ )
62
+ puts secret.id
63
+ ```
64
+
65
+ ---
66
+
67
+ ## Configuration
68
+
69
+ All options can be set globally via `BtpsClient.configure` or per-client via `Client.new(...)`.
70
+ Credentials fall back to environment variables when not supplied in code.
71
+
72
+ | Option | Default | Env var | Description |
73
+ |-------------------|---------------------|-----------------------|--------------------------------------|
74
+ | `host` | `nil` *(required)* | — | Hostname of the Password Safe server |
75
+ | `base_path` | `api/public/v3` | — | API path prefix |
76
+ | `scheme` | `https` | — | `https` or `http` |
77
+ | `port` | `nil` | — | Override port (omit for default) |
78
+ | `access_token` | `nil` | `BTPS_ACCESS_TOKEN` | Bearer token (post sign-in) |
79
+ | `client_id` | `nil` | `BTPS_CLIENT_ID` | OAuth client ID |
80
+ | `client_secret` | `nil` | `BTPS_CLIENT_SECRET` | OAuth client secret |
81
+ | `verify_ssl` | `true` | — | Set `false` only in dev/test |
82
+ | `timeout` | `30` | — | Read timeout in seconds |
83
+ | `connect_timeout` | `10` | — | Connect timeout in seconds |
84
+ | `max_retries` | `3` | — | Retry attempts on 429 / 5xx |
85
+ | `logger` | `nil` | — | Any object responding to `debug/warn/error` |
86
+
87
+ ```ruby
88
+ # Per-client override (does not affect the global config)
89
+ client = BtpsClient::Client.new(
90
+ host: 'vault.example.com',
91
+ access_token: 'eyJ...',
92
+ logger: Logger.new($stdout)
93
+ )
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Authentication
99
+
100
+ BeyondTrust Password Safe uses a two-step auth flow:
101
+
102
+ 1. **`acquire_token`** — OAuth2 client credentials grant → returns a bearer `access_token`
103
+ 2. **`sign_in`** — establishes a Password Safe session using that token → returns a `BtpsObject` with user info
104
+
105
+ ```ruby
106
+ client = BtpsClient::Client.new(host: 'vault.example.com',
107
+ client_id: ENV['BTPS_CLIENT_ID'],
108
+ client_secret: ENV['BTPS_CLIENT_SECRET'])
109
+
110
+ # Step 1 — acquire an OAuth2 access token
111
+ token_response = client.auth.acquire_token
112
+ access_token = token_response.access_token # String — use this as Bearer token
113
+
114
+ # Step 2 — sign in with the token to open a Password Safe session
115
+ authed_client = BtpsClient::Client.new(host: 'vault.example.com',
116
+ access_token: access_token)
117
+ authed_client.auth.sign_in # returns a BtpsObject with AuthUserModel fields
118
+
119
+ # Use authed_client for all subsequent API calls ...
120
+
121
+ # Sign out when done (invalidates the session server-side)
122
+ authed_client.auth.sign_out
123
+ ```
124
+
125
+ > **Note:** Pass `access_token` (the String from `acquire_token.access_token`) to `Client.new`,
126
+ > not the `BtpsObject` returned by `sign_in`.
127
+
128
+ ---
129
+
130
+ ## Folders
131
+
132
+ ```ruby
133
+ ss = client.secrets_safe
134
+
135
+ # List all folders
136
+ ss.folders.list
137
+
138
+ # Get a single folder
139
+ ss.folders.retrieve(folder_id)
140
+
141
+ # Create
142
+ ss.folders.create(Name: 'Ops', Description: 'Operations secrets')
143
+
144
+ # Update
145
+ ss.folders.update(folder_id, Name: 'Ops-Renamed')
146
+
147
+ # Move — DuplicateNameAction must be one of: Rename, Replace, Abort
148
+ ss.folders.move(folder_id,
149
+ DestinationFolderId: dest_uuid,
150
+ DuplicateNameAction: 'Rename')
151
+
152
+ # Delete
153
+ ss.folders.delete(folder_id)
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Safes
159
+
160
+ ```ruby
161
+ # CRUD
162
+ ss.safes.list
163
+ ss.safes.retrieve(safe_id)
164
+ ss.safes.create(Name: 'PaymentsSafe')
165
+ ss.safes.update(safe_id, Name: 'PaymentsSafe-v2')
166
+ ss.safes.delete(safe_id)
167
+
168
+ # Permissions
169
+ ss.safes.all_permissions
170
+ ss.safes.permissions(safe_id)
171
+
172
+ ss.safes.update_permissions(safe_id,
173
+ PrincipalID: 42,
174
+ PrincipalType: 1)
175
+
176
+ ss.safes.revoke_permissions(safe_id,
177
+ PrincipalID: 42,
178
+ PrincipalType: 1)
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Secrets
184
+
185
+ ```ruby
186
+ # List / retrieve / delete
187
+ ss.secrets.list
188
+ ss.secrets.retrieve(secret_id)
189
+ ss.secrets.delete(secret_id)
190
+
191
+ # Create a credential secret in a folder
192
+ ss.secrets.create_in_folder(folder_id,
193
+ Title: 'AWS Root',
194
+ Username: 'root',
195
+ Password: 'hunter2')
196
+
197
+ # Create a text secret
198
+ ss.secrets.create_text_in_folder(folder_id,
199
+ Title: 'deploy-key',
200
+ Text: File.read('deploy.key'))
201
+
202
+ # Create a file secret (multipart)
203
+ ss.secrets.create_file_in_folder(folder_id,
204
+ file: File.open('cert.pem'))
205
+
206
+ # Retrieve typed content
207
+ ss.secrets.retrieve_text(secret_id)
208
+ ss.secrets.retrieve_file(secret_id)
209
+ ss.secrets.download_file(secret_id) # returns raw bytes
210
+
211
+ # Update
212
+ ss.secrets.update(secret_id, Title: 'AWS Root Updated', Username: 'root')
213
+ ss.secrets.update_text(secret_id, Title: 'deploy-key', Text: 'new content')
214
+
215
+ # Move — DuplicateNameAction must be one of: Rename, Replace, Abort
216
+ ss.secrets.move(
217
+ SecretIds: [secret_id],
218
+ ShareIds: [],
219
+ DestinationFolderId: dest_folder_uuid,
220
+ DuplicateNameAction: 'Abort')
221
+
222
+ # Shares
223
+ ss.secrets.shares(secret_id)
224
+ ss.secrets.share_to_folder(secret_id, folder_id)
225
+ ss.secrets.unshare_from_folder(secret_id, folder_id)
226
+ ss.secrets.unshare_all(secret_id)
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Managed Accounts
232
+
233
+ Managed accounts are credentials controlled by BeyondTrust Password Safe.
234
+ Access them via `client.managed_accounts`.
235
+
236
+ ```ruby
237
+ # List all managed accounts
238
+ accounts = client.managed_accounts.list
239
+ accounts.each { |a| puts "#{a.system_name} / #{a.account_name}" }
240
+
241
+ # Retrieve a single managed account
242
+ client.managed_accounts.retrieve(account_id)
243
+
244
+ # List accounts scoped to a specific system
245
+ client.managed_accounts.list_by_system(system_id)
246
+
247
+ # List accounts assigned to a Smart Rule
248
+ client.managed_accounts.list_by_smart_rule(smart_rule_id)
249
+
250
+ # Create an account inside a system
251
+ client.managed_accounts.create_in_system(system_id,
252
+ AccountName: 'svc-deploy',
253
+ Password: 's3cr3t',
254
+ DomainName: 'corp.example.com',
255
+ IsWindowsSystem: true)
256
+
257
+ # Update an account
258
+ client.managed_accounts.update(account_id, Password: 'new-password')
259
+
260
+ # Delete a single account
261
+ client.managed_accounts.delete(account_id)
262
+
263
+ # Delete all accounts in a system
264
+ client.managed_accounts.delete_all_in_system(system_id)
265
+
266
+ # Delete by system + account name
267
+ client.managed_accounts.delete_by_system_and_name(system_id, 'svc-deploy')
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Requests (Credential Checkout)
273
+
274
+ Requests represent a checkout session for a managed account credential.
275
+ Access them via `client.requests`.
276
+
277
+ ```ruby
278
+ # List active requests
279
+ client.requests.list
280
+
281
+ # Create a checkout request
282
+ request = client.requests.create(
283
+ 'SystemID' => system_id,
284
+ 'AccountID' => account_id,
285
+ 'DurationMinutes' => 60,
286
+ 'Reason' => 'Deployment',
287
+ 'ConflictOption' => 'reuse' # reuse | renew — handles concurrent checkout
288
+ )
289
+ request_id = request.request_id
290
+
291
+ # Check in (release) a request — always call this when done
292
+ client.requests.checkin(request_id)
293
+
294
+ # Set rotate-on-checkin flag
295
+ client.requests.rotate_on_checkin(request_id)
296
+
297
+ # Approve / deny a pending request
298
+ client.requests.approve(request_id, 'Reason' => 'Approved by ops team')
299
+ client.requests.deny(request_id, 'Reason' => 'Out of policy')
300
+
301
+ # Terminate all requests for a managed account or system
302
+ client.requests.terminate_by_account(account_id)
303
+ client.requests.terminate_by_system(system_id)
304
+ ```
305
+
306
+ ---
307
+
308
+ ## Credentials
309
+
310
+ Retrieve or manage the actual credential value for a managed account.
311
+ Access them via `client.credentials`.
312
+
313
+ ```ruby
314
+ # Retrieve credential by active request ID (most common — use after requests.create)
315
+ credential = client.credentials.retrieve_by_request(request_id)
316
+ puts credential.value # the password / key
317
+
318
+ # Retrieve credential directly for an account (no request needed)
319
+ client.credentials.retrieve_for_account(account_id)
320
+
321
+ # Update credential for an account
322
+ client.credentials.update_for_account(account_id, Password: 'new-password')
323
+
324
+ # Update global credential settings
325
+ client.credentials.update_global(Password: 'global-default')
326
+
327
+ # Test the stored credential (verifies it works against the target system)
328
+ client.credentials.test_for_account(account_id)
329
+
330
+ # Trigger an immediate credential change on the target system
331
+ client.credentials.change_for_account(account_id)
332
+ ```
333
+
334
+ ### Full checkout workflow
335
+
336
+ ```ruby
337
+ # 1. Find the account
338
+ account = client.managed_accounts.list.find do |a|
339
+ a.system_name == 'system01' && a.account_name == 'svc-deploy'
340
+ end
341
+
342
+ # 2. Create checkout request
343
+ request = client.requests.create(
344
+ 'SystemID' => account.system_id,
345
+ 'AccountID' => account.account_id,
346
+ 'DurationMinutes' => 5,
347
+ 'ConflictOption' => 'reuse'
348
+ )
349
+
350
+ # 3. Retrieve the credential
351
+ begin
352
+ credential = client.credentials.retrieve_by_request(request.request_id)
353
+ puts credential.value
354
+ ensure
355
+ # 4. Always check in — even if step 3 raises
356
+ client.requests.checkin(request.request_id)
357
+ end
358
+ ```
359
+
360
+ ---
361
+
362
+
363
+
364
+ All SDK errors inherit from `BtpsClient::BtpsError` and carry `.http_status`, `.http_body`,
365
+ and `.request_id` (the `X-Request-Id` from the server response).
366
+
367
+ ```ruby
368
+ begin
369
+ ss.folders.create(Name: '')
370
+ rescue BtpsClient::ValidationError => e
371
+ # Client-side pre-flight validation failure (no HTTP request was made)
372
+ puts e.message # "Validation failed: Name must be at least 1 character(s)"
373
+ rescue BtpsClient::AuthenticationError => e
374
+ # HTTP 401
375
+ puts "Re-authenticate — request_id: #{e.request_id}"
376
+ rescue BtpsClient::NotFoundError => e
377
+ # HTTP 404
378
+ rescue BtpsClient::RateLimitError => e
379
+ # HTTP 429 — SDK retries automatically up to max_retries
380
+ rescue BtpsClient::ApiError => e
381
+ # HTTP 5xx
382
+ rescue BtpsClient::BtpsError => e
383
+ # Any other SDK error
384
+ end
385
+ ```
386
+
387
+ | Exception class | HTTP status |
388
+ |-----------------------------------|-------------------|
389
+ | `BtpsClient::ValidationError` | pre-flight / 422 |
390
+ | `BtpsClient::InvalidRequestError` | 400 |
391
+ | `BtpsClient::AuthenticationError` | 401 |
392
+ | `BtpsClient::PermissionError` | 403 |
393
+ | `BtpsClient::NotFoundError` | 404 |
394
+ | `BtpsClient::ConflictError` | 409 |
395
+ | `BtpsClient::RateLimitError` | 429 |
396
+ | `BtpsClient::ApiError` | 5xx |
397
+
398
+ ---
399
+
400
+ ## Client-side validation
401
+
402
+ The SDK validates request params **before** sending any HTTP request.
403
+ You get one error listing **all** failures at once:
404
+
405
+ ```ruby
406
+ ss.folders.move(folder_id,
407
+ DestinationFolderId: 'not-a-uuid',
408
+ DuplicateNameAction: 'TYPO')
409
+ # => BtpsClient::ValidationError:
410
+ # Validation failed: DestinationFolderId must be a valid UUID (...);
411
+ # DuplicateNameAction must be one of: Rename, Replace, Abort (got "TYPO")
412
+ ```
413
+
414
+ ---
415
+
416
+ ## Retry behaviour
417
+
418
+ The SDK retries automatically on `429 Too Many Requests` and `5xx` server errors,
419
+ using exponential back-off with jitter (50–100% of `2^(attempt-1) × 0.5` seconds, capped at 32 s).
420
+ Configure the maximum number of attempts with `max_retries` (default `3`).
421
+
422
+ ---
423
+
424
+ ## Security notes
425
+
426
+ - `Configuration#inspect` redacts all credentials — safe to log or print in debug sessions.
427
+ - `verify_ssl` defaults to `true`; never disable in production.
428
+ - Bearer tokens and client secrets are never written to the logger.
429
+
430
+ ---
431
+
432
+ ## Development
433
+
434
+ ```sh
435
+ bundle install
436
+ bundle exec rspec # run test suite
437
+ bundle exec rubocop # lint
438
+ ```
439
+
440
+ ---
441
+
442
+ ## License
443
+
444
+ MIT
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .action(action_name, id, ...) for RPC-style endpoints.
6
+ # Used for checkin, approve, deny, terminate, lock, rotate, etc.
7
+ module Actionable
8
+ # @param action_name [String] URL path segment (e.g. 'checkin', 'approve')
9
+ # @param id [String] Resource ID
10
+ # @param params [Hash] Optional request body
11
+ # @param method [Symbol] HTTP verb — default :post, can be :put or :delete
12
+ # @param config [Configuration]
13
+ def action(action_name, id, params: {}, method: :post, config: BtpsClient.config)
14
+ requestor = ApiRequestor.new(config)
15
+ path = "#{resource_url(id)}/#{action_name}"
16
+ body = params.empty? ? nil : params
17
+ data = requestor.request(method, path, body: body)
18
+ data ? new(data) : true
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .create(params, config:) — maps to POST /{resource_path}
6
+ module Creatable
7
+ # Returns the newly created resource object.
8
+ def create(params, config: BtpsClient.config)
9
+ Validator.validate!(schema_for(:create), params)
10
+ requestor = ApiRequestor.new(config)
11
+ data = requestor.request(:post, resource_path, body: params)
12
+ new(data)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .delete(id, config:) — maps to DELETE /{resource_path}/{id}
6
+ module Deletable
7
+ # Returns true on success (204 No Content expected).
8
+ def delete(id, config: BtpsClient.config)
9
+ requestor = ApiRequestor.new(config)
10
+ requestor.request(:delete, resource_url(id))
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .list(params, config:) — maps to GET /{resource_path}
6
+ module Listable
7
+ # Returns an array of resource objects.
8
+ def list(params = {}, config: BtpsClient.config)
9
+ requestor = ApiRequestor.new(config)
10
+ data = requestor.request(:get, resource_path, params: params)
11
+ items = data.is_a?(Hash) ? [data] : Array(data)
12
+ items.map { |item| new(item) }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .move(id, params, config:) — maps to PUT /{resource_path}/{id}/move
6
+ # Used by Folder (PUT /secrets-safe/folders/{folderId}/move).
7
+ module Movable
8
+ # Returns the updated resource object, or true on 204 No Content.
9
+ def move(id, params, config: BtpsClient.config)
10
+ Validator.validate!(schema_for(:move), params)
11
+ requestor = ApiRequestor.new(config)
12
+ data = requestor.request(:put, "#{resource_url(id)}/move", body: params)
13
+ data ? new(data) : true
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .list_for(parent_path, parent_id, ...) for nested collection endpoints.
6
+ # Used when a resource is listed under multiple parent types
7
+ # (e.g. ManagedAccounts under ManagedSystem, SmartRule, QuickRule).
8
+ module NestedListable
9
+ # @param parent_path [String] Parent resource path segment (e.g. 'managedsystems')
10
+ # @param parent_id [String] Parent resource ID
11
+ # @param sub_path [String] Override sub-path (defaults to leaf of own resource_path)
12
+ # @param params [Hash] Optional query parameters
13
+ # @param config [Configuration]
14
+ def list_for(parent_path, parent_id, sub_path: nil, params: {}, config: BtpsClient.config)
15
+ requestor = ApiRequestor.new(config)
16
+ leaf = sub_path || resource_path.split('/').last
17
+ path = "#{parent_path}/#{parent_id}/#{leaf}"
18
+ data = requestor.request(:get, path, params: params)
19
+ Array(data).map { |item| new(item) }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .retrieve(id, config:) — maps to GET /{resource_path}/{id}
6
+ module Retrievable
7
+ # Returns a single resource object by ID.
8
+ def retrieve(id, config: BtpsClient.config)
9
+ requestor = ApiRequestor.new(config)
10
+ data = requestor.request(:get, resource_url(id))
11
+ new(data)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Like Creatable, but for POST endpoints that return a plain scalar value
6
+ # (integer, string, etc.) instead of a JSON object.
7
+ #
8
+ # Usage — call the macro once in the resource class to declare which key
9
+ # should wrap the scalar before it is passed to new():
10
+ #
11
+ # extend ApiOperations::ScalarCreatable
12
+ # scalar_create_key 'RequestId'
13
+ #
14
+ # This will produce a resource where:
15
+ # Resource.create(params) → new('RequestId' => <scalar returned by API>)
16
+ module ScalarCreatable
17
+ def scalar_create_key(key = nil)
18
+ return (@scalar_create_key = key) if key
19
+
20
+ @scalar_create_key || raise(NotImplementedError, "#{name} must call scalar_create_key '<KeyName>'")
21
+ end
22
+
23
+ def create(params, config: BtpsClient.config)
24
+ Validator.validate!(schema_for(:create), params)
25
+ requestor = ApiRequestor.new(config)
26
+ scalar = requestor.request(:post, resource_path, body: params)
27
+ new(scalar_create_key => scalar)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .search(body, config:) — maps to POST /{resource_path}/search
6
+ # Used by resources with a POST /search endpoint (Assets, Keystrokes).
7
+ module Searchable
8
+ # Returns an array of resource objects matching the search criteria.
9
+ def search(body, config: BtpsClient.config)
10
+ requestor = ApiRequestor.new(config)
11
+ data = requestor.request(:post, "#{resource_path}/search", body: body)
12
+ Array(data).map { |item| new(item) }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .update(id, params, config:) — maps to PUT /{resource_path}/{id}
6
+ module Updatable
7
+ # Returns the updated resource object, or true on 204 No Content.
8
+ def update(id, params, config: BtpsClient.config)
9
+ Validator.validate!(schema_for(:update), params)
10
+ requestor = ApiRequestor.new(config)
11
+ data = requestor.request(:put, resource_url(id), body: params)
12
+ data ? new(data) : true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BtpsClient
4
+ module ApiOperations
5
+ # Adds .upload(parent_id, file:, ...) — sends multipart/form-data POST.
6
+ # Used for file secret creation and CSV bulk-import.
7
+ module Uploadable
8
+ # @param parent_id [String] ID of the parent container (e.g. folder ID)
9
+ # @param file [File|IO] File or IO object to upload
10
+ # @param metadata [Hash] Additional JSON metadata (sent as 'secretmetadata' field)
11
+ # @param sub_path [String] Optional path segment appended after parent_id
12
+ # @param base_path [String] Override base path (defaults to resource_path)
13
+ # @param config [Configuration]
14
+ def upload(parent_id, file:, metadata: {}, sub_path: nil, base_path: nil, config: BtpsClient.config)
15
+ requestor = ApiRequestor.new(config)
16
+ bp = base_path || resource_path
17
+ path = [bp, parent_id, sub_path].compact.join('/')
18
+
19
+ body = { file: file }
20
+ unless metadata.empty?
21
+ body['secretmetadata'] = metadata.is_a?(String) ? metadata : metadata.to_json
22
+ end
23
+
24
+ data = requestor.request(:post, path, body: body, opts: { multipart: true })
25
+ data ? new(data) : true
26
+ end
27
+ end
28
+ end
29
+ end