RFC-047: Cross-Proxy Namespace Reservation with Lease Management
Abstract
This RFC specifies the cross-proxy namespace reservation system with JWT-based lease management for Prism. When clients request a namespace, proxies coordinate with the admin plane to ensure global uniqueness and issue time-limited leases. Clients receive JWT tokens scoped to their namespace that grant configuration permissions. Leases must be refreshed periodically with a grace period before expiration. The system supports standalone proxy mode for single-tenant deployments without admin plane dependencies.
Key Benefits:
- Global Uniqueness: Admin plane ensures namespace names are unique across all proxy instances
- Secure Configuration: JWT tokens grant namespace-scoped configuration permissions
- Lease Lifecycle: TTL-based leases with refresh mechanism prevent abandoned namespaces
- Graceful Degradation: Standalone proxies operate without admin plane coordination
- Audit Trail: All reservations logged for compliance and debugging
Motivation
Problem Statement
Current namespace creation in Prism lacks coordination between proxy instances:
Problem 1: No Cross-Proxy Coordination
- Multiple proxies can create namespaces with the same name independently
- No global namespace registry
- Name collisions lead to data routing errors and security violations
Problem 2: Unauthorized Configuration
- No token-based authorization for namespace configuration
- Any client can modify any namespace
- No audit trail of namespace operations
Problem 3: Namespace Abandonment
- Namespaces created but never used remain indefinitely
- No mechanism to reclaim unused namespace allocations
- Resource waste from zombie namespaces
Problem 4: Standalone vs Multi-Proxy Modes
- Single-tenant deployments don't need admin plane coordination
- No configuration option to disable admin plane dependency
- Deployment complexity for simple use cases
Goals
- Cross-Proxy Reservation: Clients request namespaces through any proxy; admin plane ensures uniqueness
- JWT-Based Authorization: Namespace tokens grant configuration permissions scoped to specific namespace
- Lease Lifecycle Management: TTL-based leases with refresh mechanism and grace periods
- Configuration-Time Binding: Namespace reservation happens before pattern instantiation
- Standalone Mode: Proxies can operate without admin plane for single-tenant deployments
- Audit Logging: All reservation operations logged with client identity and timestamps
Non-Goals
- Data Plane Authorization: This RFC covers namespace reservation/configuration only, not data access
- Multi-Cluster Namespaces: Cross-cluster namespace coordination (see RFC-012)
- Namespace Migration: Moving namespaces between proxies after creation
- Hierarchical Namespaces: Nested or qualified namespace names (use flat naming)
Architecture Overview
System Components
┌─────────────────────────────────────────────────────────────────┐
│ Client Applications │
└────────────────────────┬────────────────────────────────────────┘
│ 1. Request namespace via any proxy
│
┌────────────────────────▼────────────────────────────────────────┐
│ Prism Proxy Fleet │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Proxy-01 │ │ Proxy-02 │ │ Proxy-03 │ │
│ │ (us-east-1a) │ │ (us-east-1b) │ │ (us-west-2a) │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
└──────────┼──────────────────┼──────────────────┼────────────────┘
│ 2. Forward reservation request to admin plane
│
┌──────────▼──────────────────▼──────────────────▼────────────────┐
│ Admin Control Plane │
│ (Raft Consensus) │
│ │
│ ┌─────────────────────────────── ──────────────────────────┐ │
│ │ Namespace Registry │ │
│ │ - Global uniqueness enforcement │ │
│ │ - Lease issuance and tracking │ │
│ │ - JWT token generation │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SQLite Storage (ADR-054) │ │
│ │ - namespaces: name, owner, created_at, lease_expires │ │
│ │ - leases: lease_id, namespace, expires_at, jwt_token │ │
│ │ - audit_log: operation, actor, timestamp, result │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────┬───────────────────────────────────────────────────────┘
│ 3. Return JWT token + lease info
│
┌──────────▼───────────────────────────────────────────────────────┐
│ Client with JWT Token │
│ - Can configure namespace (add patterns, backends) │
│ - Must refresh lease before expiration │
│ - Token scoped to single namespace │
└──────────────────────────────────────────────────────────────────┘
Data Flow: Namespace Reservation
Lease Lifecycle
Timeline of namespace lease:
t=0h t=12h t=23h t=24h t=25h
│────────────┼──────────────────┼──────────────┼────────────┼─────>
│ │ │ │ │
│ │ │ Grace Period │ Lease │ Namespace
│ Active │ Refresh OK │ Warning │ Expires │ Deleted
│ │ │ │ │
│ │ │ │ │
└─ Reserve └─ Client can └─ Client └─ JWT └─ Admin
Namespace refresh lease should invalid purges
anytime refresh namespace
States:
1. Active (0h-23h): Lease valid, client can operate normally
2. Grace Period (23h-24h): Lease about to expire, client receives warnings
3. Expired (24h+): JWT token invalid, client must re-reserve or extend
4. Purged (25h+): Namespace deleted if lease not renewed
Refresh Behavior:
- Client can refresh at any time during Active period
- Each refresh extends lease by TTL from current time
- Recommended: Refresh at 50% of TTL (12h for 24h lease)
- Grace period: Last 1 hour before expiration
Namespace Reservation Protocol
Protobuf Definition
// proto/prism/admin/v1/namespace.proto
syntax = "proto3";
package prism.admin.v1;
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
// Namespace reservation and lease management
service NamespaceReservationService {
// Reserve a new namespace globally
rpc ReserveNamespace(ReserveNamespaceRequest)
returns (ReserveNamespaceResponse);
// Refresh an existing namespace lease
rpc RefreshLease(RefreshLeaseRequest)
returns (RefreshLeaseResponse);
// Release a namespace (explicit cleanup)
rpc ReleaseNamespace(ReleaseNamespaceRequest)
returns (ReleaseNamespaceResponse);
// Get namespace information
rpc GetNamespace(GetNamespaceRequest)
returns (GetNamespaceResponse);
// List all namespaces (admin only)
rpc ListNamespaces(ListNamespacesRequest)
returns (ListNamespacesResponse);
}
message ReserveNamespaceRequest {
// Namespace name (must be unique globally)
string name = 1;
// Principal requesting namespace (user or service identity)
string principal = 2;
// Optional: Team or organization
string team = 3;
// Optional: Metadata for namespace
map<string, string> metadata = 4;
// Optional: Custom TTL (default: 24 hours)
google.protobuf.Duration lease_ttl = 5;
}
message ReserveNamespaceResponse {
// Success indicator
bool success = 1;
// JWT token for namespace operations
string jwt_token = 2;
// Lease identifier
string lease_id = 3;
// Lease expiration timestamp
google.protobuf.Timestamp expires_at = 4;
// Time until expiration
google.protobuf.Duration ttl = 5;
// Recommended refresh time (50% of TTL)
google.protobuf.Timestamp refresh_after = 6;
// Namespace details
NamespaceInfo namespace = 7;
}
message RefreshLeaseRequest {
// Namespace name
string namespace = 1;
// Current JWT token (for authorization)
string jwt_token = 2;
// Optional: Extend lease by custom duration
google.protobuf.Duration extend_by = 3;
}
message RefreshLeaseResponse {
bool success = 1;
// New JWT token (existing token becomes invalid)
string jwt_token = 2;
// New expiration time
google.protobuf.Timestamp expires_at = 3;
// Time until expiration
google.protobuf.Duration ttl = 4;
}
message ReleaseNamespaceRequest {
string namespace = 1;
string jwt_token = 2;
}
message ReleaseNamespaceResponse {
bool success = 1;
string message = 2;
}
message GetNamespaceRequest {
string name = 1;
}
message GetNamespaceResponse {
NamespaceInfo namespace = 1;
LeaseInfo lease = 2;
}
message ListNamespacesRequest {
// Pagination
int32 page_size = 1;
string page_token = 2;
// Filters
optional string owner_filter = 3;
optional string team_filter = 4;
optional bool include_expired = 5;
}
message ListNamespacesResponse {
repeated NamespaceInfo namespaces = 1;
string next_page_token = 2;
int32 total_count = 3;
}
message NamespaceInfo {
string name = 1;
string owner = 2;
string team = 3;
google.protobuf.Timestamp created_at = 4;
google.protobuf.Timestamp updated_at = 5;
map<string, string> metadata = 6;
NamespaceStatus status = 7;
}
message LeaseInfo {
string lease_id = 1;
string namespace = 2;
google.protobuf.Timestamp expires_at = 3;
google.protobuf.Timestamp last_refreshed_at = 4;
int32 refresh_count = 5;
bool in_grace_period = 6;
}
enum NamespaceStatus {
NAMESPACE_STATUS_UNSPECIFIED = 0;
NAMESPACE_STATUS_ACTIVE = 1;
NAMESPACE_STATUS_GRACE_PERIOD = 2;
NAMESPACE_STATUS_EXPIRED = 3;
NAMESPACE_STATUS_RELEASED = 4;
}
JWT Token Structure
Namespace JWT tokens follow RFC 7519 with custom claims:
{
"iss": "prism-admin",
"sub": "user@example.com",
"aud": ["prism-proxy"],
"exp": 1735084800,
"iat": 1735048800,
"nbf": 1735048800,
"jti": "lease_abc123",
"prism": {
"namespace": "orders-prod",
"lease_id": "abc123-def456-789",
"permissions": [
"namespace:configure",
"pattern:create",
"pattern:update",
"backend:bind"
],
"owner": "user@example.com",
"team": "payments-team"
}
}
Standard Claims:
iss: Issuer (prism-admin)sub: Subject (principal who reserved namespace)aud: Audience (prism-proxy instances)exp: Expiration time (Unix timestamp)iat: Issued at timenbf: Not before timejti: JWT ID (lease_id for revocation)
Custom Claims (prism):
namespace: Namespace name this token is scoped tolease_id: Lease identifier for trackingpermissions: Operations this token grantsowner: Principal who owns this namespaceteam: Team or organization
Implementation
Admin Plane: Namespace Registry
// cmd/prism-admin/namespace_registry.go
package main
import (
"context"
"crypto/rsa"
"database/sql"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
type NamespaceRegistry struct {
db *sql.DB
jwtPrivKey *rsa.PrivateKey
jwtPubKey *rsa.PublicKey
defaultTTL time.Duration
}
func NewNamespaceRegistry(db *sql.DB, privKey *rsa.PrivateKey, pubKey *rsa.PublicKey) *NamespaceRegistry {
return &NamespaceRegistry{
db: db,
jwtPrivKey: privKey,
jwtPubKey: pubKey,
defaultTTL: 24 * time.Hour,
}
}
func (nr *NamespaceRegistry) ReserveNamespace(
ctx context.Context,
req *ReserveNamespaceRequest,
) (*ReserveNamespaceResponse, error) {
// Validate namespace name
if err := validateNamespaceName(req.Name); err != nil {
return nil, err
}
// Start transaction
tx, err := nr.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
if err != nil {
return nil, err
}
defer tx.Rollback()
// Check if namespace already exists
var existing string
err = tx.QueryRowContext(ctx,
"SELECT name FROM namespaces WHERE name = ? AND (lease_expires IS NULL OR lease_expires > ?)",
req.Name, time.Now()).Scan(&existing)
if err == nil {
// Namespace exists
return nil, ErrNamespaceExists
} else if err != sql.ErrNoRows {
return nil, err
}
// Generate lease
leaseID := uuid.New().String()
ttl := nr.defaultTTL
if req.LeaseTtl != nil {
ttl = req.LeaseTtl.AsDuration()
}
expiresAt := time.Now().Add(ttl)
// Generate JWT token
token, err := nr.generateJWT(req.Name, req.Principal, req.Team, leaseID, expiresAt)
if err != nil {
return nil, err
}
// Insert namespace
_, err = tx.ExecContext(ctx, `
INSERT INTO namespaces (name, owner, team, created_at, updated_at, lease_expires, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, req.Name, req.Principal, req.Team, time.Now(), time.Now(), expiresAt, marshalMetadata(req.Metadata))
if err != nil {
return nil, err
}
// Insert lease
_, err = tx.ExecContext(ctx, `
INSERT INTO leases (lease_id, namespace, expires_at, last_refreshed_at, jwt_token, refresh_count)
VALUES (?, ?, ?, ?, ?, 0)
`, leaseID, req.Name, expiresAt, time.Now(), token)
if err != nil {
return nil, err
}
// Audit log
_, err = tx.ExecContext(ctx, `
INSERT INTO audit_log (operation, actor, namespace, timestamp, result, details)
VALUES (?, ?, ?, ?, ?, ?)
`, "ReserveNamespace", req.Principal, req.Name, time.Now(), "success", "")
if err != nil {
return nil, err
}
// Commit transaction
if err := tx.Commit(); err != nil {
return nil, err
}
return &ReserveNamespaceResponse{
Success: true,
JwtToken: token,
LeaseId: leaseID,
ExpiresAt: timestamppb.New(expiresAt),
Ttl: durationpb.New(ttl),
RefreshAfter: timestamppb.New(time.Now().Add(ttl / 2)),
Namespace: buildNamespaceInfo(req.Name, req.Principal, req.Team, req.Metadata),
}, nil
}
func (nr *NamespaceRegistry) RefreshLease(
ctx context.Context,
req *RefreshLeaseRequest,
) (*RefreshLeaseResponse, error) {
// Verify JWT token
claims, err := nr.verifyJWT(req.JwtToken)
if err != nil {
return nil, ErrInvalidToken
}
// Ensure token is for correct namespace
if claims.Namespace != req.Namespace {
return nil, ErrTokenNamespaceMismatch
}
// Start transaction
tx, err := nr.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
// Get current lease
var leaseID string
var currentExpiry time.Time
err = tx.QueryRowContext(ctx,
"SELECT lease_id, expires_at FROM leases WHERE namespace = ? AND lease_id = ?",
req.Namespace, claims.LeaseID).Scan(&leaseID, ¤tExpiry)
if err != nil {
return nil, ErrLeaseNotFound
}
// Check if lease has expired
if time.Now().After(currentExpiry) {
return nil, ErrLeaseExpired
}
// Calculate new expiration
extendBy := nr.defaultTTL
if req.ExtendBy != nil {
extendBy = req.ExtendBy.AsDuration()
}
newExpiry := time.Now().Add(extendBy)
// Generate new JWT token
newToken, err := nr.generateJWT(req.Namespace, claims.Subject, claims.Team, leaseID, newExpiry)
if err != nil {
return nil, err
}
// Update lease
_, err = tx.ExecContext(ctx, `
UPDATE leases
SET expires_at = ?, last_refreshed_at = ?, jwt_token = ?, refresh_count = refresh_count + 1
WHERE lease_id = ? AND namespace = ?
`, newExpiry, time.Now(), newToken, leaseID, req.Namespace)
if err != nil {
return nil, err
}
// Update namespace lease_expires
_, err = tx.ExecContext(ctx,
"UPDATE namespaces SET lease_expires = ?, updated_at = ? WHERE name = ?",
newExpiry, time.Now(), req.Namespace)
if err != nil {
return nil, err
}
// Audit log
_, err = tx.ExecContext(ctx, `
INSERT INTO audit_log (operation, actor, namespace, timestamp, result)
VALUES (?, ?, ?, ?, ?)
`, "RefreshLease", claims.Subject, req.Namespace, time.Now(), "success")
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return &RefreshLeaseResponse{
Success: true,
JwtToken: newToken,
ExpiresAt: timestamppb.New(newExpiry),
Ttl: durationpb.New(extendBy),
}, nil
}
func (nr *NamespaceRegistry) generateJWT(
namespace, principal, team, leaseID string,
expiresAt time.Time,
) (string, error) {
claims := jwt.MapClaims{
"iss": "prism-admin",
"sub": principal,
"aud": []string{"prism-proxy"},
"exp": expiresAt.Unix(),
"iat": time.Now().Unix(),
"nbf": time.Now().Unix(),
"jti": leaseID,
"prism": map[string]interface{}{
"namespace": namespace,
"lease_id": leaseID,
"permissions": []string{
"namespace:configure",
"pattern:create",
"pattern:update",
"backend:bind",
},
"owner": principal,
"team": team,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
return token.SignedString(nr.jwtPrivKey)
}
func (nr *NamespaceRegistry) verifyJWT(tokenString string) (*PrismClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, ErrInvalidSigningMethod
}
return nr.jwtPubKey, nil
})
if err != nil || !token.Valid {
return nil, ErrInvalidToken
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, ErrInvalidClaims
}
prismClaims := claims["prism"].(map[string]interface{})
return &PrismClaims{
Subject: claims["sub"].(string),
Namespace: prismClaims["namespace"].(string),
LeaseID: prismClaims["lease_id"].(string),
Team: prismClaims["team"].(string),
}, nil
}
// Background job to clean up expired leases
func (nr *NamespaceRegistry) StartLeaseCleanupJob(ctx context.Context) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
nr.cleanupExpiredLeases(ctx)
}
}
}
func (nr *NamespaceRegistry) cleanupExpiredLeases(ctx context.Context) error {
// Delete namespaces with expired leases (after grace period)
gracePeriod := 1 * time.Hour
cutoff := time.Now().Add(-gracePeriod)
result, err := nr.db.ExecContext(ctx, `
DELETE FROM namespaces
WHERE lease_expires < ?
AND NOT EXISTS (
SELECT 1 FROM patterns WHERE patterns.namespace = namespaces.name
)
`, cutoff)
if err != nil {
return err
}
rows, _ := result.RowsAffected()
if rows > 0 {
log.Printf("Cleaned up %d expired namespaces", rows)
}
return nil
}
Proxy: Standalone Mode
// prism-proxy/src/namespace/mod.rs
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use jsonwebtoken::{EncodingKey, DecodingKey, Validation, Algorithm};
pub struct NamespaceManager {
mode: OperatingMode,
admin_client: Option<AdminClient>,
local_registry: Arc<RwLock<HashMap<String, NamespaceEntry>>>,
jwt_secret: Option<EncodingKey>,
}
pub enum OperatingMode {
Standalone, // No admin plane, local namespace management
Coordinated, // Admin plane coordination for multi-proxy deployment
}
impl NamespaceManager {
pub fn new_standalone(jwt_secret: String) -> Self {
Self {
mode: OperatingMode::Standalone,
admin_client: None,
local_registry: Arc::new(RwLock::new(HashMap::new())),
jwt_secret: Some(EncodingKey::from_secret(jwt_secret.as_bytes())),
}
}
pub fn new_coordinated(admin_endpoint: String) -> Self {
Self {
mode: OperatingMode::Coordinated,
admin_client: Some(AdminClient::new(admin_endpoint)),
local_registry: Arc::new(RwLock::new(HashMap::new())),
jwt_secret: None,
}
}
pub async fn reserve_namespace(
&self,
req: ReserveNamespaceRequest,
) -> Result<ReserveNamespaceResponse> {
match &self.mode {
OperatingMode::Standalone => self.reserve_standalone(req).await,
OperatingMode::Coordinated => self.reserve_coordinated(req).await,
}
}
async fn reserve_standalone(
&self,
req: ReserveNamespaceRequest,
) -> Result<ReserveNamespaceResponse> {
let mut registry = self.local_registry.write().await;
// Check if namespace exists
if registry.contains_key(&req.name) {
return Err(Error::NamespaceExists);
}
// Create namespace entry
let entry = NamespaceEntry {
name: req.name.clone(),
owner: req.principal.clone(),
created_at: Utc::now(),
lease_expires: None, // No expiration in standalone mode
};
registry.insert(req.name.clone(), entry);
// Generate JWT token (no expiration for standalone)
let token = self.generate_standalone_jwt(&req.name, &req.principal)?;
Ok(ReserveNamespaceResponse {
success: true,
jwt_token: token,
lease_id: "standalone".to_string(),
expires_at: None,
ttl: None,
refresh_after: None,
namespace: Some(NamespaceInfo {
name: req.name,
owner: req.principal,
created_at: Some(prost_types::Timestamp::from(Utc::now())),
..Default::default()
}),
})
}
async fn reserve_coordinated(
&self,
req: ReserveNamespaceRequest,
) -> Result<ReserveNamespaceResponse> {
// Forward to admin plane
let admin_client = self.admin_client.as_ref().unwrap();
let response = admin_client.reserve_namespace(req).await?;
// Cache namespace locally for fast lookups
let mut registry = self.local_registry.write().await;
registry.insert(response.namespace.as_ref().unwrap().name.clone(), NamespaceEntry {
name: response.namespace.as_ref().unwrap().name.clone(),
owner: response.namespace.as_ref().unwrap().owner.clone(),
created_at: Utc::now(),
lease_expires: response.expires_at.map(|ts| ts.try_into().unwrap()),
});
Ok(response)
}
fn generate_standalone_jwt(&self, namespace: &str, principal: &str) -> Result<String> {
let claims = serde_json::json!({
"iss": "prism-proxy-standalone",
"sub": principal,
"aud": ["prism-proxy"],
"prism": {
"namespace": namespace,
"lease_id": "standalone",
"permissions": [
"namespace:configure",
"pattern:create",
"pattern:update",
"backend:bind"
],
"owner": principal
}
});
let header = jsonwebtoken::Header::new(Algorithm::HS256);
let token = jsonwebtoken::encode(
&header,
&claims,
self.jwt_secret.as_ref().unwrap()
)?;
Ok(token)
}
}
Configuration
Proxy Configuration
# prism-proxy.yaml
# Operating mode
namespace_management:
mode: coordinated # or "standalone"
# For coordinated mode
admin_endpoint: "admin.prism.local:8981"
admin_tls:
enabled: true
ca_cert: /etc/prism/certs/ca.crt
# For standalone mode
jwt_secret: ${PRISM_JWT_SECRET}
# Lease refresh settings
refresh:
enabled: true
check_interval: 30m # Check lease expiry every 30 min
refresh_threshold: 0.5 # Refresh when 50% of TTL elapsed
grace_period_warning: 1h # Warn when entering grace period
Admin Configuration
# prism-admin.yaml
namespace_registry:
# JWT signing
jwt_private_key: /etc/prism/keys/jwt-private.pem
jwt_public_key: /etc/prism/keys/jwt-public.pem
jwt_algorithm: RS256
# Lease settings
default_lease_ttl: 24h
max_lease_ttl: 168h # 7 days
min_lease_ttl: 1h
grace_period: 1h # Grace period before deletion
# Cleanup job
cleanup:
enabled: true
interval: 1h
delete_after_expiry: 1h # Delete 1h after lease expires
Client Usage Examples
Reserve Namespace
# Python client example
from prism_client import PrismClient
client = PrismClient(proxy_endpoint="localhost:8980")
# Reserve namespace
response = client.reserve_namespace(
name="orders-prod",
principal="user@example.com",
team="payments-team",
metadata={"environment": "production", "region": "us-east-1"}
)
# Store JWT token
jwt_token = response.jwt_token
lease_id = response.lease_id
expires_at = response.expires_at
print(f"Namespace reserved: {response.namespace.name}")
print(f"Lease expires at: {expires_at}")
print(f"Refresh after: {response.refresh_after}")
# Save token for future operations
with open(".prism-token", "w") as f:
f.write(jwt_token)
Configure Namespace with JWT
# Configure namespace (requires JWT token)
client = PrismClient(
proxy_endpoint="localhost:8980",
jwt_token=jwt_token # From reservation
)
# Create KeyValue pattern in namespace
client.create_pattern(
namespace="orders-prod",
pattern_type="keyvalue",
backend="redis-main",
config={
"cache_ttl": "5m",
"consistency": "strong"
}
)
Refresh Lease
# Refresh lease before expiration
client = PrismClient(proxy_endpoint="localhost:8980")
response = client.refresh_lease(
namespace="orders-prod",
jwt_token=jwt_token
)
# Update stored token (old token is now invalid)
new_jwt_token = response.jwt_token
new_expires_at = response.expires_at
with open(".prism-token", "w") as f:
f.write(new_jwt_token)
print(f"Lease refreshed, new expiration: {new_expires_at}")
Automatic Lease Refresh
# Client SDK with automatic refresh
from prism_client import PrismClient
import threading
import time
class LeaseRefresher:
def __init__(self, client, namespace, token, expires_at):
self.client = client
self.namespace = namespace
self.token = token
self.expires_at = expires_at
self.running = True
self.thread = threading.Thread(target=self._refresh_loop)
self.thread.daemon = True
self.thread.start()
def _refresh_loop(self):
while self.running:
# Calculate time until refresh (50% of TTL)
ttl = (self.expires_at - time.time())
refresh_in = ttl * 0.5
if refresh_in > 0:
time.sleep(refresh_in)
# Refresh lease
try:
response = self.client.refresh_lease(
namespace=self.namespace,
jwt_token=self.token
)
self.token = response.jwt_token
self.expires_at = response.expires_at.timestamp()
print(f"Lease refreshed for {self.namespace}")
except Exception as e:
print(f"Failed to refresh lease: {e}")
# Exponential backoff retry
time.sleep(60)
def stop(self):
self.running = False
self.thread.join()
# Usage
client = PrismClient(proxy_endpoint="localhost:8980")
response = client.reserve_namespace(name="orders-prod", principal="user@example.com")
refresher = LeaseRefresher(
client=client,
namespace="orders-prod",
token=response.jwt_token,
expires_at=response.expires_at.timestamp()
)
# Lease will be refreshed automatically in background
Security Considerations
JWT Token Security
Storage:
- Tokens should be stored securely (e.g., keychain, environment variables)
- Never commit tokens to version control
- Rotate tokens regularly by refreshing leases
Transmission:
- Always use TLS for gRPC connections
- Tokens sent in gRPC metadata:
authorization: Bearer <token> - Admin plane validates token signature on every request
Revocation:
- Tokens become invalid after expiration
- Refreshing lease invalidates old token
- Admin can revoke leases explicitly
- Lease table tracks active tokens for revocation
Authorization Model
Token Permissions:
namespace:configure: Modify namespace settingspattern:create: Create new patterns in namespacepattern:update: Update existing patternspattern:delete: Delete patternsbackend:bind: Bind backends to patterns
Enforcement:
- Proxy validates JWT signature using admin's public key
- Proxy checks
prism.namespaceclaim matches requested namespace - Proxy verifies
expclaim has not passed - Proxy checks
prism.permissionscontains required permission
Audit Logging
All namespace operations logged:
CREATE TABLE audit_log (
id UUID PRIMARY KEY,
timestamp TIMESTAMPTZ NOT NULL,
operation TEXT NOT NULL, -- ReserveNamespace, RefreshLease, etc.
actor TEXT NOT NULL, -- Principal from JWT
namespace TEXT, -- Affected namespace
result TEXT NOT NULL, -- success, error
error_message TEXT,
request_metadata JSONB,
INDEX idx_audit_timestamp ON audit_log(timestamp DESC),
INDEX idx_audit_namespace ON audit_log(namespace),
INDEX idx_audit_actor ON audit_log(actor)
);
Database Schema
-- Namespace registry
CREATE TABLE namespaces (
name TEXT PRIMARY KEY,
owner TEXT NOT NULL,
team TEXT,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL,
lease_expires TIMESTAMPTZ,
metadata JSONB,
status TEXT DEFAULT 'active'
);
CREATE INDEX idx_namespaces_lease_expires ON namespaces(lease_expires);
CREATE INDEX idx_namespaces_owner ON namespaces(owner);
-- Lease tracking
CREATE TABLE leases (
lease_id TEXT PRIMARY KEY,
namespace TEXT NOT NULL REFERENCES namespaces(name) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL,
last_refreshed_at TIMESTAMPTZ NOT NULL,
jwt_token TEXT NOT NULL,
refresh_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_leases_namespace ON leases(namespace);
CREATE INDEX idx_leases_expires_at ON leases(expires_at);
-- Audit log
CREATE TABLE audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
operation TEXT NOT NULL,
actor TEXT NOT NULL,
namespace TEXT,
result TEXT NOT NULL,
error_message TEXT,
details JSONB
);
CREATE INDEX idx_audit_timestamp ON audit_log(timestamp DESC);
CREATE INDEX idx_audit_namespace ON audit_log(namespace);
CREATE INDEX idx_audit_actor ON audit_log(actor);
Related Documents
- RFC-027: Namespace Configuration and Client Request Flow - Client namespace configuration
- ADR-006: Namespace and Multi-Tenancy - Namespace design and isolation
- ADR-055: Proxy-Admin Control Plane - Admin plane protocol
- RFC-011: Data Proxy Authentication - JWT and mTLS authentication
- RFC-048: Cross-Proxy Partition Strategies - Companion RFC for partition management
Revision History
- 2025-10-25: Initial draft - Cross-proxy namespace reservation with JWT-based lease management