use chrono::{DateTime, Utc};
use perfgate_error::AuthError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub const API_KEY_PREFIX_LIVE: &str = "pg_live_";
pub const API_KEY_PREFIX_TEST: &str = "pg_test_";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
Read,
Write,
Promote,
Delete,
Admin,
}
impl std::fmt::Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scope::Read => write!(f, "read"),
Scope::Write => write!(f, "write"),
Scope::Promote => write!(f, "promote"),
Scope::Delete => write!(f, "delete"),
Scope::Admin => write!(f, "admin"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Role {
Viewer,
Contributor,
Promoter,
Admin,
}
impl Role {
pub fn allowed_scopes(&self) -> Vec<Scope> {
match self {
Role::Viewer => vec![Scope::Read],
Role::Contributor => vec![Scope::Read, Scope::Write],
Role::Promoter => vec![Scope::Read, Scope::Write, Scope::Promote],
Role::Admin => vec![
Scope::Read,
Scope::Write,
Scope::Promote,
Scope::Delete,
Scope::Admin,
],
}
}
pub fn has_scope(&self, scope: Scope) -> bool {
self.allowed_scopes().contains(&scope)
}
pub fn from_scopes(scopes: &[Scope]) -> Self {
if scopes.contains(&Scope::Admin) || scopes.contains(&Scope::Delete) {
Self::Admin
} else if scopes.contains(&Scope::Promote) {
Self::Promoter
} else if scopes.contains(&Scope::Write) {
Self::Contributor
} else {
Self::Viewer
}
}
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::Viewer => write!(f, "viewer"),
Role::Contributor => write!(f, "contributor"),
Role::Promoter => write!(f, "promoter"),
Role::Admin => write!(f, "admin"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ApiKey {
pub id: String,
pub name: String,
pub project_id: String,
pub scopes: Vec<Scope>,
pub role: Role,
#[serde(skip_serializing_if = "Option::is_none")]
pub benchmark_regex: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_used_at: Option<DateTime<Utc>>,
}
impl ApiKey {
pub fn new(id: String, name: String, project_id: String, role: Role) -> Self {
Self {
id,
name,
project_id,
scopes: role.allowed_scopes(),
role,
benchmark_regex: None,
expires_at: None,
created_at: Utc::now(),
last_used_at: None,
}
}
pub fn is_expired(&self) -> bool {
if let Some(exp) = self.expires_at {
return exp < Utc::now();
}
false
}
pub fn has_scope(&self, scope: Scope) -> bool {
self.scopes.contains(&scope)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct JwtClaims {
pub sub: String,
pub project_id: String,
pub scopes: Vec<Scope>,
pub exp: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iat: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
}
pub fn validate_key_format(key: &str) -> Result<(), AuthError> {
if key.starts_with(API_KEY_PREFIX_LIVE) || key.starts_with(API_KEY_PREFIX_TEST) {
let remainder = key
.strip_prefix(API_KEY_PREFIX_LIVE)
.or_else(|| key.strip_prefix(API_KEY_PREFIX_TEST))
.unwrap();
if remainder.len() >= 32 && remainder.chars().all(|c| c.is_alphanumeric()) {
return Ok(());
}
}
Err(AuthError::InvalidKeyFormat)
}
pub fn generate_api_key(test: bool) -> String {
let prefix = if test {
API_KEY_PREFIX_TEST
} else {
API_KEY_PREFIX_LIVE
};
let random: String = uuid::Uuid::new_v4()
.simple()
.to_string()
.chars()
.take(32)
.collect();
format!("{}{}", prefix, random)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_key_format() {
assert!(validate_key_format("pg_live_abcdefghijklmnopqrstuvwxyz123456").is_ok());
assert!(validate_key_format("pg_test_abcdefghijklmnopqrstuvwxyz123456").is_ok());
assert!(validate_key_format("invalid_abcdefghijklmnopqrstuvwxyz123456").is_err());
assert!(validate_key_format("pg_live_short").is_err());
assert!(validate_key_format("pg_live_abcdefghijklmnopqrstuvwxyz12345!@").is_err());
}
#[test]
fn test_role_scopes() {
let viewer = Role::Viewer;
assert!(viewer.has_scope(Scope::Read));
assert!(!viewer.has_scope(Scope::Write));
let contributor = Role::Contributor;
assert!(contributor.has_scope(Scope::Read));
assert!(contributor.has_scope(Scope::Write));
assert!(!contributor.has_scope(Scope::Promote));
let promoter = Role::Promoter;
assert!(promoter.has_scope(Scope::Promote));
assert!(!promoter.has_scope(Scope::Delete));
let admin = Role::Admin;
assert!(admin.has_scope(Scope::Delete));
assert!(admin.has_scope(Scope::Admin));
}
#[test]
fn test_generate_api_key() {
let live_key = generate_api_key(false);
assert!(live_key.starts_with(API_KEY_PREFIX_LIVE));
assert!(live_key.len() >= 40);
let test_key = generate_api_key(true);
assert!(test_key.starts_with(API_KEY_PREFIX_TEST));
assert!(test_key.len() >= 40);
}
}