use crate::auth::types::Permission;
use crate::error::{FusekiError, FusekiResult};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKey {
pub id: String,
pub name: String,
pub key_hash: String,
pub owner: String,
pub permissions: Vec<Permission>,
pub created_at: DateTime<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub last_used_at: Option<DateTime<Utc>>,
pub usage_count: u64,
pub is_active: bool,
pub metadata: ApiKeyMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKeyMetadata {
pub description: Option<String>,
pub allowed_ips: Vec<String>,
pub allowed_cidrs: Vec<String>,
pub rate_limit: Option<u32>,
pub tags: Vec<String>,
pub attributes: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct CreateApiKeyParams {
pub name: String,
pub owner: String,
pub permissions: Vec<Permission>,
pub expires_in: Option<Duration>,
pub metadata: ApiKeyMetadata,
}
#[derive(Debug, Clone)]
pub struct ApiKeyRotation {
pub new_key_id: String,
pub new_key: String,
pub old_key_id: String,
pub rotated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ApiKeyUsageStats {
pub key_id: String,
pub owner: String,
pub total_uses: u64,
pub last_used: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub age_days: i64,
pub uses_per_day: f64,
}
pub struct ApiKeyService {
keys: Arc<RwLock<HashMap<String, ApiKey>>>,
hash_to_id: Arc<RwLock<HashMap<String, String>>>,
usage_history: Arc<RwLock<HashMap<String, Vec<DateTime<Utc>>>>>,
key_prefix: String,
}
impl ApiKeyService {
pub fn new(key_prefix: impl Into<String>) -> Self {
ApiKeyService {
keys: Arc::new(RwLock::new(HashMap::new())),
hash_to_id: Arc::new(RwLock::new(HashMap::new())),
usage_history: Arc::new(RwLock::new(HashMap::new())),
key_prefix: key_prefix.into(),
}
}
pub async fn create_key(&self, params: CreateApiKeyParams) -> FusekiResult<(String, ApiKey)> {
let raw_key = self.generate_secure_key().await;
let key_hash = self.hash_key(&raw_key);
let key_id = uuid::Uuid::new_v4().to_string();
let expires_at = params.expires_in.map(|duration| Utc::now() + duration);
let api_key = ApiKey {
id: key_id.clone(),
name: params.name,
key_hash: key_hash.clone(),
owner: params.owner,
permissions: params.permissions,
created_at: Utc::now(),
expires_at,
last_used_at: None,
usage_count: 0,
is_active: true,
metadata: params.metadata,
};
let mut keys = self.keys.write().await;
let mut hash_map = self.hash_to_id.write().await;
keys.insert(key_id.clone(), api_key.clone());
hash_map.insert(key_hash, key_id.clone());
info!("Created API key: {} for owner: {}", key_id, api_key.owner);
Ok((raw_key, api_key))
}
async fn generate_secure_key(&self) -> String {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
let uuid1 = uuid::Uuid::new_v4();
let uuid2 = uuid::Uuid::new_v4();
let random_bytes: Vec<u8> = uuid1
.as_bytes()
.iter()
.chain(uuid2.as_bytes().iter())
.copied() .collect();
let random_part = URL_SAFE_NO_PAD.encode(&random_bytes[..32]);
format!("{}{}", self.key_prefix, random_part)
}
fn hash_key(&self, key: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(key.as_bytes());
format!("{:x}", hasher.finalize())
}
pub async fn validate_key(&self, raw_key: &str) -> FusekiResult<ApiKey> {
let key_hash = self.hash_key(raw_key);
let key_id = {
let hash_map = self.hash_to_id.read().await;
hash_map
.get(&key_hash)
.cloned()
.ok_or_else(|| FusekiError::authentication("Invalid API key"))?
};
let mut key = {
let keys = self.keys.read().await;
keys.get(&key_id)
.cloned()
.ok_or_else(|| FusekiError::authentication("API key not found"))?
};
if !key.is_active {
return Err(FusekiError::authentication("API key has been revoked"));
}
if let Some(expires_at) = key.expires_at {
if Utc::now() > expires_at {
self.revoke_key(&key_id).await?;
return Err(FusekiError::authentication("API key has expired"));
}
}
key.last_used_at = Some(Utc::now());
key.usage_count += 1;
{
let mut keys = self.keys.write().await;
if let Some(stored_key) = keys.get_mut(&key_id) {
stored_key.last_used_at = key.last_used_at;
stored_key.usage_count = key.usage_count;
}
}
{
let mut history = self.usage_history.write().await;
history
.entry(key_id.clone())
.or_insert_with(Vec::new)
.push(Utc::now());
}
debug!("API key validated: {}", key_id);
Ok(key)
}
pub async fn rotate_key(&self, key_id: &str) -> FusekiResult<ApiKeyRotation> {
let old_key = {
let keys = self.keys.read().await;
keys.get(key_id)
.cloned()
.ok_or_else(|| FusekiError::not_found("API key not found"))?
};
let (new_raw_key, new_key) = self
.create_key(CreateApiKeyParams {
name: format!("{} (rotated)", old_key.name),
owner: old_key.owner.clone(),
permissions: old_key.permissions.clone(),
expires_in: old_key.expires_at.map(|exp| exp - Utc::now()),
metadata: old_key.metadata.clone(),
})
.await?;
self.revoke_key(key_id).await?;
info!("Rotated API key: {} -> {}", key_id, new_key.id);
Ok(ApiKeyRotation {
new_key_id: new_key.id,
new_key: new_raw_key,
old_key_id: key_id.to_string(),
rotated_at: Utc::now(),
})
}
pub async fn revoke_key(&self, key_id: &str) -> FusekiResult<()> {
let mut keys = self.keys.write().await;
let key = keys
.get_mut(key_id)
.ok_or_else(|| FusekiError::not_found("API key not found"))?;
key.is_active = false;
info!("Revoked API key: {}", key_id);
Ok(())
}
pub async fn delete_key(&self, key_id: &str) -> FusekiResult<()> {
let mut keys = self.keys.write().await;
let mut hash_map = self.hash_to_id.write().await;
let key = keys
.remove(key_id)
.ok_or_else(|| FusekiError::not_found("API key not found"))?;
hash_map.remove(&key.key_hash);
{
let mut history = self.usage_history.write().await;
history.remove(key_id);
}
info!("Deleted API key: {}", key_id);
Ok(())
}
pub async fn get_keys_for_owner(&self, owner: &str) -> Vec<ApiKey> {
let keys = self.keys.read().await;
keys.values()
.filter(|key| key.owner == owner)
.cloned()
.collect()
}
pub async fn get_key(&self, key_id: &str) -> Option<ApiKey> {
let keys = self.keys.read().await;
keys.get(key_id).cloned()
}
pub async fn get_usage_stats(&self, key_id: &str) -> FusekiResult<ApiKeyUsageStats> {
let key = {
let keys = self.keys.read().await;
keys.get(key_id)
.cloned()
.ok_or_else(|| FusekiError::not_found("API key not found"))?
};
let age_days = (Utc::now() - key.created_at).num_days();
let uses_per_day = if age_days > 0 {
key.usage_count as f64 / age_days as f64
} else {
key.usage_count as f64
};
Ok(ApiKeyUsageStats {
key_id: key.id,
owner: key.owner,
total_uses: key.usage_count,
last_used: key.last_used_at,
created_at: key.created_at,
age_days,
uses_per_day,
})
}
pub async fn cleanup_expired_keys(&self) -> usize {
let mut keys = self.keys.write().await;
let now = Utc::now();
let expired_keys: Vec<String> = keys
.iter()
.filter_map(|(id, key)| {
if let Some(expires_at) = key.expires_at {
if now > expires_at && key.is_active {
Some(id.clone())
} else {
None
}
} else {
None
}
})
.collect();
let count = expired_keys.len();
for key_id in expired_keys {
if let Some(key) = keys.get_mut(&key_id) {
key.is_active = false;
}
}
if count > 0 {
info!("Auto-revoked {} expired API keys", count);
}
count
}
pub fn start_cleanup_task(self: Arc<Self>) {
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
loop {
interval.tick().await;
self.cleanup_expired_keys().await;
}
});
}
pub async fn get_key_count(&self) -> usize {
let keys = self.keys.read().await;
keys.len()
}
pub async fn get_active_key_count(&self) -> usize {
let keys = self.keys.read().await;
keys.values().filter(|k| k.is_active).count()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_params() -> CreateApiKeyParams {
CreateApiKeyParams {
name: "Test Key".to_string(),
owner: "test_user".to_string(),
permissions: vec![Permission::GlobalRead, Permission::SparqlQuery],
expires_in: Some(Duration::days(30)),
metadata: ApiKeyMetadata {
description: Some("Test API key".to_string()),
allowed_ips: vec![],
allowed_cidrs: vec![],
rate_limit: None,
tags: vec![],
attributes: HashMap::new(),
},
}
}
#[tokio::test]
async fn test_create_key() {
let service = ApiKeyService::new("oxirs_");
let (raw_key, api_key) = service.create_key(create_test_params()).await.unwrap();
assert!(raw_key.starts_with("oxirs_"));
assert_eq!(api_key.owner, "test_user");
assert!(api_key.is_active);
}
#[tokio::test]
async fn test_validate_key() {
let service = ApiKeyService::new("oxirs_");
let (raw_key, _) = service.create_key(create_test_params()).await.unwrap();
let result = service.validate_key(&raw_key).await;
assert!(result.is_ok());
let result = service.validate_key("invalid_key").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_revoke_key() {
let service = ApiKeyService::new("oxirs_");
let (raw_key, api_key) = service.create_key(create_test_params()).await.unwrap();
service.revoke_key(&api_key.id).await.unwrap();
let result = service.validate_key(&raw_key).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_rotate_key() {
let service = ApiKeyService::new("oxirs_");
let (old_raw_key, old_key) = service.create_key(create_test_params()).await.unwrap();
let rotation = service.rotate_key(&old_key.id).await.unwrap();
let result = service.validate_key(&old_raw_key).await;
assert!(result.is_err());
let result = service.validate_key(&rotation.new_key).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_usage_tracking() {
let service = ApiKeyService::new("oxirs_");
let (raw_key, api_key) = service.create_key(create_test_params()).await.unwrap();
for _ in 0..5 {
service.validate_key(&raw_key).await.unwrap();
}
let stats = service.get_usage_stats(&api_key.id).await.unwrap();
assert_eq!(stats.total_uses, 5);
}
#[tokio::test]
async fn test_expired_key() {
let service = ApiKeyService::new("oxirs_");
let mut params = create_test_params();
params.expires_in = Some(Duration::seconds(-1));
let (raw_key, _) = service.create_key(params).await.unwrap();
let result = service.validate_key(&raw_key).await;
assert!(result.is_err());
}
}