1use chrono::{DateTime, Utc};
4use perfgate_error::AuthError;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8pub const API_KEY_PREFIX_LIVE: &str = "pg_live_";
10
11pub const API_KEY_PREFIX_TEST: &str = "pg_test_";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
16#[serde(rename_all = "snake_case")]
17pub enum Scope {
18 Read,
20 Write,
22 Promote,
24 Delete,
26 Admin,
28}
29
30impl std::fmt::Display for Scope {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 Scope::Read => write!(f, "read"),
34 Scope::Write => write!(f, "write"),
35 Scope::Promote => write!(f, "promote"),
36 Scope::Delete => write!(f, "delete"),
37 Scope::Admin => write!(f, "admin"),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "snake_case")]
45pub enum Role {
46 Viewer,
48 Contributor,
50 Promoter,
52 Admin,
54}
55
56impl Role {
57 pub fn allowed_scopes(&self) -> Vec<Scope> {
59 match self {
60 Role::Viewer => vec![Scope::Read],
61 Role::Contributor => vec![Scope::Read, Scope::Write],
62 Role::Promoter => vec![Scope::Read, Scope::Write, Scope::Promote],
63 Role::Admin => vec![
64 Scope::Read,
65 Scope::Write,
66 Scope::Promote,
67 Scope::Delete,
68 Scope::Admin,
69 ],
70 }
71 }
72
73 pub fn has_scope(&self, scope: Scope) -> bool {
75 self.allowed_scopes().contains(&scope)
76 }
77
78 pub fn from_scopes(scopes: &[Scope]) -> Self {
80 if scopes.contains(&Scope::Admin) || scopes.contains(&Scope::Delete) {
81 Self::Admin
82 } else if scopes.contains(&Scope::Promote) {
83 Self::Promoter
84 } else if scopes.contains(&Scope::Write) {
85 Self::Contributor
86 } else {
87 Self::Viewer
88 }
89 }
90}
91
92impl std::fmt::Display for Role {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 Role::Viewer => write!(f, "viewer"),
96 Role::Contributor => write!(f, "contributor"),
97 Role::Promoter => write!(f, "promoter"),
98 Role::Admin => write!(f, "admin"),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
105pub struct ApiKey {
106 pub id: String,
108
109 pub name: String,
111
112 pub project_id: String,
114
115 pub scopes: Vec<Scope>,
117
118 pub role: Role,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub benchmark_regex: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub expires_at: Option<DateTime<Utc>>,
128
129 pub created_at: DateTime<Utc>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub last_used_at: Option<DateTime<Utc>>,
135}
136
137impl ApiKey {
138 pub fn new(id: String, name: String, project_id: String, role: Role) -> Self {
140 Self {
141 id,
142 name,
143 project_id,
144 scopes: role.allowed_scopes(),
145 role,
146 benchmark_regex: None,
147 expires_at: None,
148 created_at: Utc::now(),
149 last_used_at: None,
150 }
151 }
152
153 pub fn is_expired(&self) -> bool {
155 if let Some(exp) = self.expires_at {
156 return exp < Utc::now();
157 }
158 false
159 }
160
161 pub fn has_scope(&self, scope: Scope) -> bool {
163 self.scopes.contains(&scope)
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
169pub struct JwtClaims {
170 pub sub: String,
172
173 pub project_id: String,
175
176 pub scopes: Vec<Scope>,
178
179 pub exp: u64,
181
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub iat: Option<u64>,
185
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub iss: Option<String>,
189
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub aud: Option<String>,
193}
194
195pub fn validate_key_format(key: &str) -> Result<(), AuthError> {
197 if key.starts_with(API_KEY_PREFIX_LIVE) || key.starts_with(API_KEY_PREFIX_TEST) {
198 let remainder = key
199 .strip_prefix(API_KEY_PREFIX_LIVE)
200 .or_else(|| key.strip_prefix(API_KEY_PREFIX_TEST))
201 .unwrap();
202
203 if remainder.len() >= 32 && remainder.chars().all(|c| c.is_alphanumeric()) {
205 return Ok(());
206 }
207 }
208
209 Err(AuthError::InvalidKeyFormat)
210}
211
212pub fn generate_api_key(test: bool) -> String {
214 let prefix = if test {
215 API_KEY_PREFIX_TEST
216 } else {
217 API_KEY_PREFIX_LIVE
218 };
219 let random: String = uuid::Uuid::new_v4()
220 .simple()
221 .to_string()
222 .chars()
223 .take(32)
224 .collect();
225 format!("{}{}", prefix, random)
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_validate_key_format() {
234 assert!(validate_key_format("pg_live_abcdefghijklmnopqrstuvwxyz123456").is_ok());
235 assert!(validate_key_format("pg_test_abcdefghijklmnopqrstuvwxyz123456").is_ok());
236 assert!(validate_key_format("invalid_abcdefghijklmnopqrstuvwxyz123456").is_err());
237 assert!(validate_key_format("pg_live_short").is_err());
238 assert!(validate_key_format("pg_live_abcdefghijklmnopqrstuvwxyz12345!@").is_err());
239 }
240
241 #[test]
242 fn test_role_scopes() {
243 let viewer = Role::Viewer;
244 assert!(viewer.has_scope(Scope::Read));
245 assert!(!viewer.has_scope(Scope::Write));
246
247 let contributor = Role::Contributor;
248 assert!(contributor.has_scope(Scope::Read));
249 assert!(contributor.has_scope(Scope::Write));
250 assert!(!contributor.has_scope(Scope::Promote));
251
252 let promoter = Role::Promoter;
253 assert!(promoter.has_scope(Scope::Promote));
254 assert!(!promoter.has_scope(Scope::Delete));
255
256 let admin = Role::Admin;
257 assert!(admin.has_scope(Scope::Delete));
258 assert!(admin.has_scope(Scope::Admin));
259 }
260
261 #[test]
262 fn test_generate_api_key() {
263 let live_key = generate_api_key(false);
264 assert!(live_key.starts_with(API_KEY_PREFIX_LIVE));
265 assert!(live_key.len() >= 40);
266
267 let test_key = generate_api_key(true);
268 assert!(test_key.starts_with(API_KEY_PREFIX_TEST));
269 assert!(test_key.len() >= 40);
270 }
271}