Implementation:OpenHands OpenHands SaasSecretsStore
| Knowledge Sources | |
|---|---|
| Domains | Storage, Security, Secrets_Management |
| Last Updated | 2026-02-11 21:00 GMT |
Overview
Encrypted secrets store for user custom secrets with Fernet symmetric encryption and organization scoping, provided by the OpenHands enterprise storage layer.
Description
SaasSecretsStore manages user-defined custom secrets (such as API keys and tokens for external services) with transparent encryption at rest. All secret values are encrypted using Fernet symmetric encryption before being persisted to the database and decrypted on read.
The store is scoped to an organization, meaning secrets are isolated per-org and users can only access secrets within their organization. The _fernet() method initializes and caches the Fernet cipher using a key derived from the application configuration.
The store() method uses a delete-and-replace pattern: it first deletes all existing secrets for the user within the org, then inserts the new set. This atomic replacement avoids partial-update scenarios and ensures the stored secrets always reflect the user's most recent complete set.
The internal _encrypt_kwargs() and _decrypt_kwargs() methods handle the transformation of secret values between plaintext (in application memory) and encrypted (in the database) representations.
Usage
Use SaasSecretsStore to manage user custom secrets in the SaaS settings UI. Call load() to retrieve and decrypt a user's secrets for display or injection into runtime environments. Call store() to persist an updated set of secrets, replacing any previously stored values.
Code Reference
Source Location
- Repository: OpenHands
- File: enterprise/storage/saas_secrets_store.py
- Lines: 1-136
Signature
class SaasSecretsStore:
def __init__(self, session: Session, org_id: str, config: AppConfig):
...
def load(self, user_id: str) -> dict:
"""Loads and decrypts all secrets for the given user within the org."""
...
def store(self, user_id: str, secrets: dict) -> None:
"""Deletes existing secrets and replaces with the new set (delete-and-replace)."""
...
def _decrypt_kwargs(self, encrypted_data: bytes) -> dict:
"""Decrypts Fernet-encrypted bytes back to a dictionary."""
...
def _encrypt_kwargs(self, data: dict) -> bytes:
"""Encrypts a dictionary to Fernet-encrypted bytes."""
...
def _fernet(self) -> Fernet:
"""Returns a cached Fernet cipher instance derived from the app config key."""
...
Import
from enterprise.storage.saas_secrets_store import SaasSecretsStore
I/O Contract
Inputs
Constructor
| Name | Type | Required | Description |
|---|---|---|---|
| session | Session | Yes | SQLAlchemy database session for executing queries |
| org_id | str | Yes | Organization ID that scopes the secrets |
| config | AppConfig | Yes | Application configuration containing the Fernet encryption key |
load()
| Name | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | The user whose secrets to load and decrypt |
store()
| Name | Type | Required | Description |
|---|---|---|---|
| user_id | str | Yes | The user whose secrets to replace |
| secrets | dict | Yes | Dictionary of secret name-value pairs to encrypt and store |
Outputs
| Method | Return Type | Description |
|---|---|---|
| load() | dict | Dictionary of decrypted secret name-value pairs for the user |
| store() | None | Deletes all existing secrets and inserts the new encrypted set |
| _decrypt_kwargs() | dict | Plaintext dictionary from encrypted bytes |
| _encrypt_kwargs() | bytes | Fernet-encrypted bytes from a plaintext dictionary |
| _fernet() | Fernet | Cached Fernet cipher instance |
Usage Examples
Loading and Storing Secrets
from enterprise.storage.saas_secrets_store import SaasSecretsStore
secrets_store = SaasSecretsStore(
session=db_session,
org_id="org-123",
config=app_config
)
# Load existing secrets for a user
user_secrets = secrets_store.load(user_id="user-456")
# Returns: {"GITHUB_TOKEN": "ghp_xxx", "SLACK_WEBHOOK": "https://hooks.slack.com/..."}
# Store updated secrets (replaces all existing secrets)
secrets_store.store(
user_id="user-456",
secrets={
"GITHUB_TOKEN": "ghp_new_token",
"SLACK_WEBHOOK": "https://hooks.slack.com/new-url",
"CUSTOM_API_KEY": "sk-new-key"
}
)
Delete-and-Replace Behavior
# If a user previously had 3 secrets and stores 2 new ones,
# the old 3 are deleted and only the new 2 remain:
secrets_store.store(
user_id="user-456",
secrets={
"GITHUB_TOKEN": "ghp_updated",
"NEW_SECRET": "new_value"
# SLACK_WEBHOOK and CUSTOM_API_KEY are removed
}
)
# Subsequent load returns only the 2 new secrets:
result = secrets_store.load(user_id="user-456")
# Returns: {"GITHUB_TOKEN": "ghp_updated", "NEW_SECRET": "new_value"}
Related Pages
Related Implementations
- Implementation:OpenHands_OpenHands_SaasSettingsStore -- Settings store that also handles encrypted values