use std::collections::HashMap;
use std::sync::RwLock;
use std::time::SystemTime;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Role {
Reader,
Writer,
Admin,
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::Reader => write!(f, "reader"),
Role::Writer => write!(f, "writer"),
Role::Admin => write!(f, "admin"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKeyEntry {
pub key: String,
pub role: Role,
pub description: String,
pub active: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEntry {
pub timestamp: u64,
pub api_key_prefix: String,
pub role: Role,
pub operation: String,
pub allowed: bool,
}
pub struct AuthManager {
keys: RwLock<HashMap<String, ApiKeyEntry>>,
enabled: bool,
audit_log: RwLock<Vec<AuditEntry>>,
max_audit_entries: usize,
}
impl AuthManager {
pub fn disabled() -> Self {
Self {
keys: RwLock::new(HashMap::new()),
enabled: false,
audit_log: RwLock::new(Vec::new()),
max_audit_entries: 10000,
}
}
pub fn new(keys: Vec<ApiKeyEntry>) -> Self {
let key_map: HashMap<String, ApiKeyEntry> =
keys.into_iter().map(|k| (k.key.clone(), k)).collect();
Self {
keys: RwLock::new(key_map),
enabled: true,
audit_log: RwLock::new(Vec::new()),
max_audit_entries: 10000,
}
}
pub fn is_enabled(&self) -> bool {
self.enabled
}
pub fn authenticate(&self, api_key: &str) -> Option<Role> {
if !self.enabled {
return Some(Role::Admin); }
let keys = self.keys.read().unwrap();
keys.get(api_key)
.filter(|entry| entry.active)
.map(|entry| entry.role)
}
pub fn authorize(role: Role, operation: &str) -> bool {
match role {
Role::Admin => true,
Role::Writer => !Self::is_admin_operation(operation),
Role::Reader => Self::is_read_operation(operation),
}
}
fn is_read_operation(operation: &str) -> bool {
matches!(
operation,
"GetNode"
| "GetEdge"
| "Neighbors"
| "NeighborsAt"
| "Bfs"
| "BfsAt"
| "ShortestPath"
| "ShortestPathAt"
| "VectorSearch"
| "HybridSearch"
| "SemanticNeighbors"
| "SemanticWalk"
| "Query"
| "ExtractSubgraph"
| "GraphRag"
| "Ping"
)
}
fn is_admin_operation(_operation: &str) -> bool {
false
}
pub fn audit(&self, api_key: &str, role: Role, operation: &str, allowed: bool) {
let entry = AuditEntry {
timestamp: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
api_key_prefix: if api_key.len() >= 8 {
format!("{}...", &api_key[..8])
} else {
api_key.to_string()
},
role,
operation: operation.to_string(),
allowed,
};
let mut log = self.audit_log.write().unwrap();
log.push(entry);
if log.len() > self.max_audit_entries {
let drain_count = self.max_audit_entries / 10;
log.drain(..drain_count);
}
}
pub fn recent_audit(&self, count: usize) -> Vec<AuditEntry> {
let log = self.audit_log.read().unwrap();
log.iter().rev().take(count).cloned().collect()
}
pub fn add_key(&self, entry: ApiKeyEntry) {
let mut keys = self.keys.write().unwrap();
keys.insert(entry.key.clone(), entry);
}
pub fn revoke_key(&self, api_key: &str) -> bool {
let mut keys = self.keys.write().unwrap();
if let Some(entry) = keys.get_mut(api_key) {
entry.active = false;
true
} else {
false
}
}
pub fn operation_name(request_json: &str) -> &str {
if let Some(start) = request_json.find("\"type\":\"") {
let rest = &request_json[start + 8..];
if let Some(end) = rest.find('"') {
return &rest[..end];
}
}
"Unknown"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_keys() -> Vec<ApiKeyEntry> {
vec![
ApiKeyEntry {
key: "admin-key-12345678".into(),
role: Role::Admin,
description: "Admin key".into(),
active: true,
},
ApiKeyEntry {
key: "writer-key-12345678".into(),
role: Role::Writer,
description: "Writer key".into(),
active: true,
},
ApiKeyEntry {
key: "reader-key-12345678".into(),
role: Role::Reader,
description: "Reader key".into(),
active: true,
},
ApiKeyEntry {
key: "inactive-key-12345678".into(),
role: Role::Admin,
description: "Inactive key".into(),
active: false,
},
]
}
#[test]
fn disabled_auth_allows_all() {
let auth = AuthManager::disabled();
assert!(!auth.is_enabled());
assert_eq!(auth.authenticate("anything"), Some(Role::Admin));
}
#[test]
fn valid_key_returns_role() {
let auth = AuthManager::new(make_keys());
assert_eq!(auth.authenticate("admin-key-12345678"), Some(Role::Admin));
assert_eq!(auth.authenticate("writer-key-12345678"), Some(Role::Writer));
assert_eq!(auth.authenticate("reader-key-12345678"), Some(Role::Reader));
}
#[test]
fn invalid_key_returns_none() {
let auth = AuthManager::new(make_keys());
assert_eq!(auth.authenticate("bad-key"), None);
}
#[test]
fn inactive_key_returns_none() {
let auth = AuthManager::new(make_keys());
assert_eq!(auth.authenticate("inactive-key-12345678"), None);
}
#[test]
fn admin_can_do_everything() {
assert!(AuthManager::authorize(Role::Admin, "CreateNode"));
assert!(AuthManager::authorize(Role::Admin, "DeleteNode"));
assert!(AuthManager::authorize(Role::Admin, "GetNode"));
assert!(AuthManager::authorize(Role::Admin, "Ping"));
}
#[test]
fn writer_can_read_and_write() {
assert!(AuthManager::authorize(Role::Writer, "CreateNode"));
assert!(AuthManager::authorize(Role::Writer, "DeleteNode"));
assert!(AuthManager::authorize(Role::Writer, "GetNode"));
assert!(AuthManager::authorize(Role::Writer, "Query"));
}
#[test]
fn reader_cannot_write() {
assert!(!AuthManager::authorize(Role::Reader, "CreateNode"));
assert!(!AuthManager::authorize(Role::Reader, "DeleteNode"));
assert!(!AuthManager::authorize(Role::Reader, "UpdateNode"));
assert!(AuthManager::authorize(Role::Reader, "GetNode"));
assert!(AuthManager::authorize(Role::Reader, "Query"));
assert!(AuthManager::authorize(Role::Reader, "VectorSearch"));
assert!(AuthManager::authorize(Role::Reader, "Ping"));
}
#[test]
fn audit_log_records_entries() {
let auth = AuthManager::new(make_keys());
auth.audit("admin-key-12345678", Role::Admin, "CreateNode", true);
auth.audit("reader-key-12345678", Role::Reader, "GetNode", true);
auth.audit("reader-key-12345678", Role::Reader, "CreateNode", false);
let recent = auth.recent_audit(10);
assert_eq!(recent.len(), 3);
assert!(!recent[0].allowed); assert!(recent[1].allowed);
}
#[test]
fn revoke_key_prevents_auth() {
let auth = AuthManager::new(make_keys());
assert_eq!(auth.authenticate("writer-key-12345678"), Some(Role::Writer));
assert!(auth.revoke_key("writer-key-12345678"));
assert_eq!(auth.authenticate("writer-key-12345678"), None);
}
#[test]
fn add_key_works() {
let auth = AuthManager::new(vec![]);
assert_eq!(auth.authenticate("new-key"), None);
auth.add_key(ApiKeyEntry {
key: "new-key".into(),
role: Role::Writer,
description: "test".into(),
active: true,
});
assert_eq!(auth.authenticate("new-key"), Some(Role::Writer));
}
#[test]
fn role_ordering() {
assert!(Role::Reader < Role::Writer);
assert!(Role::Writer < Role::Admin);
}
}