mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 11:06:17 +00:00
fix(security): harden CORS and license handling (#2774)
This commit is contained in:
Generated
+1
@@ -7700,6 +7700,7 @@ dependencies = [
|
|||||||
"pkcs1 0.8.0-rc.4",
|
"pkcs1 0.8.0-rc.4",
|
||||||
"pkcs8 0.11.0",
|
"pkcs8 0.11.0",
|
||||||
"rand_core 0.10.1",
|
"rand_core 0.10.1",
|
||||||
|
"sha2 0.11.0",
|
||||||
"signature 3.0.0-rc.10",
|
"signature 3.0.0-rc.10",
|
||||||
"spki 0.8.0",
|
"spki 0.8.0",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
|||||||
+1
-2
@@ -99,8 +99,7 @@ RUN addgroup -g 10001 -S rustfs && \
|
|||||||
chown -R rustfs:rustfs /data /logs && \
|
chown -R rustfs:rustfs /data /logs && \
|
||||||
chmod 0750 /data /logs
|
chmod 0750 /data /logs
|
||||||
|
|
||||||
ENV RUSTFS_CORS_ALLOWED_ORIGINS="*" \
|
ENV RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
|
||||||
RUSTFS_VOLUMES="/data" \
|
RUSTFS_VOLUMES="/data" \
|
||||||
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
||||||
RUSTFS_OBS_LOG_DIRECTORY=/logs \
|
RUSTFS_OBS_LOG_DIRECTORY=/logs \
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ ENV RUSTFS_ADDRESS=":9000" \
|
|||||||
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
RUSTFS_ACCESS_KEY="rustfsadmin" \
|
||||||
RUSTFS_SECRET_KEY="rustfsadmin" \
|
RUSTFS_SECRET_KEY="rustfsadmin" \
|
||||||
RUSTFS_CONSOLE_ENABLE="true" \
|
RUSTFS_CONSOLE_ENABLE="true" \
|
||||||
RUSTFS_CORS_ALLOWED_ORIGINS="*" \
|
|
||||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS="*" \
|
||||||
RUSTFS_VOLUMES="/data" \
|
RUSTFS_VOLUMES="/data" \
|
||||||
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
RUSTFS_OBS_LOGGER_LEVEL=warn \
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ categories = ["web-programming", "development-tools", "authentication"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64-simd = { workspace = true }
|
base64-simd = { workspace = true }
|
||||||
rsa = { workspace = true }
|
rsa = { workspace = true, features = ["sha2"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
|
|||||||
+123
-36
@@ -15,9 +15,13 @@
|
|||||||
use rsa::{
|
use rsa::{
|
||||||
Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey,
|
Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey,
|
||||||
pkcs8::{DecodePrivateKey, DecodePublicKey},
|
pkcs8::{DecodePrivateKey, DecodePublicKey},
|
||||||
|
pss::{BlindedSigningKey, Signature, VerifyingKey},
|
||||||
|
sha2::Sha256,
|
||||||
|
signature::{RandomizedSigner, Verifier},
|
||||||
|
traits::PublicKeyParts,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::io::{Error, Result};
|
use std::io::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||||
pub struct Token {
|
pub struct Token {
|
||||||
@@ -25,10 +29,11 @@ pub struct Token {
|
|||||||
pub expired: u64, // Expiry time (UNIX timestamp)
|
pub expired: u64, // Expiry time (UNIX timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Public key generation Token
|
/// Legacy public-key encryption Token encoder.
|
||||||
/// [token] Token object
|
///
|
||||||
/// [key] Public key string
|
/// Use `sign_license_token` for license issuance so verifiers only need a
|
||||||
/// Returns the encrypted string processed by base64
|
/// public key.
|
||||||
|
#[deprecated(note = "use sign_license_token for signed license issuance")]
|
||||||
pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
||||||
let data = serde_json::to_vec(token)?;
|
let data = serde_json::to_vec(token)?;
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
@@ -37,35 +42,62 @@ pub fn gencode(token: &Token, key: &str) -> Result<String> {
|
|||||||
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data))
|
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&encrypted_data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Private key resolution Token
|
/// Legacy private-key Token decoder.
|
||||||
/// [token] Encrypted string processed by base64
|
///
|
||||||
/// [key] Private key string
|
/// Use `parse_signed_license_token` or `parse_license_with_public_key` for
|
||||||
/// Return to the Token object
|
/// license verification so runtime services never need private key material.
|
||||||
|
#[deprecated(note = "use parse_signed_license_token or parse_license_with_public_key for signed license verification")]
|
||||||
pub fn parse(token: &str, key: &str) -> Result<Token> {
|
pub fn parse(token: &str, key: &str) -> Result<Token> {
|
||||||
let encrypted_data = base64_simd::URL_SAFE_NO_PAD
|
let encrypted_data = base64_simd::URL_SAFE_NO_PAD
|
||||||
.decode_to_vec(token.as_bytes())
|
.decode_to_vec(token.as_bytes())
|
||||||
.map_err(Error::other)?;
|
.map_err(Error::other)?;
|
||||||
let private_key = RsaPrivateKey::from_pkcs8_pem(key).map_err(Error::other)?;
|
let private_key = RsaPrivateKey::from_pkcs8_pem(key).map_err(Error::other)?;
|
||||||
let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data).map_err(Error::other)?;
|
let decrypted_data = private_key.decrypt(Pkcs1v15Encrypt, &encrypted_data).map_err(Error::other)?;
|
||||||
let res: Token = serde_json::from_slice(&decrypted_data)?;
|
serde_json::from_slice(&decrypted_data).map_err(Error::other)
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_license(license: &str) -> Result<Token> {
|
/// Signs a license token with an RSA private key.
|
||||||
parse(license, TEST_PRIVATE_KEY)
|
///
|
||||||
// match parse(license, TEST_PRIVATE_KEY) {
|
/// The returned token is base64url(signature || payload), where the signature is
|
||||||
// Ok(token) => {
|
/// RSASSA-PSS over the JSON payload using SHA-256.
|
||||||
// if token.expired > SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() {
|
pub fn sign_license_token(token: &Token, private_key_pem: &str) -> Result<String> {
|
||||||
// Ok(token)
|
let payload = serde_json::to_vec(token)?;
|
||||||
// } else {
|
let mut rng = rand::rng();
|
||||||
// Err("Token expired".into())
|
let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(Error::other)?;
|
||||||
// }
|
let signing_key = BlindedSigningKey::<Sha256>::new(private_key);
|
||||||
// }
|
let signature: Signature = signing_key.try_sign_with_rng(&mut rng, &payload).map_err(Error::other)?;
|
||||||
// Err(e) => Err(e),
|
let signature: Box<[u8]> = signature.into();
|
||||||
// }
|
|
||||||
|
let mut signed_payload = Vec::with_capacity(signature.as_ref().len() + payload.len());
|
||||||
|
signed_payload.extend_from_slice(signature.as_ref());
|
||||||
|
signed_payload.extend_from_slice(&payload);
|
||||||
|
|
||||||
|
Ok(base64_simd::URL_SAFE_NO_PAD.encode_to_string(&signed_payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
static TEST_PRIVATE_KEY: &str = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj86SrJIuxSxR6\nBJ/dlJEUIj6NeBRnhLQlCDdovuz61+7kJXVcxaR66w4m8W7SLEUP+IlPtnn6vmiG\n7XMhGNHIr7r1JsEVVLhZmL3tKI66DEZl786ZhG81BWqUlmcooIPS8UEPZNqJXLuz\nVGhxNyVGbj/tV7QC2pSISnKaixc+nrhxvo7w56p5qrm9tik0PjTgfZsUePkoBsSN\npoRkAauS14MAzK6HGB75CzG3dZqXUNWSWVocoWtQbZUwFGXyzU01ammsHQDvc2xu\nK1RQpd1qYH5bOWZ0N0aPFwT0r59HztFXg9sbjsnuhO1A7OiUOkc6iGVuJ0wm/9nA\nwZIBqzgjAgMBAAECggEAPMpeSEbotPhNw2BrllE76ec4omPfzPJbiU+em+wPGoNu\nRJHPDnMKJbl6Kd5jZPKdOOrCnxfd6qcnQsBQa/kz7+GYxMV12l7ra+1Cnujm4v0i\nLTHZvPpp8ZLsjeOmpF3AAzsJEJgon74OqtOlVjVIUPEYKvzV9ijt4gsYq0zfdYv0\nhrTMzyrGM4/UvKLsFIBROAfCeWfA7sXLGH8JhrRAyDrtCPzGtyyAmzoHKHtHafcB\nuyPFw/IP8otAgpDk5iiQPNkH0WwzAQIm12oHuNUa66NwUK4WEjXTnDg8KeWLHHNv\nIfN8vdbZchMUpMIvvkr7is315d8f2cHCB5gEO+GWAQKBgQDR/0xNll+FYaiUKCPZ\nvkOCAd3l5mRhsqnjPQ/6Ul1lAyYWpoJSFMrGGn/WKTa/FVFJRTGbBjwP+Mx10bfb\ngUg2GILDTISUh54fp4zngvTi9w4MWGKXrb7I1jPkM3vbJfC/v2fraQ/r7qHPpO2L\nf6ZbGxasIlSvr37KeGoelwcAQQKBgQDH3hmOTS2Hl6D4EXdq5meHKrfeoicGN7m8\noQK7u8iwn1R9zK5nh6IXxBhKYNXNwdCQtBZVRvFjjZ56SZJb7lKqa1BcTsgJfZCy\nnI3Uu4UykrECAH8AVCVqBXUDJmeA2yE+gDAtYEjvhSDHpUfWxoGHr0B/Oqk2Lxc/\npRy1qV5fYwKBgBWSL/hYVf+RhIuTg/s9/BlCr9SJ0g3nGGRrRVTlWQqjRCpXeFOO\nJzYqSq9pFGKUggEQxoOyJEFPwVDo9gXqRcyov+Xn2kaXl7qQr3yoixc1YZALFDWY\nd1ySBEqQr0xXnV9U/gvEgwotPRnjSzNlLWV2ZuHPtPtG/7M0o1H5GZMBAoGAKr3N\nW0gX53o+my4pCnxRQW+aOIsWq1a5aqRIEFudFGBOUkS2Oz+fI1P1GdrRfhnnfzpz\n2DK+plp/vIkFOpGhrf4bBlJ2psjqa7fdANRFLMaAAfyXLDvScHTQTCcnVUAHQPVq\n2BlSH56pnugyj7SNuLV6pnql+wdhAmRN2m9o1h8CgYAbX2juSr4ioXwnYjOUdrIY\n4+ERvHcXdjoJmmPcAm4y5NbSqLXyU0FQmplNMt2A5LlniWVJ9KNdjAQUt60FZw/+\nr76LdxXaHNZghyx0BOs7mtq5unSQXamZ8KixasfhE9uz3ij1jXjG6hafWkS8/68I\nuWbaZqgvy7a9oPHYlKH7Jg==\n-----END PRIVATE KEY-----\n";
|
/// Verifies and parses a signed license token with an RSA public key.
|
||||||
|
pub fn parse_signed_license_token(token: &str, public_key_pem: &str) -> Result<Token> {
|
||||||
|
let signed_payload = base64_simd::URL_SAFE_NO_PAD
|
||||||
|
.decode_to_vec(token.as_bytes())
|
||||||
|
.map_err(Error::other)?;
|
||||||
|
let public_key = RsaPublicKey::from_public_key_pem(public_key_pem).map_err(Error::other)?;
|
||||||
|
let signature_len = public_key.size();
|
||||||
|
|
||||||
|
if signed_payload.len() <= signature_len {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "license token is missing signed payload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (signature, payload) = signed_payload.split_at(signature_len);
|
||||||
|
let signature = Signature::try_from(signature).map_err(Error::other)?;
|
||||||
|
let verifying_key = VerifyingKey::<Sha256>::new(public_key);
|
||||||
|
verifying_key.verify(payload, &signature).map_err(Error::other)?;
|
||||||
|
|
||||||
|
serde_json::from_slice(payload).map_err(Error::other)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_license_with_public_key(license: &str, public_key: &str) -> Result<Token> {
|
||||||
|
parse_signed_license_token(license, public_key)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -77,7 +109,7 @@ mod tests {
|
|||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gencode_and_parse() {
|
fn test_sign_license_token_and_parse_signed_license_token() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let bits = 2048;
|
let bits = 2048;
|
||||||
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||||
@@ -91,8 +123,31 @@ mod tests {
|
|||||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||||
|
|
||||||
|
let decoded = parse_signed_license_token(&encoded, &public_key_pem).expect("Failed to decode token");
|
||||||
|
|
||||||
|
assert_eq!(token.name, decoded.name);
|
||||||
|
assert_eq!(token.expired, decoded.expired);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(deprecated)]
|
||||||
|
fn test_legacy_gencode_and_parse_roundtrip() {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let bits = 2048;
|
||||||
|
let private_key = RsaPrivateKey::new(&mut rng, bits).expect("Failed to generate private key");
|
||||||
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
|
|
||||||
|
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||||
|
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||||
|
|
||||||
|
let token = Token {
|
||||||
|
name: "test_app".to_string(),
|
||||||
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = gencode(&token, &public_key_pem).expect("Failed to encode token");
|
||||||
let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token");
|
let decoded = parse(&encoded, &private_key_pem).expect("Failed to decode token");
|
||||||
|
|
||||||
assert_eq!(token.name, decoded.name);
|
assert_eq!(token.name, decoded.name);
|
||||||
@@ -100,28 +155,60 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_invalid_token() {
|
fn test_parse_signed_license_token_rejects_tampered_payload() {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
let private_key_pem = RsaPrivateKey::new(&mut rng, 2048)
|
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate private key");
|
||||||
.expect("Failed to generate private key")
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
.to_pkcs8_pem(LineEnding::LF)
|
let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).unwrap();
|
||||||
.unwrap();
|
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||||
|
let token = Token {
|
||||||
|
name: "test_app".to_string(),
|
||||||
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600,
|
||||||
|
};
|
||||||
|
|
||||||
let invalid_token = "invalid_base64_token";
|
let encoded = sign_license_token(&token, &private_key_pem).expect("Failed to encode token");
|
||||||
let result = parse(invalid_token, &private_key_pem);
|
let mut signed_payload = base64_simd::URL_SAFE_NO_PAD
|
||||||
|
.decode_to_vec(encoded.as_bytes())
|
||||||
|
.expect("Failed to decode signed payload");
|
||||||
|
let last_byte = signed_payload.last_mut().expect("Signed payload should not be empty");
|
||||||
|
*last_byte ^= 0x01;
|
||||||
|
let tampered = base64_simd::URL_SAFE_NO_PAD.encode_to_string(&signed_payload);
|
||||||
|
|
||||||
|
let result = parse_signed_license_token(&tampered, &public_key_pem);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_gencode_with_invalid_key() {
|
fn test_source_does_not_embed_private_key() {
|
||||||
|
let source = include_str!("token.rs");
|
||||||
|
let forbidden = ["BEGIN", "PRIVATE KEY"].join(" ");
|
||||||
|
|
||||||
|
assert!(!source.contains(&forbidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_signed_license_token_rejects_invalid_token() {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("Failed to generate private key");
|
||||||
|
let public_key = RsaPublicKey::from(&private_key);
|
||||||
|
let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).unwrap();
|
||||||
|
|
||||||
|
let invalid_token = "invalid_base64_token";
|
||||||
|
let result = parse_signed_license_token(invalid_token, &public_key_pem);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sign_license_token_with_invalid_signing_key() {
|
||||||
let token = Token {
|
let token = Token {
|
||||||
name: "test_app".to_string(),
|
name: "test_app".to_string(),
|
||||||
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
expired: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 3600, // 1 hour from now
|
||||||
};
|
};
|
||||||
|
|
||||||
let invalid_key = "invalid_public_key";
|
let invalid_key = "invalid_private_key";
|
||||||
let result = gencode(&token, invalid_key);
|
let result = sign_license_token(&token, invalid_key);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Examples:
|
|||||||
- `RUSTFS_ADDRESS`
|
- `RUSTFS_ADDRESS`
|
||||||
- `RUSTFS_VOLUMES`
|
- `RUSTFS_VOLUMES`
|
||||||
- `RUSTFS_LICENSE`
|
- `RUSTFS_LICENSE`
|
||||||
|
- `RUSTFS_LICENSE_PUBLIC_KEY`
|
||||||
|
|
||||||
Current guidance:
|
Current guidance:
|
||||||
- Prefer module-specific names only when they are not top-level product configuration.
|
- Prefer module-specific names only when they are not top-level product configuration.
|
||||||
@@ -51,6 +52,16 @@ Current guidance:
|
|||||||
- `RUSTFS_ENABLE_HEAL` -> `RUSTFS_HEAL_ENABLED`
|
- `RUSTFS_ENABLE_HEAL` -> `RUSTFS_HEAL_ENABLED`
|
||||||
- `RUSTFS_DATA_SCANNER_START_DELAY_SECS` -> `RUSTFS_SCANNER_START_DELAY_SECS`
|
- `RUSTFS_DATA_SCANNER_START_DELAY_SECS` -> `RUSTFS_SCANNER_START_DELAY_SECS`
|
||||||
|
|
||||||
|
## License environment variables
|
||||||
|
|
||||||
|
- `RUSTFS_LICENSE` contains the signed license token.
|
||||||
|
- `RUSTFS_LICENSE_PUBLIC_KEY` contains the RSA public key used to verify signed license tokens.
|
||||||
|
|
||||||
|
## CORS environment variables
|
||||||
|
|
||||||
|
- `RUSTFS_CORS_ALLOWED_ORIGINS` defaults to empty, so the S3 endpoint emits no generic CORS headers unless configured. Set `*` for wildcard origins without credentials, or a comma-separated allow-list for credentialed explicit origins.
|
||||||
|
- `RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS` defaults to `*` for the console service.
|
||||||
|
|
||||||
## Scanner environment aliases
|
## Scanner environment aliases
|
||||||
|
|
||||||
- `RUSTFS_SCANNER_SPEED` (canonical, also accepts `MINIO_SCANNER_SPEED`)
|
- `RUSTFS_SCANNER_SPEED` (canonical, also accepts `MINIO_SCANNER_SPEED`)
|
||||||
|
|||||||
@@ -235,6 +235,9 @@ pub const ENV_RUSTFS_REGION: &str = "RUSTFS_REGION";
|
|||||||
/// Environment variable for server license.
|
/// Environment variable for server license.
|
||||||
pub const ENV_RUSTFS_LICENSE: &str = "RUSTFS_LICENSE";
|
pub const ENV_RUSTFS_LICENSE: &str = "RUSTFS_LICENSE";
|
||||||
|
|
||||||
|
/// Environment variable for the RSA public key used to verify server licenses.
|
||||||
|
pub const ENV_RUSTFS_LICENSE_PUBLIC_KEY: &str = "RUSTFS_LICENSE_PUBLIC_KEY";
|
||||||
|
|
||||||
/// Default log filename for rustfs
|
/// Default log filename for rustfs
|
||||||
/// This is the default log filename for rustfs.
|
/// This is the default log filename for rustfs.
|
||||||
/// It is used to store the logs of the application.
|
/// It is used to store the logs of the application.
|
||||||
@@ -348,6 +351,7 @@ mod tests {
|
|||||||
fn test_environment_constants() {
|
fn test_environment_constants() {
|
||||||
// Test environment related constants
|
// Test environment related constants
|
||||||
assert_eq!(ENVIRONMENT, "production");
|
assert_eq!(ENVIRONMENT, "production");
|
||||||
|
assert_eq!(ENV_RUSTFS_LICENSE_PUBLIC_KEY, "RUSTFS_LICENSE_PUBLIC_KEY");
|
||||||
assert!(
|
assert!(
|
||||||
["development", "staging", "production", "test"].contains(&ENVIRONMENT),
|
["development", "staging", "production", "test"].contains(&ENVIRONMENT),
|
||||||
"Environment should be a standard environment name"
|
"Environment should be a standard environment name"
|
||||||
|
|||||||
@@ -17,9 +17,8 @@
|
|||||||
pub const ENV_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CORS_ALLOWED_ORIGINS";
|
pub const ENV_CORS_ALLOWED_ORIGINS: &str = "RUSTFS_CORS_ALLOWED_ORIGINS";
|
||||||
|
|
||||||
/// Default CORS allowed origins for the endpoint service
|
/// Default CORS allowed origins for the endpoint service
|
||||||
/// Comes from the console service default
|
/// Empty means the S3 endpoint emits no generic CORS headers unless configured.
|
||||||
/// See DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS
|
pub const DEFAULT_CORS_ALLOWED_ORIGINS: &str = "";
|
||||||
pub const DEFAULT_CORS_ALLOWED_ORIGINS: &str = DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS;
|
|
||||||
|
|
||||||
/// CORS allowed origins for the console service
|
/// CORS allowed origins for the console service
|
||||||
/// Comma-separated list of origins or "*" for all origins
|
/// Comma-separated list of origins or "*" for all origins
|
||||||
@@ -89,3 +88,20 @@ pub const ENV_UPDATE_CHECK: &str = "RUSTFS_CHECK_UPDATE";
|
|||||||
|
|
||||||
/// Default value for update toggle
|
/// Default value for update toggle
|
||||||
pub const DEFAULT_UPDATE_CHECK: bool = true;
|
pub const DEFAULT_UPDATE_CHECK: bool = true;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn endpoint_cors_default_is_restrictive() {
|
||||||
|
assert_eq!(ENV_CORS_ALLOWED_ORIGINS, "RUSTFS_CORS_ALLOWED_ORIGINS");
|
||||||
|
assert_eq!(DEFAULT_CORS_ALLOWED_ORIGINS, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn console_cors_default_remains_wildcard() {
|
||||||
|
assert_eq!(ENV_CONSOLE_CORS_ALLOWED_ORIGINS, "RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS");
|
||||||
|
assert_eq!(DEFAULT_CONSOLE_CORS_ALLOWED_ORIGINS, "*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ services:
|
|||||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||||
- RUSTFS_CONSOLE_ENABLE=true
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
|
||||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||||
- RUSTFS_ACCESS_KEY=rustfsadmin # CHANGEME
|
- RUSTFS_ACCESS_KEY=rustfsadmin # CHANGEME
|
||||||
- RUSTFS_SECRET_KEY=rustfsadmin # CHANGEME
|
- RUSTFS_SECRET_KEY=rustfsadmin # CHANGEME
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ services:
|
|||||||
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
RUSTFS_ADDRESS: "0.0.0.0:9000"
|
||||||
RUSTFS_CONSOLE_ADDRESS: "0.0.0.0:9001"
|
RUSTFS_CONSOLE_ADDRESS: "0.0.0.0:9001"
|
||||||
RUSTFS_CONSOLE_ENABLE: "true"
|
RUSTFS_CONSOLE_ENABLE: "true"
|
||||||
RUSTFS_CORS_ALLOWED_ORIGINS: "*"
|
|
||||||
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS: "*"
|
RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS: "*"
|
||||||
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
RUSTFS_ACCESS_KEY: "rustfsadmin"
|
||||||
RUSTFS_SECRET_KEY: "rustfsadmin"
|
RUSTFS_SECRET_KEY: "rustfsadmin"
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ services:
|
|||||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||||
- RUSTFS_CONSOLE_ENABLE=true
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
|
||||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||||
- RUSTFS_ACCESS_KEY=rustfsadmin
|
- RUSTFS_ACCESS_KEY=rustfsadmin
|
||||||
- RUSTFS_SECRET_KEY=rustfsadmin
|
- RUSTFS_SECRET_KEY=rustfsadmin
|
||||||
@@ -90,7 +89,6 @@ services:
|
|||||||
- RUSTFS_ADDRESS=0.0.0.0:9000
|
- RUSTFS_ADDRESS=0.0.0.0:9000
|
||||||
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
- RUSTFS_CONSOLE_ADDRESS=0.0.0.0:9001
|
||||||
- RUSTFS_CONSOLE_ENABLE=true
|
- RUSTFS_CONSOLE_ENABLE=true
|
||||||
- RUSTFS_CORS_ALLOWED_ORIGINS=*
|
|
||||||
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
- RUSTFS_CONSOLE_CORS_ALLOWED_ORIGINS=*
|
||||||
- RUSTFS_ACCESS_KEY=devadmin
|
- RUSTFS_ACCESS_KEY=devadmin
|
||||||
- RUSTFS_SECRET_KEY=devadmin
|
- RUSTFS_SECRET_KEY=devadmin
|
||||||
|
|||||||
+36
-14
@@ -13,11 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use crate::admin::handlers::health::{HealthProbe, build_health_payload, collect_dependency_readiness, health_check_state};
|
use crate::admin::handlers::health::{HealthProbe, build_health_payload, collect_dependency_readiness, health_check_state};
|
||||||
use crate::license::get_license;
|
use crate::license::has_valid_license;
|
||||||
use crate::server::{CONSOLE_PREFIX, FAVICON_PATH, HEALTH_PREFIX, HEALTH_READY_PATH, LICENSE, RUSTFS_ADMIN_PREFIX, VERSION};
|
use crate::server::{CONSOLE_PREFIX, FAVICON_PATH, HEALTH_PREFIX, HEALTH_READY_PATH, LICENSE, RUSTFS_ADMIN_PREFIX, VERSION};
|
||||||
use crate::version::build;
|
use crate::version::build;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Json, Router,
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::Request,
|
extract::Request,
|
||||||
middleware,
|
middleware,
|
||||||
@@ -241,20 +241,17 @@ pub(crate) fn init_console_cfg(local_ip: IpAddr, port: u16) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// License handler
|
#[derive(Serialize)]
|
||||||
/// Returns the current license information of the console.
|
struct LicensePublicStatus {
|
||||||
///
|
licensed: bool,
|
||||||
/// # Returns:
|
}
|
||||||
/// - 200 OK with JSON body containing license details.
|
|
||||||
|
/// Returns coarse public license status without exposing license metadata.
|
||||||
#[instrument]
|
#[instrument]
|
||||||
async fn license_handler() -> impl IntoResponse {
|
async fn license_handler() -> impl IntoResponse {
|
||||||
let license = get_license().unwrap_or_default();
|
Json(LicensePublicStatus {
|
||||||
|
licensed: has_valid_license(),
|
||||||
Response::builder()
|
})
|
||||||
.header("content-type", "application/json")
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.body(Body::from(serde_json::to_string(&license).unwrap_or_default()))
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given IP address is a private IP
|
/// Check if the given IP address is a private IP
|
||||||
@@ -678,6 +675,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use http::{Request, StatusCode};
|
use http::{Request, StatusCode};
|
||||||
|
use http_body_util::BodyExt;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use temp_env::async_with_vars;
|
use temp_env::async_with_vars;
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
@@ -757,4 +755,28 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn console_license_route_returns_public_status_only() {
|
||||||
|
let app = setup_console_middleware_stack(parse_cors_origins(None), false, 0, 30);
|
||||||
|
let request = Request::builder()
|
||||||
|
.uri(format!("{CONSOLE_PREFIX}{LICENSE}"))
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("failed to build license request");
|
||||||
|
|
||||||
|
let response = app.oneshot(request).await.expect("license request should complete");
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let body = response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("license body should collect")
|
||||||
|
.to_bytes();
|
||||||
|
let value: serde_json::Value = serde_json::from_slice(&body).expect("license response should be valid JSON");
|
||||||
|
|
||||||
|
assert_eq!(value, serde_json::json!({ "licensed": false }));
|
||||||
|
assert!(value.get("name").is_none());
|
||||||
|
assert!(value.get("expired").is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-3
@@ -12,8 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
use rustfs_appauth::token::Token;
|
use rustfs_appauth::token::{Token, parse_license_with_public_key};
|
||||||
use rustfs_appauth::token::parse_license;
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::{Error, ErrorKind, Result};
|
use std::io::{Error, ErrorKind, Result};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -108,7 +107,9 @@ struct AppAuthLicenseVerifier;
|
|||||||
|
|
||||||
impl LicenseVerifier for AppAuthLicenseVerifier {
|
impl LicenseVerifier for AppAuthLicenseVerifier {
|
||||||
fn validate(&self, raw_license: &str, _now: u64) -> LicenseResult<Token> {
|
fn validate(&self, raw_license: &str, _now: u64) -> LicenseResult<Token> {
|
||||||
let token = parse_license(raw_license).map_err(|err| LicenseError::Invalid(err.to_string()))?;
|
let public_key = license_public_key()?;
|
||||||
|
let token =
|
||||||
|
parse_license_with_public_key(raw_license, &public_key).map_err(|err| LicenseError::Invalid(err.to_string()))?;
|
||||||
|
|
||||||
#[cfg(feature = "license")]
|
#[cfg(feature = "license")]
|
||||||
if token.expired <= _now {
|
if token.expired <= _now {
|
||||||
@@ -148,6 +149,30 @@ fn normalized_license(raw_license: Option<String>) -> Option<String> {
|
|||||||
raw_license.map(|raw| raw.trim().to_string()).filter(|raw| !raw.is_empty())
|
raw_license.map(|raw| raw.trim().to_string()).filter(|raw| !raw.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn license_public_key() -> LicenseResult<String> {
|
||||||
|
let public_key = std::env::var(rustfs_config::ENV_RUSTFS_LICENSE_PUBLIC_KEY)
|
||||||
|
.map(|raw| raw.trim().to_string())
|
||||||
|
.map_err(|_| {
|
||||||
|
LicenseError::Invalid(format!(
|
||||||
|
"{} must contain the RSA public key used to verify licenses",
|
||||||
|
rustfs_config::ENV_RUSTFS_LICENSE_PUBLIC_KEY
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if public_key.is_empty() {
|
||||||
|
return Err(LicenseError::Invalid(format!(
|
||||||
|
"{} must contain the RSA public key used to verify licenses",
|
||||||
|
rustfs_config::ENV_RUSTFS_LICENSE_PUBLIC_KEY
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_license_token_current(token: &Token, now: u64) -> bool {
|
||||||
|
token.expired > now
|
||||||
|
}
|
||||||
|
|
||||||
fn strict_build_missing_status() -> LicenseStatus {
|
fn strict_build_missing_status() -> LicenseStatus {
|
||||||
if cfg!(feature = "license") {
|
if cfg!(feature = "license") {
|
||||||
LicenseStatus::Missing
|
LicenseStatus::Missing
|
||||||
@@ -243,6 +268,18 @@ pub fn current_license() -> Option<Token> {
|
|||||||
get_license()
|
get_license()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return whether the loaded license token is present and not expired.
|
||||||
|
pub fn has_valid_license() -> bool {
|
||||||
|
let Some(token) = get_license() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(now) = now_epoch_secs() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
is_license_token_current(&token, now)
|
||||||
|
}
|
||||||
|
|
||||||
/// Observe the current license status for observability.
|
/// Observe the current license status for observability.
|
||||||
pub fn license_status() -> String {
|
pub fn license_status() -> String {
|
||||||
license_state()
|
license_state()
|
||||||
@@ -283,3 +320,20 @@ pub fn ensure_license() -> LicenseResult<()> {
|
|||||||
pub fn license_check() -> Result<()> {
|
pub fn license_check() -> Result<()> {
|
||||||
ensure_license().map_err(LicenseError::into_io)
|
ensure_license().map_err(LicenseError::into_io)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn license_token_current_requires_future_expiration() {
|
||||||
|
let token = Token {
|
||||||
|
name: "test_app".to_string(),
|
||||||
|
expired: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(is_license_token_current(&token, 99));
|
||||||
|
assert!(!is_license_token_current(&token, 100));
|
||||||
|
assert!(!is_license_token_current(&token, 101));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+46
-35
@@ -670,7 +670,7 @@ pub struct ConditionalCorsLayer {
|
|||||||
|
|
||||||
impl ConditionalCorsLayer {
|
impl ConditionalCorsLayer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let cors_origins = get_env_opt_str("RUSTFS_CORS_ALLOWED_ORIGINS").filter(|s| !s.is_empty());
|
let cors_origins = get_env_opt_str(rustfs_config::ENV_CORS_ALLOWED_ORIGINS).filter(|s| !s.is_empty());
|
||||||
Self { cors_origins }
|
Self { cors_origins }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,36 +687,31 @@ impl ConditionalCorsLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn apply_cors_headers(&self, request_headers: &HeaderMap, response_headers: &mut HeaderMap) {
|
fn apply_cors_headers(&self, request_headers: &HeaderMap, response_headers: &mut HeaderMap) {
|
||||||
let origin = request_headers
|
let Some(origin) = request_headers.get(cors::standard::ORIGIN).and_then(|v| v.to_str().ok()) else {
|
||||||
.get(cors::standard::ORIGIN)
|
return;
|
||||||
.and_then(|v| v.to_str().ok())
|
};
|
||||||
.map(|s| s.to_string());
|
let Some(config) = self
|
||||||
|
.cors_origins
|
||||||
let allowed_origin = match (origin, &self.cors_origins) {
|
.as_deref()
|
||||||
(Some(orig), Some(config)) if config == "*" => Some(orig),
|
.map(str::trim)
|
||||||
(Some(orig), Some(config)) => {
|
.filter(|config| !config.is_empty())
|
||||||
if config.split(',').map(|s| s.trim()).any(|x| x == orig.as_str()) {
|
else {
|
||||||
Some(orig)
|
return;
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(orig), None) => Some(orig), // Default: allow all if not configured
|
|
||||||
_ => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Track whether we're using a specific origin (not wildcard)
|
let (allow_origin, allow_credentials) = if config == "*" {
|
||||||
let using_specific_origin = if let Some(origin) = &allowed_origin {
|
(HeaderValue::from_static("*"), false)
|
||||||
if let Ok(header_value) = HeaderValue::from_str(origin) {
|
} else if config.split(',').map(str::trim).any(|allowed| allowed == origin) {
|
||||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, header_value);
|
let Ok(origin) = HeaderValue::from_str(origin) else {
|
||||||
true // Using specific origin, credentials allowed
|
return;
|
||||||
} else {
|
};
|
||||||
false
|
(origin, true)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
false
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN, allow_origin);
|
||||||
|
|
||||||
// Allow all methods by default (S3-compatible set)
|
// Allow all methods by default (S3-compatible set)
|
||||||
response_headers.insert(
|
response_headers.insert(
|
||||||
cors::response::ACCESS_CONTROL_ALLOW_METHODS,
|
cors::response::ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
@@ -732,9 +727,8 @@ impl ConditionalCorsLayer {
|
|||||||
HeaderValue::from_static("x-request-id, content-type, content-length, etag"),
|
HeaderValue::from_static("x-request-id, content-type, content-length, etag"),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only set credentials when using a specific origin (not wildcard)
|
// Credentials are only safe for origins matched from an explicit allow-list.
|
||||||
// CORS spec: credentials cannot be used with wildcard origins
|
if allow_credentials {
|
||||||
if using_specific_origin {
|
|
||||||
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
|
response_headers.insert(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS, HeaderValue::from_static("true"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1077,7 +1071,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_generic_cors_layer_echoes_allowed_origin() {
|
fn test_generic_cors_layer_omits_headers_without_configured_origins() {
|
||||||
let cors = ConditionalCorsLayer { cors_origins: None };
|
let cors = ConditionalCorsLayer { cors_origins: None };
|
||||||
let mut req_headers = HeaderMap::new();
|
let mut req_headers = HeaderMap::new();
|
||||||
req_headers.insert("origin", "https://example.com".parse().unwrap());
|
req_headers.insert("origin", "https://example.com".parse().unwrap());
|
||||||
@@ -1085,10 +1079,11 @@ mod tests {
|
|||||||
let mut resp_headers = HeaderMap::new();
|
let mut resp_headers = HeaderMap::new();
|
||||||
cors.apply_cors_headers(&req_headers, &mut resp_headers);
|
cors.apply_cors_headers(&req_headers, &mut resp_headers);
|
||||||
|
|
||||||
assert_eq!(
|
assert!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).is_none());
|
||||||
resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
|
assert!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS).is_none());
|
||||||
"https://example.com"
|
assert!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_METHODS).is_none());
|
||||||
);
|
assert!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_HEADERS).is_none());
|
||||||
|
assert!(resp_headers.get(cors::response::ACCESS_CONTROL_EXPOSE_HEADERS).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1111,11 +1106,27 @@ mod tests {
|
|||||||
resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
|
resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(),
|
||||||
"https://allowed.com"
|
"https://allowed.com"
|
||||||
);
|
);
|
||||||
|
assert_eq!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS).unwrap(), "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generic_cors_layer_wildcard_does_not_allow_credentials() {
|
||||||
|
let cors = ConditionalCorsLayer {
|
||||||
|
cors_origins: Some("*".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut req_headers = HeaderMap::new();
|
||||||
|
req_headers.insert("origin", "https://example.com".parse().unwrap());
|
||||||
|
let mut resp_headers = HeaderMap::new();
|
||||||
|
cors.apply_cors_headers(&req_headers, &mut resp_headers);
|
||||||
|
|
||||||
|
assert_eq!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), "*");
|
||||||
|
assert!(resp_headers.get(cors::response::ACCESS_CONTROL_ALLOW_CREDENTIALS).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conditional_cors_layer_reads_env() {
|
fn test_conditional_cors_layer_reads_env() {
|
||||||
with_var("RUSTFS_CORS_ALLOWED_ORIGINS", Some("https://allowed.com"), || {
|
with_var(rustfs_config::ENV_CORS_ALLOWED_ORIGINS, Some("https://allowed.com"), || {
|
||||||
let cors = ConditionalCorsLayer::new();
|
let cors = ConditionalCorsLayer::new();
|
||||||
assert_eq!(cors.cors_origins.as_deref(), Some("https://allowed.com"));
|
assert_eq!(cors.cors_origins.as_deref(), Some("https://allowed.com"));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user