postrust_proxy/saas/
api_keys.rs1use 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
11pub struct ApiKeyService {
13 pool: PgPool,
14}
15
16impl ApiKeyService {
17 pub fn new(pool: PgPool) -> Self {
19 Self { pool }
20 }
21
22 pub async fn create_api_key(
26 &self,
27 tenant_id: Uuid,
28 req: CreateApiKeyRequest,
29 ) -> ProxyResult<ApiKey> {
30 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 let raw_key = generate_api_key();
43 let key_hash = hash_api_key(&raw_key);
44 let key_prefix = &raw_key[..8];
45
46 let row = db::create_api_key(&self.pool, tenant_id, req, &key_hash, key_prefix).await?;
48
49 Ok(ApiKey {
51 id: row.id,
52 tenant_id: row.tenant_id,
53 name: row.name,
54 key: Some(raw_key), 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
118pub struct ValidatedApiKey {
119 pub key_id: Uuid,
121 pub tenant_id: Uuid,
123 pub scopes: Vec<String>,
125}
126
127impl ValidatedApiKey {
128 pub fn has_scope(&self, scope: &str) -> bool {
130 self.scopes.iter().any(|s| s == scope || s == "*")
131 }
132
133 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 pub fn can_write(&self, resource: &str) -> bool {
142 self.has_scope(&format!("{}:write", resource)) || self.has_scope("*")
143 }
144}
145
146fn 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
163pub 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); }
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); let hash2 = hash_api_key(key);
189 assert_eq!(hash, hash2);
190
191 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}