Files
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

548 lines
17 KiB
Rust

//! Security: JWT validation and IP whitelist checking.
//!
//! Matches Go's security/guard.go and security/jwt.go.
//! - Guard: combines whitelist IP checking with JWT token validation
//! - JWT: HS256 HMAC signing with file-id claims
pub mod tls;
use std::collections::HashSet;
use std::net::IpAddr;
use std::time::{SystemTime, UNIX_EPOCH};
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation, decode, encode};
use serde::{Deserialize, Serialize};
// ============================================================================
// JWT Claims
// ============================================================================
/// Claims for volume server file access tokens.
/// Matches Go's `SeaweedFileIdClaims`.
#[derive(Debug, Serialize, Deserialize)]
pub struct FileIdClaims {
/// File ID this token grants access to (e.g., "3,01637037d6").
#[serde(skip_serializing_if = "Option::is_none")]
pub fid: Option<String>,
/// Expiration time (Unix timestamp).
#[serde(skip_serializing_if = "Option::is_none")]
pub exp: Option<u64>,
/// Not before (Unix timestamp).
#[serde(skip_serializing_if = "Option::is_none")]
pub nbf: Option<u64>,
}
/// Signing key wrapper (empty = security disabled).
#[derive(Clone)]
pub struct SigningKey(pub Vec<u8>);
impl SigningKey {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn from_string(s: &str) -> Self {
SigningKey(s.as_bytes().to_vec())
}
}
/// Generate a JWT token for file access.
pub fn gen_jwt(
signing_key: &SigningKey,
expires_after_sec: i64,
file_id: &str,
) -> Result<String, JwtError> {
if signing_key.is_empty() {
return Err(JwtError::NoSigningKey);
}
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let claims = FileIdClaims {
fid: Some(file_id.to_string()),
exp: if expires_after_sec > 0 {
Some(now + expires_after_sec as u64)
} else {
None
},
nbf: None,
};
let token = encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret(&signing_key.0),
)?;
Ok(token)
}
/// Decode and validate a JWT token.
pub fn decode_jwt(signing_key: &SigningKey, token: &str) -> Result<FileIdClaims, JwtError> {
if signing_key.is_empty() {
return Err(JwtError::NoSigningKey);
}
let mut validation = Validation::new(Algorithm::HS256);
// Match Go behavior: tokens without exp are accepted (Go's jwt-go does not require exp)
// But if exp IS present, it must be valid (not expired).
validation.required_spec_claims.clear();
validation.validate_exp = true;
// Go's jwt-go/v5 validates nbf when present
validation.validate_nbf = true;
validation.leeway = 0;
let data = decode::<FileIdClaims>(
token,
&DecodingKey::from_secret(&signing_key.0),
&validation,
)?;
Ok(data.claims)
}
// ============================================================================
// Guard
// ============================================================================
/// Security guard: IP whitelist + JWT token validation.
pub struct Guard {
whitelist_ips: HashSet<String>,
whitelist_cidrs: Vec<(IpAddr, u8)>, // (network, prefix_len)
pub signing_key: SigningKey,
pub expires_after_sec: i64,
pub read_signing_key: SigningKey,
pub read_expires_after_sec: i64,
/// Combined flag: true when whitelist is non-empty OR signing key is present.
/// Matches Go's `isWriteActive = !isEmptyWhiteList || len(SigningKey) != 0`.
is_write_active: bool,
}
impl Guard {
pub fn new(
whitelist: &[String],
signing_key: SigningKey,
expires_after_sec: i64,
read_signing_key: SigningKey,
read_expires_after_sec: i64,
) -> Self {
let mut guard = Guard {
whitelist_ips: HashSet::new(),
whitelist_cidrs: Vec::new(),
signing_key,
expires_after_sec,
read_signing_key,
read_expires_after_sec,
is_write_active: false,
};
guard.update_whitelist(whitelist);
guard
}
/// Update the IP whitelist.
pub fn update_whitelist(&mut self, entries: &[String]) {
self.whitelist_ips.clear();
self.whitelist_cidrs.clear();
for entry in entries {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
if entry.contains('/') {
// CIDR range
if let Some((ip, prefix)) = parse_cidr(entry) {
self.whitelist_cidrs.push((ip, prefix));
} else {
tracing::error!("Parse CIDR {} in whitelist failed", entry);
}
} else {
// Exact IP/hostname
self.whitelist_ips.insert(entry.to_string());
}
}
// Match Go: isWriteActive = !isEmptyWhiteList || len(SigningKey) != 0
let is_empty_whitelist = self.whitelist_ips.is_empty() && self.whitelist_cidrs.is_empty();
self.is_write_active = !is_empty_whitelist || !self.signing_key.is_empty();
}
/// Refresh the JWT signing keys and their expirations in place so operators
/// can rotate keys via SIGHUP without restarting the process. Mirrors Go's
/// `Guard::UpdateSigningKeys`: it swaps the fields and recomputes
/// `is_write_active`.
pub fn update_signing_keys(
&mut self,
signing_key: SigningKey,
expires_after_sec: i64,
read_signing_key: SigningKey,
read_expires_after_sec: i64,
) {
self.signing_key = signing_key;
self.expires_after_sec = expires_after_sec;
self.read_signing_key = read_signing_key;
self.read_expires_after_sec = read_expires_after_sec;
let is_empty_whitelist = self.whitelist_ips.is_empty() && self.whitelist_cidrs.is_empty();
self.is_write_active = !is_empty_whitelist || !self.signing_key.is_empty();
}
/// Whether any write-side control is configured: a non-empty whitelist or a
/// signing key. Mirrors Go's non-nil `guard` -- when this is false, every
/// caller is allowed and admin RPCs need not carry peer info.
pub fn is_write_active(&self) -> bool {
self.is_write_active
}
/// Check if a remote IP is in the whitelist.
/// Returns true if write security is inactive (no whitelist and no signing key),
/// if the whitelist is empty, or if the IP matches.
pub fn check_whitelist(&self, remote_addr: &str) -> bool {
if !self.is_write_active {
return true;
}
if self.whitelist_ips.is_empty() && self.whitelist_cidrs.is_empty() {
return true;
}
let host = extract_host(remote_addr);
// Check exact match
if self.whitelist_ips.contains(&host) {
return true;
}
// Check CIDR ranges
if let Ok(ip) = host.parse::<IpAddr>() {
for &(ref network, prefix_len) in &self.whitelist_cidrs {
if ip_in_cidr(&ip, network, prefix_len) {
return true;
}
}
}
false
}
/// Check if a read signing key is configured.
pub fn has_read_signing_key(&self) -> bool {
!self.read_signing_key.is_empty()
}
/// Validate a request's JWT token.
/// `is_write` determines which signing key to use.
/// Returns Ok(()) if valid, or if security is disabled.
pub fn check_jwt(&self, token: Option<&str>, is_write: bool) -> Result<(), JwtError> {
let key = if is_write {
&self.signing_key
} else {
&self.read_signing_key
};
if key.is_empty() {
return Ok(()); // Security disabled for this operation type
}
let token = token.ok_or(JwtError::MissingToken)?;
decode_jwt(key, token)?;
Ok(())
}
/// Check JWT and validate the file ID claim matches.
pub fn check_jwt_for_file(
&self,
token: Option<&str>,
expected_fid: &str,
is_write: bool,
) -> Result<(), JwtError> {
let key = if is_write {
&self.signing_key
} else {
&self.read_signing_key
};
if key.is_empty() {
return Ok(());
}
let token = token.ok_or(JwtError::MissingToken)?;
let claims = decode_jwt(key, token)?;
match claims.fid {
None => {
return Err(JwtError::MissingFileIdClaim);
}
Some(ref fid) if fid != expected_fid => {
return Err(JwtError::FileIdMismatch {
expected: expected_fid.to_string(),
got: fid.to_string(),
});
}
_ => {}
}
Ok(())
}
}
// ============================================================================
// Helpers
// ============================================================================
/// Extract host from "host:port" or "[::1]:port" format.
fn extract_host(addr: &str) -> String {
// Handle IPv6 with brackets
if addr.starts_with('[')
&& let Some(end) = addr.find(']')
{
return addr[1..end].to_string();
}
// Handle host:port
if let Some(pos) = addr.rfind(':') {
return addr[..pos].to_string();
}
addr.to_string()
}
/// Parse CIDR notation "192.168.1.0/24" into (IpAddr, prefix_len).
fn parse_cidr(cidr: &str) -> Option<(IpAddr, u8)> {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
return None;
}
let ip: IpAddr = parts[0].parse().ok()?;
let prefix: u8 = parts[1].parse().ok()?;
Some((ip, prefix))
}
/// Check if an IP is within a CIDR range.
fn ip_in_cidr(ip: &IpAddr, network: &IpAddr, prefix_len: u8) -> bool {
match (ip, network) {
(IpAddr::V4(ip), IpAddr::V4(net)) => {
let ip_bits = u32::from(*ip);
let net_bits = u32::from(*net);
let mask = if prefix_len == 0 {
0
} else if prefix_len >= 32 {
u32::MAX
} else {
u32::MAX << (32 - prefix_len)
};
(ip_bits & mask) == (net_bits & mask)
}
(IpAddr::V6(ip), IpAddr::V6(net)) => {
let ip_bits = u128::from(*ip);
let net_bits = u128::from(*net);
let mask = if prefix_len == 0 {
0
} else if prefix_len >= 128 {
u128::MAX
} else {
u128::MAX << (128 - prefix_len)
};
(ip_bits & mask) == (net_bits & mask)
}
_ => false, // V4/V6 mismatch
}
}
// ============================================================================
// Errors
// ============================================================================
#[derive(Debug, thiserror::Error)]
pub enum JwtError {
#[error("no signing key configured")]
NoSigningKey,
#[error("missing JWT token")]
MissingToken,
#[error("JWT error: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
#[error("JWT token missing required fid claim")]
MissingFileIdClaim,
#[error("file ID mismatch: expected {expected}, got {got}")]
FileIdMismatch { expected: String, got: String },
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jwt_round_trip() {
let key = SigningKey::from_string("test-secret-key");
let token = gen_jwt(&key, 3600, "3,01637037d6").unwrap();
let claims = decode_jwt(&key, &token).unwrap();
assert_eq!(claims.fid, Some("3,01637037d6".to_string()));
}
#[test]
fn test_jwt_no_signing_key() {
let key = SigningKey(vec![]);
assert!(gen_jwt(&key, 3600, "1,abc").is_err());
}
#[test]
fn test_jwt_invalid_token() {
let key = SigningKey::from_string("secret");
let result = decode_jwt(&key, "invalid.token.here");
assert!(result.is_err());
}
#[test]
fn test_jwt_wrong_key() {
let key1 = SigningKey::from_string("secret1");
let key2 = SigningKey::from_string("secret2");
let token = gen_jwt(&key1, 3600, "1,abc").unwrap();
assert!(decode_jwt(&key2, &token).is_err());
}
#[test]
fn test_guard_empty_whitelist() {
let guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
assert!(guard.check_whitelist("192.168.1.1:8080"));
}
#[test]
fn test_guard_whitelist_exact() {
let guard = Guard::new(
&["192.168.1.1".to_string(), "10.0.0.1".to_string()],
SigningKey(vec![]),
0,
SigningKey(vec![]),
0,
);
assert!(guard.check_whitelist("192.168.1.1:8080"));
assert!(guard.check_whitelist("10.0.0.1:1234"));
assert!(!guard.check_whitelist("172.16.0.1:8080"));
}
#[test]
fn test_guard_whitelist_cidr() {
let guard = Guard::new(
&["10.0.0.0/8".to_string()],
SigningKey(vec![]),
0,
SigningKey(vec![]),
0,
);
assert!(guard.check_whitelist("10.1.2.3:8080"));
assert!(guard.check_whitelist("10.255.255.255:80"));
assert!(!guard.check_whitelist("11.0.0.1:80"));
}
#[test]
fn test_guard_check_jwt_disabled() {
let guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
// No signing key = security disabled
assert!(guard.check_jwt(None, true).is_ok());
assert!(guard.check_jwt(None, false).is_ok());
}
#[test]
fn test_guard_check_jwt_enabled() {
let key = SigningKey::from_string("write-secret");
let read_key = SigningKey::from_string("read-secret");
let guard = Guard::new(&[], key.clone(), 3600, read_key.clone(), 3600);
// Missing token
assert!(guard.check_jwt(None, true).is_err());
// Valid write token
let token = gen_jwt(&key, 3600, "1,abc").unwrap();
assert!(guard.check_jwt(Some(&token), true).is_ok());
// Write token for read should fail (different key)
assert!(guard.check_jwt(Some(&token), false).is_err());
// Valid read token
let read_token = gen_jwt(&read_key, 3600, "1,abc").unwrap();
assert!(guard.check_jwt(Some(&read_token), false).is_ok());
}
#[test]
fn test_guard_check_jwt_file_id() {
let key = SigningKey::from_string("secret");
let guard = Guard::new(&[], key.clone(), 3600, SigningKey(vec![]), 0);
let token = gen_jwt(&key, 3600, "3,01637037d6").unwrap();
// Correct file ID
assert!(
guard
.check_jwt_for_file(Some(&token), "3,01637037d6", true)
.is_ok()
);
// Wrong file ID
let err = guard.check_jwt_for_file(Some(&token), "4,deadbeef", true);
assert!(matches!(err, Err(JwtError::FileIdMismatch { .. })));
}
#[test]
fn test_update_signing_keys_rotates() {
let mut guard = Guard::new(
&[],
SigningKey::from_string("old-write"),
10,
SigningKey::from_string("old-read"),
60,
);
guard.update_signing_keys(
SigningKey::from_string("new-write"),
11,
SigningKey::from_string("new-read"),
61,
);
let token = gen_jwt(&guard.signing_key, 60, "3,01637037d6").unwrap();
// Validates with the rotated key, not the old one.
assert!(decode_jwt(&guard.signing_key, &token).is_ok());
assert!(decode_jwt(&SigningKey::from_string("old-write"), &token).is_err());
assert_eq!(guard.expires_after_sec, 11);
assert_eq!(guard.read_expires_after_sec, 61);
}
#[test]
fn test_update_signing_keys_toggles_write_active() {
let mut guard = Guard::new(&[], SigningKey(vec![]), 0, SigningKey(vec![]), 0);
assert!(guard.check_jwt(None, true).is_ok());
guard.update_signing_keys(SigningKey::from_string("write"), 10, SigningKey(vec![]), 0);
assert!(guard.is_write_active);
guard.update_signing_keys(SigningKey(vec![]), 0, SigningKey(vec![]), 0);
assert!(!guard.is_write_active);
}
#[test]
fn test_extract_host() {
assert_eq!(extract_host("192.168.1.1:8080"), "192.168.1.1");
assert_eq!(extract_host("[::1]:8080"), "::1");
assert_eq!(extract_host("localhost"), "localhost");
}
#[test]
fn test_ip_in_cidr() {
let net: IpAddr = "10.0.0.0".parse().unwrap();
let ip1: IpAddr = "10.1.2.3".parse().unwrap();
let ip2: IpAddr = "11.0.0.1".parse().unwrap();
assert!(ip_in_cidr(&ip1, &net, 8));
assert!(!ip_in_cidr(&ip2, &net, 8));
}
}