btps_client — BeyondTrust Password Safe Ruby SDK
Ruby SDK for the BeyondTrust Password Safe REST API v3. Covers Auth, Secrets Safe Folders, Safes, and Secrets (credential, text, and file types).
Installation
Add to your Gemfile:
gem 'btps_client'
Or install directly:
gem install btps_client
Quick start
require 'btps_client'
# 1. Configure once (usually in an initializer)
BtpsClient.configure do |c|
c.host = 'vault.example.com'
c.base_path = 'BeyondTrust/api/public/v3'
c.client_id = ENV['BTPS_CLIENT_ID']
c.client_secret = ENV['BTPS_CLIENT_SECRET']
c.verify_ssl = true
end
# 2. Acquire an OAuth2 access token (client credentials grant)
client = BtpsClient::Client.new
token_response = client.auth.acquire_token
access_token = token_response.access_token # String
# 3. Sign in to open a Password Safe session
authed_client = BtpsClient::Client.new(access_token: access_token)
authed_client.auth.sign_in
# 4. Use the authenticated client for all API calls
ss = authed_client.secrets_safe
# List folders
folders = ss.folders.list
folders.each { |f| puts f.name }
# Create a credential secret
secret = ss.secrets.create_in_folder(folder_id,
Title: 'My App DB',
Username: 'dbadmin',
Password: 's3cr3t'
)
puts secret.id
Configuration
All options can be set globally via BtpsClient.configure or per-client via Client.new(...).
Credentials fall back to environment variables when not supplied in code.
| Option | Default | Env var | Description |
|---|---|---|---|
host |
nil (required) |
— | Hostname of the Password Safe server |
base_path |
api/public/v3 |
— | API path prefix |
scheme |
https |
— | https or http |
port |
nil |
— | Override port (omit for default) |
access_token |
nil |
BTPS_ACCESS_TOKEN |
Bearer token (post sign-in) |
client_id |
nil |
BTPS_CLIENT_ID |
OAuth client ID |
client_secret |
nil |
BTPS_CLIENT_SECRET |
OAuth client secret |
verify_ssl |
true |
— | Set false only in dev/test |
timeout |
30 |
— | Read timeout in seconds |
connect_timeout |
10 |
— | Connect timeout in seconds |
max_retries |
3 |
— | Retry attempts on 429 / 5xx |
logger |
nil |
— | Any object responding to debug/warn/error |
# Per-client override (does not affect the global config)
client = BtpsClient::Client.new(
host: 'vault.example.com',
access_token: 'eyJ...',
logger: Logger.new($stdout)
)
Authentication
BeyondTrust Password Safe uses a two-step auth flow:
acquire_token— OAuth2 client credentials grant → returns a beareraccess_tokensign_in— establishes a Password Safe session using that token → returns aBtpsObjectwith user info
client = BtpsClient::Client.new(host: 'vault.example.com',
client_id: ENV['BTPS_CLIENT_ID'],
client_secret: ENV['BTPS_CLIENT_SECRET'])
# Step 1 — acquire an OAuth2 access token
token_response = client.auth.acquire_token
access_token = token_response.access_token # String — use this as Bearer token
# Step 2 — sign in with the token to open a Password Safe session
authed_client = BtpsClient::Client.new(host: 'vault.example.com',
access_token: access_token)
authed_client.auth.sign_in # returns a BtpsObject with AuthUserModel fields
# Use authed_client for all subsequent API calls ...
# Sign out when done (invalidates the session server-side)
authed_client.auth.sign_out
Note: Pass
access_token(the String fromacquire_token.access_token) toClient.new, not theBtpsObjectreturned bysign_in.
Folders
ss = client.secrets_safe
# List all folders
ss.folders.list
# Get a single folder
ss.folders.retrieve(folder_id)
# Create
ss.folders.create(Name: 'Ops', Description: 'Operations secrets')
# Update
ss.folders.update(folder_id, Name: 'Ops-Renamed')
# Move — DuplicateNameAction must be one of: Rename, Replace, Abort
ss.folders.move(folder_id,
DestinationFolderId: dest_uuid,
DuplicateNameAction: 'Rename')
# Delete
ss.folders.delete(folder_id)
Safes
# CRUD
ss.safes.list
ss.safes.retrieve(safe_id)
ss.safes.create(Name: 'PaymentsSafe')
ss.safes.update(safe_id, Name: 'PaymentsSafe-v2')
ss.safes.delete(safe_id)
# Permissions
ss.safes.
ss.safes.(safe_id)
ss.safes.(safe_id,
PrincipalID: 42,
PrincipalType: 1)
ss.safes.(safe_id,
PrincipalID: 42,
PrincipalType: 1)
Secrets
# List / retrieve / delete
ss.secrets.list
ss.secrets.retrieve(secret_id)
ss.secrets.delete(secret_id)
# Create a credential secret in a folder
ss.secrets.create_in_folder(folder_id,
Title: 'AWS Root',
Username: 'root',
Password: 'hunter2')
# Create a text secret
ss.secrets.create_text_in_folder(folder_id,
Title: 'deploy-key',
Text: File.read('deploy.key'))
# Create a file secret (multipart)
ss.secrets.create_file_in_folder(folder_id,
file: File.open('cert.pem'))
# Retrieve typed content
ss.secrets.retrieve_text(secret_id)
ss.secrets.retrieve_file(secret_id)
ss.secrets.download_file(secret_id) # returns raw bytes
# Update
ss.secrets.update(secret_id, Title: 'AWS Root Updated', Username: 'root')
ss.secrets.update_text(secret_id, Title: 'deploy-key', Text: 'new content')
# Move — DuplicateNameAction must be one of: Rename, Replace, Abort
ss.secrets.move(
SecretIds: [secret_id],
ShareIds: [],
DestinationFolderId: dest_folder_uuid,
DuplicateNameAction: 'Abort')
# Shares
ss.secrets.shares(secret_id)
ss.secrets.share_to_folder(secret_id, folder_id)
ss.secrets.unshare_from_folder(secret_id, folder_id)
ss.secrets.unshare_all(secret_id)
Managed Accounts
Managed accounts are credentials controlled by BeyondTrust Password Safe.
Access them via client.managed_accounts.
# List all managed accounts
accounts = client.managed_accounts.list
accounts.each { |a| puts "#{a.system_name} / #{a.account_name}" }
# Retrieve a single managed account
client.managed_accounts.retrieve(account_id)
# List accounts scoped to a specific system
client.managed_accounts.list_by_system(system_id)
# List accounts assigned to a Smart Rule
client.managed_accounts.list_by_smart_rule(smart_rule_id)
# Create an account inside a system
client.managed_accounts.create_in_system(system_id,
AccountName: 'svc-deploy',
Password: 's3cr3t',
DomainName: 'corp.example.com',
IsWindowsSystem: true)
# Update an account
client.managed_accounts.update(account_id, Password: 'new-password')
# Delete a single account
client.managed_accounts.delete(account_id)
# Delete all accounts in a system
client.managed_accounts.delete_all_in_system(system_id)
# Delete by system + account name
client.managed_accounts.delete_by_system_and_name(system_id, 'svc-deploy')
Requests (Credential Checkout)
Requests represent a checkout session for a managed account credential.
Access them via client.requests.
# List active requests
client.requests.list
# Create a checkout request
request = client.requests.create(
'SystemID' => system_id,
'AccountID' => account_id,
'DurationMinutes' => 60,
'Reason' => 'Deployment',
'ConflictOption' => 'reuse' # reuse | renew — handles concurrent checkout
)
request_id = request.request_id
# Check in (release) a request — always call this when done
client.requests.checkin(request_id)
# Set rotate-on-checkin flag
client.requests.rotate_on_checkin(request_id)
# Approve / deny a pending request
client.requests.approve(request_id, 'Reason' => 'Approved by ops team')
client.requests.deny(request_id, 'Reason' => 'Out of policy')
# Terminate all requests for a managed account or system
client.requests.terminate_by_account(account_id)
client.requests.terminate_by_system(system_id)
Credentials
Retrieve or manage the actual credential value for a managed account.
Access them via client.credentials.
# Retrieve credential by active request ID (most common — use after requests.create)
credential = client.credentials.retrieve_by_request(request_id)
puts credential.value # the password / key
# Retrieve credential directly for an account (no request needed)
client.credentials.retrieve_for_account(account_id)
# Update credential for an account
client.credentials.update_for_account(account_id, Password: 'new-password')
# Update global credential settings
client.credentials.update_global(Password: 'global-default')
# Test the stored credential (verifies it works against the target system)
client.credentials.test_for_account(account_id)
# Trigger an immediate credential change on the target system
client.credentials.change_for_account(account_id)
Full checkout workflow
# 1. Find the account
account = client.managed_accounts.list.find do |a|
a.system_name == 'system01' && a.account_name == 'svc-deploy'
end
# 2. Create checkout request
request = client.requests.create(
'SystemID' => account.system_id,
'AccountID' => account.account_id,
'DurationMinutes' => 5,
'ConflictOption' => 'reuse'
)
# 3. Retrieve the credential
begin
credential = client.credentials.retrieve_by_request(request.request_id)
puts credential.value
ensure
# 4. Always check in — even if step 3 raises
client.requests.checkin(request.request_id)
end
All SDK errors inherit from BtpsClient::BtpsError and carry .http_status, .http_body,
and .request_id (the X-Request-Id from the server response).
begin
ss.folders.create(Name: '')
rescue BtpsClient::ValidationError => e
# Client-side pre-flight validation failure (no HTTP request was made)
puts e. # "Validation failed: Name must be at least 1 character(s)"
rescue BtpsClient::AuthenticationError => e
# HTTP 401
puts "Re-authenticate — request_id: #{e.request_id}"
rescue BtpsClient::NotFoundError => e
# HTTP 404
rescue BtpsClient::RateLimitError => e
# HTTP 429 — SDK retries automatically up to max_retries
rescue BtpsClient::ApiError => e
# HTTP 5xx
rescue BtpsClient::BtpsError => e
# Any other SDK error
end
| Exception class | HTTP status |
|---|---|
BtpsClient::ValidationError |
pre-flight / 422 |
BtpsClient::InvalidRequestError |
400 |
BtpsClient::AuthenticationError |
401 |
BtpsClient::PermissionError |
403 |
BtpsClient::NotFoundError |
404 |
BtpsClient::ConflictError |
409 |
BtpsClient::RateLimitError |
429 |
BtpsClient::ApiError |
5xx |
Client-side validation
The SDK validates request params before sending any HTTP request. You get one error listing all failures at once:
ss.folders.move(folder_id,
DestinationFolderId: 'not-a-uuid',
DuplicateNameAction: 'TYPO')
# => BtpsClient::ValidationError:
# Validation failed: DestinationFolderId must be a valid UUID (...);
# DuplicateNameAction must be one of: Rename, Replace, Abort (got "TYPO")
Retry behaviour
The SDK retries automatically on 429 Too Many Requests and 5xx server errors,
using exponential back-off with jitter (50–100% of 2^(attempt-1) × 0.5 seconds, capped at 32 s).
Configure the maximum number of attempts with max_retries (default 3).
Security notes
Configuration#inspectredacts all credentials — safe to log or print in debug sessions.verify_ssldefaults totrue; never disable in production.- Bearer tokens and client secrets are never written to the logger.
Development
bundle install
bundle exec rspec # run test suite
bundle exec rubocop # lint
License
MIT