Skip to main content

postrust_proxy/saas/
api_keys.rs

1//! API key generation and validation service.
2
3use crate::error::{ProxyError, ProxyResult};
4use crate::saas::db;
5use crate::saas::types::{ApiKey, CreateApiKeyRequest};
6use rand::Rng;
7use sha2::{Digest, Sha256};
8use sqlx::PgPool;
9use uuid::Uuid;
10
11/// API key service for generating and validating API keys.
12pub struct ApiKeyService {
13    pool: PgPool,
14}
15
16impl ApiKeyService {
17    /// Create a new API key service.
18    pub fn new(pool: PgPool) -> Self {
19        Self { pool }
20    }
21
22    /// Generate a new API key for a tenant.
23    ///
24    /// Returns the API key with the raw key value (only available at creation time).
25    pub async fn create_api_key(
26        &self,
27        tenant_id: Uuid,
28        req: CreateApiKeyRequest,
29    ) -> ProxyResult<ApiKey> {
30        // Generate a secure random key
31        let raw_key = generate_api_key();
32        let key_hash = hash_api_key(&raw_key);
33        let key_prefix = &raw_key[..8];
34
35        // Store in database
36        let row = db::create_api_key(&self.pool, tenant_id, req, &key_hash, key_prefix).await?;
37
38        // Return with the raw key (only time it's available)
39        Ok(ApiKey {
40            id: row.id,
41            tenant_id: row.tenant_id,
42            name: row.name,
43            key: Some(raw_key), // Only returned on creation
44            key_prefix: row.key_prefix,
45            scopes: row.scopes,
46            last_used_at: row.last_used_at,
47            expires_at: row.expires_at,
48            enabled: row.enabled,
49            created_at: row.created_at,
50        })
51    }
52
53    /// Validate an API key and return the tenant ID and scopes.
54    pub async fn validate_api_key(&self, raw_key: &str) -> ProxyResult<ValidatedApiKey> {
55        let key_hash = hash_api_key(raw_key);
56
57        let validation = db::validate_api_key_by_hash(&self.pool, &key_hash)
58            .await?
59            .ok_or_else(|| ProxyError::Auth("Invalid or expired API key".into()))?;
60
61        if !validation.enabled {
62            return Err(ProxyError::Auth("API key is disabled".into()));
63        }
64
65        // Update last used timestamp asynchronously
66        let pool = self.pool.clone();
67        let key_id = validation.id;
68        tokio::spawn(async move {
69            let _ = db::update_last_used(&pool, key_id).await;
70        });
71
72        Ok(ValidatedApiKey {
73            key_id: validation.id,
74            tenant_id: validation.tenant_id,
75            scopes: validation.scopes,
76        })
77    }
78
79    /// List API keys for a tenant.
80    pub async fn list_api_keys(&self, tenant_id: Uuid) -> ProxyResult<Vec<ApiKey>> {
81        db::list_api_keys(&self.pool, tenant_id).await
82    }
83
84    /// Get an API key by ID.
85    pub async fn get_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Option<ApiKey>> {
86        db::get_api_key_for_tenant(&self.pool, id, tenant_id).await
87    }
88
89    /// Revoke (delete) an API key.
90    pub async fn revoke_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
91        db::delete_api_key(&self.pool, id, tenant_id).await
92    }
93
94    /// Disable an API key without deleting it.
95    pub async fn disable_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
96        db::disable_api_key(&self.pool, id, tenant_id).await
97    }
98
99    /// Enable a disabled API key.
100    pub async fn enable_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
101        db::enable_api_key(&self.pool, id, tenant_id).await
102    }
103}
104
105/// Result of API key validation.
106#[derive(Debug, Clone)]
107pub struct ValidatedApiKey {
108    pub key_id: Uuid,
109    pub tenant_id: Uuid,
110    pub scopes: Vec<String>,
111}
112
113impl ValidatedApiKey {
114    /// Check if the key has a specific scope.
115    pub fn has_scope(&self, scope: &str) -> bool {
116        self.scopes.iter().any(|s| s == scope || s == "*")
117    }
118
119    /// Check if the key has read access for a resource.
120    pub fn can_read(&self, resource: &str) -> bool {
121        self.has_scope(&format!("{}:read", resource))
122            || self.has_scope(&format!("{}:write", resource))
123            || self.has_scope("*")
124    }
125
126    /// Check if the key has write access for a resource.
127    pub fn can_write(&self, resource: &str) -> bool {
128        self.has_scope(&format!("{}:write", resource)) || self.has_scope("*")
129    }
130}
131
132/// Generate a secure random API key.
133///
134/// Format: `pr_live_` + 32 random alphanumeric characters
135fn generate_api_key() -> String {
136    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
137    let mut rng = rand::rng();
138
139    let random_part: String = (0..32)
140        .map(|_| {
141            let idx = rng.random_range(0..CHARSET.len());
142            CHARSET[idx] as char
143        })
144        .collect();
145
146    format!("pr_live_{}", random_part)
147}
148
149/// Hash an API key using SHA-256.
150pub fn hash_api_key(key: &str) -> String {
151    let mut hasher = Sha256::new();
152    hasher.update(key.as_bytes());
153    hex::encode(hasher.finalize())
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_generate_api_key() {
162        let key = generate_api_key();
163        assert!(key.starts_with("pr_live_"));
164        assert_eq!(key.len(), 8 + 32); // prefix + random
165    }
166
167    #[test]
168    fn test_hash_api_key() {
169        let key = "pr_live_test123456789012345678901234";
170        let hash = hash_api_key(key);
171        assert_eq!(hash.len(), 64); // SHA-256 produces 32 bytes = 64 hex chars
172
173        // Same input should produce same hash
174        let hash2 = hash_api_key(key);
175        assert_eq!(hash, hash2);
176
177        // Different input should produce different hash
178        let hash3 = hash_api_key("pr_live_different12345678901234567");
179        assert_ne!(hash, hash3);
180    }
181
182    #[test]
183    fn test_validated_api_key_scopes() {
184        let validated = ValidatedApiKey {
185            key_id: Uuid::new_v4(),
186            tenant_id: Uuid::new_v4(),
187            scopes: vec!["domains:read".to_string(), "domains:write".to_string()],
188        };
189
190        assert!(validated.has_scope("domains:read"));
191        assert!(validated.has_scope("domains:write"));
192        assert!(!validated.has_scope("upstreams:write"));
193
194        assert!(validated.can_read("domains"));
195        assert!(validated.can_write("domains"));
196        assert!(!validated.can_read("upstreams"));
197        assert!(!validated.can_write("upstreams"));
198    }
199
200    #[test]
201    fn test_wildcard_scope() {
202        let validated = ValidatedApiKey {
203            key_id: Uuid::new_v4(),
204            tenant_id: Uuid::new_v4(),
205            scopes: vec!["*".to_string()],
206        };
207
208        assert!(validated.can_read("domains"));
209        assert!(validated.can_write("domains"));
210        assert!(validated.can_read("upstreams"));
211        assert!(validated.can_write("upstreams"));
212    }
213}