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 let raw_key = generate_api_key();
32 let key_hash = hash_api_key(&raw_key);
33 let key_prefix = &raw_key[..8];
34
35 let row = db::create_api_key(&self.pool, tenant_id, req, &key_hash, key_prefix).await?;
37
38 Ok(ApiKey {
40 id: row.id,
41 tenant_id: row.tenant_id,
42 name: row.name,
43 key: Some(raw_key), 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 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 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 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 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 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 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 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#[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 pub fn has_scope(&self, scope: &str) -> bool {
116 self.scopes.iter().any(|s| s == scope || s == "*")
117 }
118
119 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 pub fn can_write(&self, resource: &str) -> bool {
128 self.has_scope(&format!("{}:write", resource)) || self.has_scope("*")
129 }
130}
131
132fn 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
149pub 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); }
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); let hash2 = hash_api_key(key);
175 assert_eq!(hash, hash2);
176
177 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}