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        // Whether the caller may grant these scopes is the handler's call --
31        // it has the caller's credential. What can be checked without it is
32        // that each scope means something.
33        crate::saas::validation::validate_resource_name("API key", &req.name)?;
34        crate::saas::auth::validate_scopes(&req.scopes)?;
35        if req.expires_at.is_some_and(|at| at <= chrono::Utc::now()) {
36            return Err(ProxyError::Validation(
37                "expires_at must be in the future".into(),
38            ));
39        }
40
41        // Generate a secure random key
42        let raw_key = generate_api_key();
43        let key_hash = hash_api_key(&raw_key);
44        let key_prefix = &raw_key[..8];
45
46        // Store in database
47        let row = db::create_api_key(&self.pool, tenant_id, req, &key_hash, key_prefix).await?;
48
49        // Return with the raw key (only time it's available)
50        Ok(ApiKey {
51            id: row.id,
52            tenant_id: row.tenant_id,
53            name: row.name,
54            key: Some(raw_key), // Only returned on creation
55            key_prefix: row.key_prefix,
56            scopes: row.scopes,
57            last_used_at: row.last_used_at,
58            expires_at: row.expires_at,
59            enabled: row.enabled,
60            created_at: row.created_at,
61        })
62    }
63
64    /// Validate an API key and return the tenant ID and scopes.
65    pub async fn validate_api_key(&self, raw_key: &str) -> ProxyResult<ValidatedApiKey> {
66        let key_hash = hash_api_key(raw_key);
67
68        let validation = db::validate_api_key_by_hash(&self.pool, &key_hash)
69            .await?
70            .ok_or_else(|| ProxyError::Auth("Invalid or expired API key".into()))?;
71
72        if !validation.enabled {
73            return Err(ProxyError::Auth("API key is disabled".into()));
74        }
75
76        // Update last used timestamp asynchronously
77        let pool = self.pool.clone();
78        let key_id = validation.id;
79        tokio::spawn(async move {
80            let _ = db::update_last_used(&pool, key_id).await;
81        });
82
83        Ok(ValidatedApiKey {
84            key_id: validation.id,
85            tenant_id: validation.tenant_id,
86            scopes: validation.scopes,
87        })
88    }
89
90    /// List API keys for a tenant.
91    pub async fn list_api_keys(&self, tenant_id: Uuid) -> ProxyResult<Vec<ApiKey>> {
92        db::list_api_keys(&self.pool, tenant_id).await
93    }
94
95    /// Get an API key by ID.
96    pub async fn get_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<Option<ApiKey>> {
97        db::get_api_key_for_tenant(&self.pool, id, tenant_id).await
98    }
99
100    /// Revoke (delete) an API key.
101    pub async fn revoke_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
102        db::delete_api_key(&self.pool, id, tenant_id).await
103    }
104
105    /// Disable an API key without deleting it.
106    pub async fn disable_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
107        db::disable_api_key(&self.pool, id, tenant_id).await
108    }
109
110    /// Enable a disabled API key.
111    pub async fn enable_api_key(&self, id: Uuid, tenant_id: Uuid) -> ProxyResult<bool> {
112        db::enable_api_key(&self.pool, id, tenant_id).await
113    }
114}
115
116/// Result of API key validation.
117#[derive(Debug, Clone)]
118pub struct ValidatedApiKey {
119    /// Which key it was, for the audit log. Not the key itself.
120    pub key_id: Uuid,
121    /// The tenant the key authenticates.
122    pub tenant_id: Uuid,
123    /// What the key may do, as `resource:action` strings.
124    pub scopes: Vec<String>,
125}
126
127impl ValidatedApiKey {
128    /// Check if the key has a specific scope.
129    pub fn has_scope(&self, scope: &str) -> bool {
130        self.scopes.iter().any(|s| s == scope || s == "*")
131    }
132
133    /// Check if the key has read access for a resource.
134    pub fn can_read(&self, resource: &str) -> bool {
135        self.has_scope(&format!("{}:read", resource))
136            || self.has_scope(&format!("{}:write", resource))
137            || self.has_scope("*")
138    }
139
140    /// Check if the key has write access for a resource.
141    pub fn can_write(&self, resource: &str) -> bool {
142        self.has_scope(&format!("{}:write", resource)) || self.has_scope("*")
143    }
144}
145
146/// Generate a secure random API key.
147///
148/// Format: `pr_live_` + 32 random alphanumeric characters
149fn generate_api_key() -> String {
150    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
151    let mut rng = rand::rng();
152
153    let random_part: String = (0..32)
154        .map(|_| {
155            let idx = rng.random_range(0..CHARSET.len());
156            CHARSET[idx] as char
157        })
158        .collect();
159
160    format!("pr_live_{}", random_part)
161}
162
163/// Hash an API key using SHA-256.
164pub fn hash_api_key(key: &str) -> String {
165    let mut hasher = Sha256::new();
166    hasher.update(key.as_bytes());
167    hex::encode(hasher.finalize())
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_generate_api_key() {
176        let key = generate_api_key();
177        assert!(key.starts_with("pr_live_"));
178        assert_eq!(key.len(), 8 + 32); // prefix + random
179    }
180
181    #[test]
182    fn test_hash_api_key() {
183        let key = "pr_live_test123456789012345678901234";
184        let hash = hash_api_key(key);
185        assert_eq!(hash.len(), 64); // SHA-256 produces 32 bytes = 64 hex chars
186
187        // Same input should produce same hash
188        let hash2 = hash_api_key(key);
189        assert_eq!(hash, hash2);
190
191        // Different input should produce different hash
192        let hash3 = hash_api_key("pr_live_different12345678901234567");
193        assert_ne!(hash, hash3);
194    }
195
196    #[test]
197    fn test_validated_api_key_scopes() {
198        let validated = ValidatedApiKey {
199            key_id: Uuid::new_v4(),
200            tenant_id: Uuid::new_v4(),
201            scopes: vec!["domains:read".to_string(), "domains:write".to_string()],
202        };
203
204        assert!(validated.has_scope("domains:read"));
205        assert!(validated.has_scope("domains:write"));
206        assert!(!validated.has_scope("upstreams:write"));
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
214    #[test]
215    fn test_wildcard_scope() {
216        let validated = ValidatedApiKey {
217            key_id: Uuid::new_v4(),
218            tenant_id: Uuid::new_v4(),
219            scopes: vec!["*".to_string()],
220        };
221
222        assert!(validated.can_read("domains"));
223        assert!(validated.can_write("domains"));
224        assert!(validated.can_read("upstreams"));
225        assert!(validated.can_write("upstreams"));
226    }
227}