Loading…
Loading…
Manage the lifecycle of SaaS relay agents, including creation, rotation, revocation, and versioning. This repository provides tenant-scoped database
The RelayAgentRepository is a server-side component responsible for managing the lifecycle of relay agents in the database. It provides a set of tenant-scoped Create, Read, and Update operations for platform operators to administer relays associated with specific tenants. This component interacts directly with the s2r_relay database table.
Its primary functions include:
The repository is designed with a strong security posture. It never stores or handles plaintext relay keys. Instead, it only persists a SHA-256 hash of the key (relay_key_hash), which is used for authentication by the runtime. The plaintext key is provided to the operator only once upon creation or rotation.
All operations are strictly isolated by tenant. An operation targeting a specific tenant can only see or modify relays belonging to that tenant. Attempts to access a relay owned by a different tenant will be treated as if the relay does not exist, preventing any cross-tenant information leakage.
NOTE
This functionality is available only in SaaS deployments. In other deployment models, the API endpoints that use this repository are disabled and will return a 404 Not Found error.
The RelayAgentRepository ensures strict tenant data isolation through a two-layered approach involving both database Row-Level Security (RLS) and explicit SQL query filtering. This is necessary because the service connects to the database as a table owner, which can bypass RLS policies.
The process for any given operation is as follows:
findByIdAndTenant(tenantId, id) is called, it first uses a utility to bind the specified tenantId to the current execution context. This utility safely stores and later restores any pre-existing context.tenantId to set the app.tenant_id session variable (GUC) for that connection. This activates the database's RLS policy, scoping all subsequent queries to that tenant's data.WHERE clause (... AND tenant_id = current_setting('app.tenant_id')). This provides a second layer of defense, ensuring that even if RLS were bypassed, the query is still hard-coded to access only the data for the intended tenant.SELECT, UPDATE, INSERT) is executed. Read queries are designed to never return sensitive data like the relay_key_hash.finally block, the original tenant context is restored, ensuring that the temporary context does not leak into subsequent operations on the same thread.This mechanism guarantees that each operation is self-contained and securely scoped to the target tenant specified by the platform operator.
The following diagram illustrates the sequence for retrieving a relay agent:
sequenceDiagram
participant C as API Caller
participant R as RelayAgentRepository
participant T as TenantContext
participant D as Database
C->>R: findByIdAndTenant(tenantId, relayAgentId)
R->>T: save(currentContext)
R->>T: set(tenantId)
R->>D: Get Connection (sets app.tenant_id GUC)
Note over R,D: SQL includes "WHERE id=? AND tenant_id=current_setting('app.tenant_id')"
R->>D: Executes SELECT query
D-->>R: Returns row or empty set
R->>T: restore(savedContext)
R-->>C: Returns RelayRow or nullThe RelayAgentRepository exposes methods for managing the complete lifecycle of a relay agent.
listForTenantLists all relay agents associated with a specific tenant, ordered by creation date in descending order.
public List<RelayRow> listForTenant(UUID tenantId)tenantId (UUID): The ID of the tenant whose relays are to be listed.List of RelayRow objects. The list will be empty if the tenant has no relays.findByIdAndTenantFetches a single relay agent by its unique ID, scoped to a specific tenant.
public RelayRow findByIdAndTenant(UUID tenantId, UUID id)tenantId (UUID): The ID of the tenant that should own the relay.id (UUID): The unique ID of the relay agent.RelayRow object if a matching relay is found for the specified tenant. Returns null if no such relay exists or if it belongs to a different tenant.createCreates a new relay agent record for a tenant. This method requires a pre-computed SHA-256 hash of the relay key. The plaintext key is never passed to or stored by the repository. The new relay is created with a status of 'active'.
public RelayRow create(UUID tenantId, UUID id, String relayId, String label, String keyHash, String keyPrefix, String channel)tenantId (UUID): The ID of the tenant for whom the relay is being created.id (UUID): The unique ID for the new relay agent row.relayId (String): A non-secret identifier for the relay.label (String): A human-readable label for the relay.keyHash (String): The SHA-256 hex digest of the plaintext relay key.keyPrefix (String): A short, non-secret prefix of the key for display purposes.channel (String): The initial software update channel. If null, the database defaults to 'release'.RelayRow object representing the newly created record, including database-generated values like created_at.rotateKeyUpdates the credentials for an existing relay agent. This replaces the stored key hash and prefix with new values and resets the relay's status to 'active'.
public int rotateKey(UUID tenantId, UUID id, String keyHash, String keyPrefix)tenantId (UUID): The ID of the tenant that owns the relay.id (UUID): The unique ID of the relay agent to update.keyHash (String): The new SHA-256 hex digest of the relay key.keyPrefix (String): The new non-secret key prefix.1 on success, or 0 if the relay ID was not found under the specified tenant.revokeDisables a relay agent by setting its status to 'revoked'. This action is idempotent.
public int revoke(UUID tenantId, UUID id)tenantId (UUID): The ID of the tenant that owns the relay.id (UUID): The unique ID of the relay agent to revoke.1 on success, or 0 if the relay ID was not found under the specified tenant.patchDesiredUpdates a relay agent's software update channel and/or desired version. This is used to control automatic updates for the agent.
public int patchDesired(UUID tenantId, UUID id, boolean setDesiredVersion, String desiredVersion, String channel)channel is provided (not null), the relay's channel is updated.setDesiredVersion is true, the relay's desired_version is updated to the value of desiredVersion. Providing null for desiredVersion will clear the setting, causing the relay to stay on its current version.channel is null and setDesiredVersion is false, the method performs no update but will return 1 if the relay exists for the tenant, confirming its presence.1 on success, or 0 if the relay ID was not found under the specified tenant.RelayRowThis record represents the data for a single relay agent as returned by read operations. It is a view of the database row that intentionally omits sensitive information like the key hash.
| Field | Type | Description |
|---|---|---|
id | UUID | The unique identifier for the relay agent record. |
tenantId | UUID | The ID of the tenant that owns this relay. |
relayId | String | A non-secret identifier for the relay. |
label | String | A human-readable label for the relay. |
status | String | The current status of the relay (e.g., active, revoked). |
version | String | The software version currently reported by the relay agent. |
lastHeartbeatAt | OffsetDateTime | The timestamp of the last successful heartbeat received from the relay. |
channel | String | The software update channel the relay is configured to follow (e.g., release). |
desiredVersion | String | The specific software version the relay is instructed to update to. null means stay current. |
keyPrefix | String | A short, non-secret prefix of the key used for display and identification. |
createdAt | OffsetDateTime | The timestamp when the relay agent record was created. |
A platform operator can register a new relay agent by invoking the create method. The caller is responsible for generating the relay key and computing its SHA-256 hash before calling the repository.
Method Invocation: The controller calls the create method with the tenant's ID and the new relay's details.
// Example parameters
UUID tenantId = UUID.fromString("...");
UUID newRelayUUID = UUID.fromString("...");
String relayId = "prod-relay-01";
String label = "Production Relay (East)";
String keyHash = "a1b2c3d4..."; // SHA-256 hex digest of the key
String keyPrefix = "s2k_abc123";
String channel = "release";
RelayRow newRelay = relayAgentRepository.create(
tenantId, newRelayUUID, relayId, label, keyHash, keyPrefix, channel
);
SQL Execution: The repository executes the following SQL INSERT statement. The ? placeholders are filled with the parameters from the method call.
insert into s2r_relay (id, tenant_id, relay_id, relay_key_hash, key_prefix, label, status, channel)
values (?, ?, ?, ?, ?, ?, 'active', coalesce(?, 'release'))
Result: The method then re-reads the newly created row from the database and returns it as a RelayRow object. This object includes all the provided details plus any values set by the database, such as the created_at timestamp.
Condition: An update, rotate, or revoke operation returns 0 rows affected, or a find operation returns null, even when you expect the relay agent to exist.
Trigger: This occurs if the relay agent id either does not exist or belongs to a different tenantId than the one specified in the method call.
Meaning: This is the expected behavior for enforcing tenant isolation. The system does not distinguish between "does not exist" and "exists but belongs to another tenant" to prevent leaking information about the existence of resources across tenant boundaries. The calling service should treat this outcome as a "404 Not Found" error and report it to the operator accordingly.