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.
- checksums.yaml +7 -0
- data/README.md +444 -0
- data/lib/btps_client/api_operations/actionable.rb +22 -0
- data/lib/btps_client/api_operations/creatable.rb +16 -0
- data/lib/btps_client/api_operations/deletable.rb +15 -0
- data/lib/btps_client/api_operations/listable.rb +16 -0
- data/lib/btps_client/api_operations/movable.rb +17 -0
- data/lib/btps_client/api_operations/nested_listable.rb +23 -0
- data/lib/btps_client/api_operations/retrievable.rb +15 -0
- data/lib/btps_client/api_operations/scalar_creatable.rb +31 -0
- data/lib/btps_client/api_operations/searchable.rb +16 -0
- data/lib/btps_client/api_operations/updatable.rb +16 -0
- data/lib/btps_client/api_operations/uploadable.rb +29 -0
- data/lib/btps_client/api_requestor.rb +143 -0
- data/lib/btps_client/api_resource.rb +45 -0
- data/lib/btps_client/btps_object.rb +58 -0
- data/lib/btps_client/client.rb +37 -0
- data/lib/btps_client/configuration.rb +71 -0
- data/lib/btps_client/errors.rb +39 -0
- data/lib/btps_client/resources/credential.rb +98 -0
- data/lib/btps_client/resources/folder.rb +36 -0
- data/lib/btps_client/resources/managed_account.rb +97 -0
- data/lib/btps_client/resources/request.rb +107 -0
- data/lib/btps_client/resources/safe.rb +77 -0
- data/lib/btps_client/resources/secret.rb +179 -0
- data/lib/btps_client/schema_builder.rb +50 -0
- data/lib/btps_client/services/auth_service.rb +57 -0
- data/lib/btps_client/services/credentials_service.rb +36 -0
- data/lib/btps_client/services/folders_service.rb +37 -0
- data/lib/btps_client/services/managed_accounts_service.rb +48 -0
- data/lib/btps_client/services/requests_service.rb +44 -0
- data/lib/btps_client/services/safes_service.rb +48 -0
- data/lib/btps_client/services/secrets_safe_service.rb +25 -0
- data/lib/btps_client/services/secrets_service.rb +96 -0
- data/lib/btps_client/validator.rb +129 -0
- data/lib/btps_client/version.rb +5 -0
- data/lib/btps_client.rb +64 -0
- 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
|
+
[](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
|