chore: sync repository to current working state
Some checks failed
Build and Test / Build Server (Linux) (push) Has been cancelled
Build and Test / Build Agent (Windows) (push) Has been cancelled
Build and Test / Security Audit (push) Has been cancelled
Build and Test / Build Summary (push) Has been cancelled
Run Tests / Test Server (push) Has been cancelled
Run Tests / Test Agent (push) Has been cancelled
Run Tests / Code Coverage (push) Has been cancelled
Run Tests / Lint and Format Check (push) Has been cancelled

Brings azcomputerguru/guru-connect up to the authoritative working copy that
had been maintained in the claudetools monorepo: Phase 1 security and
infrastructure (middleware, metrics, utils, token blacklist, deployment
scripts, security audits) plus the native-remote-control integration spec.
Preserves the repo .gitignore, .cargo, and server/static/downloads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 06:15:29 -07:00
parent 5b7cf5fb07
commit e3e95f8fa7
73 changed files with 15608 additions and 5757 deletions

View File

@@ -0,0 +1,164 @@
//! Token blacklist for JWT revocation
//!
//! Provides in-memory token blacklist for immediate revocation of JWTs.
//! Tokens are automatically cleaned up after expiration.
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{info, debug};
/// Token blacklist for revocation
///
/// Maintains a set of revoked token signatures. When a token is revoked
/// (e.g., on logout or admin action), it's added to this blacklist and
/// all subsequent validation attempts will fail.
#[derive(Clone)]
pub struct TokenBlacklist {
/// Set of revoked token strings
tokens: Arc<RwLock<HashSet<String>>>,
}
impl TokenBlacklist {
/// Create a new empty blacklist
pub fn new() -> Self {
Self {
tokens: Arc::new(RwLock::new(HashSet::new())),
}
}
/// Add a token to the blacklist (revoke it)
///
/// # Arguments
/// * `token` - The full JWT token string to revoke
///
/// # Example
/// ```rust
/// blacklist.revoke("eyJ...").await;
/// ```
pub async fn revoke(&self, token: &str) {
let mut tokens = self.tokens.write().await;
let was_new = tokens.insert(token.to_string());
if was_new {
debug!("Token revoked and added to blacklist (length: {})", token.len());
}
}
/// Check if a token has been revoked
///
/// # Arguments
/// * `token` - The JWT token string to check
///
/// # Returns
/// `true` if the token is in the blacklist (revoked), `false` otherwise
pub async fn is_revoked(&self, token: &str) -> bool {
let tokens = self.tokens.read().await;
tokens.contains(token)
}
/// Get the number of tokens currently in the blacklist
pub async fn len(&self) -> usize {
let tokens = self.tokens.read().await;
tokens.len()
}
/// Check if the blacklist is empty
pub async fn is_empty(&self) -> bool {
let tokens = self.tokens.read().await;
tokens.is_empty()
}
/// Remove expired tokens from blacklist (cleanup)
///
/// This should be called periodically to prevent memory buildup.
/// Tokens that can no longer be validated (expired) are removed.
///
/// # Arguments
/// * `jwt_config` - JWT configuration for validating token expiration
///
/// # Returns
/// Number of tokens removed from blacklist
pub async fn cleanup_expired(&self, jwt_config: &super::JwtConfig) -> usize {
let mut tokens = self.tokens.write().await;
let original_len = tokens.len();
// Remove tokens that fail validation (expired)
tokens.retain(|token| {
// If token is expired (validation fails), remove it from blacklist
jwt_config.validate_token(token).is_ok()
});
let removed = original_len - tokens.len();
if removed > 0 {
info!("Cleaned {} expired tokens from blacklist ({} remaining)", removed, tokens.len());
}
removed
}
/// Clear all tokens from the blacklist
///
/// WARNING: This removes all revoked tokens. Use with caution.
pub async fn clear(&self) {
let mut tokens = self.tokens.write().await;
let count = tokens.len();
tokens.clear();
info!("Cleared {} tokens from blacklist", count);
}
}
impl Default for TokenBlacklist {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_revoke_and_check() {
let blacklist = TokenBlacklist::new();
let token = "test.token.here";
assert!(!blacklist.is_revoked(token).await);
blacklist.revoke(token).await;
assert!(blacklist.is_revoked(token).await);
assert_eq!(blacklist.len().await, 1);
}
#[tokio::test]
async fn test_multiple_revocations() {
let blacklist = TokenBlacklist::new();
blacklist.revoke("token1").await;
blacklist.revoke("token2").await;
blacklist.revoke("token3").await;
assert_eq!(blacklist.len().await, 3);
assert!(blacklist.is_revoked("token1").await);
assert!(blacklist.is_revoked("token2").await);
assert!(blacklist.is_revoked("token3").await);
assert!(!blacklist.is_revoked("token4").await);
}
#[tokio::test]
async fn test_clear() {
let blacklist = TokenBlacklist::new();
blacklist.revoke("token1").await;
blacklist.revoke("token2").await;
assert_eq!(blacklist.len().await, 2);
blacklist.clear().await;
assert_eq!(blacklist.len().await, 0);
assert!(blacklist.is_empty().await);
}
}