1use chrono::{DateTime, Utc};
18use perfgate_error::AuthError;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21
22pub const API_KEY_PREFIX_LIVE: &str = "pg_live_";
24
25pub const API_KEY_PREFIX_TEST: &str = "pg_test_";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
30#[serde(rename_all = "snake_case")]
31pub enum Scope {
32 Read,
34 Write,
36 Promote,
38 Delete,
40 Admin,
42}
43
44impl std::fmt::Display for Scope {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Scope::Read => write!(f, "read"),
48 Scope::Write => write!(f, "write"),
49 Scope::Promote => write!(f, "promote"),
50 Scope::Delete => write!(f, "delete"),
51 Scope::Admin => write!(f, "admin"),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
58#[serde(rename_all = "snake_case")]
59pub enum Role {
60 Viewer,
62 Contributor,
64 Promoter,
66 Admin,
68}
69
70impl Role {
71 pub fn allowed_scopes(&self) -> Vec<Scope> {
73 match self {
74 Role::Viewer => vec![Scope::Read],
75 Role::Contributor => vec![Scope::Read, Scope::Write],
76 Role::Promoter => vec![Scope::Read, Scope::Write, Scope::Promote],
77 Role::Admin => vec![
78 Scope::Read,
79 Scope::Write,
80 Scope::Promote,
81 Scope::Delete,
82 Scope::Admin,
83 ],
84 }
85 }
86
87 pub fn has_scope(&self, scope: Scope) -> bool {
89 self.allowed_scopes().contains(&scope)
90 }
91
92 pub fn from_scopes(scopes: &[Scope]) -> Self {
94 if scopes.contains(&Scope::Admin) || scopes.contains(&Scope::Delete) {
95 Self::Admin
96 } else if scopes.contains(&Scope::Promote) {
97 Self::Promoter
98 } else if scopes.contains(&Scope::Write) {
99 Self::Contributor
100 } else {
101 Self::Viewer
102 }
103 }
104}
105
106impl std::fmt::Display for Role {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Role::Viewer => write!(f, "viewer"),
110 Role::Contributor => write!(f, "contributor"),
111 Role::Promoter => write!(f, "promoter"),
112 Role::Admin => write!(f, "admin"),
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
119pub struct ApiKey {
120 pub id: String,
122
123 pub name: String,
125
126 pub project_id: String,
128
129 pub scopes: Vec<Scope>,
131
132 pub role: Role,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub benchmark_regex: Option<String>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub expires_at: Option<DateTime<Utc>>,
142
143 pub created_at: DateTime<Utc>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub last_used_at: Option<DateTime<Utc>>,
149}
150
151impl ApiKey {
152 pub fn new(id: String, name: String, project_id: String, role: Role) -> Self {
154 Self {
155 id,
156 name,
157 project_id,
158 scopes: role.allowed_scopes(),
159 role,
160 benchmark_regex: None,
161 expires_at: None,
162 created_at: Utc::now(),
163 last_used_at: None,
164 }
165 }
166
167 pub fn is_expired(&self) -> bool {
169 if let Some(exp) = self.expires_at {
170 return exp < Utc::now();
171 }
172 false
173 }
174
175 pub fn has_scope(&self, scope: Scope) -> bool {
177 self.scopes.contains(&scope)
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
183pub struct JwtClaims {
184 pub sub: String,
186
187 pub project_id: String,
189
190 pub scopes: Vec<Scope>,
192
193 pub exp: u64,
195
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub iat: Option<u64>,
199
200 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub iss: Option<String>,
203
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub aud: Option<String>,
207}
208
209pub fn validate_key_format(key: &str) -> Result<(), AuthError> {
211 if key.starts_with(API_KEY_PREFIX_LIVE) || key.starts_with(API_KEY_PREFIX_TEST) {
212 let remainder = key
213 .strip_prefix(API_KEY_PREFIX_LIVE)
214 .or_else(|| key.strip_prefix(API_KEY_PREFIX_TEST))
215 .unwrap();
216
217 if remainder.len() >= 32 && remainder.chars().all(|c| c.is_alphanumeric()) {
219 return Ok(());
220 }
221 }
222
223 Err(AuthError::InvalidKeyFormat)
224}
225
226pub fn generate_api_key(test: bool) -> String {
228 let prefix = if test {
229 API_KEY_PREFIX_TEST
230 } else {
231 API_KEY_PREFIX_LIVE
232 };
233 let random: String = uuid::Uuid::new_v4()
234 .simple()
235 .to_string()
236 .chars()
237 .take(32)
238 .collect();
239 format!("{}{}", prefix, random)
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_validate_key_format() {
248 assert!(validate_key_format("pg_live_abcdefghijklmnopqrstuvwxyz123456").is_ok());
249 assert!(validate_key_format("pg_test_abcdefghijklmnopqrstuvwxyz123456").is_ok());
250 assert!(validate_key_format("invalid_abcdefghijklmnopqrstuvwxyz123456").is_err());
251 assert!(validate_key_format("pg_live_short").is_err());
252 assert!(validate_key_format("pg_live_abcdefghijklmnopqrstuvwxyz12345!@").is_err());
253 }
254
255 #[test]
256 fn test_role_scopes() {
257 let viewer = Role::Viewer;
258 assert!(viewer.has_scope(Scope::Read));
259 assert!(!viewer.has_scope(Scope::Write));
260
261 let contributor = Role::Contributor;
262 assert!(contributor.has_scope(Scope::Read));
263 assert!(contributor.has_scope(Scope::Write));
264 assert!(!contributor.has_scope(Scope::Promote));
265
266 let promoter = Role::Promoter;
267 assert!(promoter.has_scope(Scope::Promote));
268 assert!(!promoter.has_scope(Scope::Delete));
269
270 let admin = Role::Admin;
271 assert!(admin.has_scope(Scope::Delete));
272 assert!(admin.has_scope(Scope::Admin));
273 }
274
275 #[test]
276 fn test_generate_api_key() {
277 let live_key = generate_api_key(false);
278 assert!(live_key.starts_with(API_KEY_PREFIX_LIVE));
279 assert!(live_key.len() >= 40);
280
281 let test_key = generate_api_key(true);
282 assert!(test_key.starts_with(API_KEY_PREFIX_TEST));
283 assert!(test_key.len() >= 40);
284 }
285}