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).

Ruby


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.

# 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:

  1. acquire_token — OAuth2 client credentials grant → returns a bearer access_token
  2. sign_in — establishes a Password Safe session using that token → returns a BtpsObject with 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.  # 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 from acquire_token.access_token) to Client.new, not the BtpsObject returned by sign_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.all_permissions
ss.safes.permissions(safe_id)

ss.safes.update_permissions(safe_id,
  PrincipalID:   42,
  PrincipalType: 1)

ss.safes.revoke_permissions(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.}" }

# Retrieve a single managed account
client.managed_accounts.retrieve()

# 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(, Password: 'new-password')

# Delete a single account
client.managed_accounts.delete()

# 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'       => ,
  '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.()
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.()

# Update credential for an account
client.credentials.(, 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.()

# Trigger an immediate credential change on the target system
client.credentials.()

Full checkout workflow

# 1. Find the account
 = client.managed_accounts.list.find do |a|
  a.system_name == 'system01' && a. == 'svc-deploy'
end

# 2. Create checkout request
request = client.requests.create(
  'SystemID'        => .system_id,
  'AccountID'       => .,
  '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.message        # "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#inspect redacts all credentials — safe to log or print in debug sessions.
  • verify_ssl defaults to true; 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