mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
security(admin): reject unknown JSON ingress fields (#3376)
This commit is contained in:
@@ -20,7 +20,7 @@ use crate::config::{
|
||||
};
|
||||
use crate::service_manager::KmsServiceStatus;
|
||||
use crate::types::{KeyMetadata, KeyUsage};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
@@ -28,6 +28,7 @@ use std::time::Duration;
|
||||
|
||||
/// Request to configure KMS with Local backend
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigureLocalKmsRequest {
|
||||
/// Directory to store key files
|
||||
pub key_dir: PathBuf,
|
||||
@@ -71,10 +72,12 @@ impl fmt::Debug for ConfigureLocalKmsRequest {
|
||||
|
||||
/// Request to configure KMS with Vault KV v2 + Transit backend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigureVaultKmsRequest {
|
||||
/// Vault server URL
|
||||
pub address: String,
|
||||
/// Authentication method
|
||||
#[serde(deserialize_with = "deserialize_strict_vault_auth_method")]
|
||||
pub auth_method: VaultAuthMethod,
|
||||
/// Vault namespace (Vault Enterprise, optional)
|
||||
pub namespace: Option<String>,
|
||||
@@ -104,10 +107,12 @@ pub struct ConfigureVaultKmsRequest {
|
||||
|
||||
/// Request to configure KMS with Vault Transit backend
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigureVaultTransitKmsRequest {
|
||||
/// Vault server URL
|
||||
pub address: String,
|
||||
/// Authentication method
|
||||
#[serde(deserialize_with = "deserialize_strict_vault_auth_method")]
|
||||
pub auth_method: VaultAuthMethod,
|
||||
/// Vault namespace (Vault Enterprise, optional)
|
||||
pub namespace: Option<String>,
|
||||
@@ -165,11 +170,35 @@ pub struct ConfigureKmsResponse {
|
||||
|
||||
/// Request to start KMS service
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StartKmsRequest {
|
||||
/// Whether to force start (restart if already running)
|
||||
pub force: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
enum StrictVaultAuthMethod {
|
||||
Token { token: String },
|
||||
AppRole { role_id: String, secret_id: String },
|
||||
}
|
||||
|
||||
impl From<StrictVaultAuthMethod> for VaultAuthMethod {
|
||||
fn from(value: StrictVaultAuthMethod) -> Self {
|
||||
match value {
|
||||
StrictVaultAuthMethod::Token { token } => Self::Token { token },
|
||||
StrictVaultAuthMethod::AppRole { role_id, secret_id } => Self::AppRole { role_id, secret_id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_strict_vault_auth_method<'de, D>(deserializer: D) -> Result<VaultAuthMethod, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
StrictVaultAuthMethod::deserialize(deserializer).map(Into::into)
|
||||
}
|
||||
|
||||
/// KMS start response
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StartKmsResponse {
|
||||
@@ -566,6 +595,39 @@ mod tests {
|
||||
assert!(request.to_kms_config().validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_configure_request_rejects_unknown_fields() {
|
||||
let raw = serde_json::json!({
|
||||
"backend_type": "local",
|
||||
"key_dir": "./target/kms-key-dir",
|
||||
"unexpected_field": true
|
||||
});
|
||||
|
||||
let err = serde_json::from_value::<ConfigureKmsRequest>(raw).expect_err("unknown configure field should fail");
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
|
||||
let raw = serde_json::json!({
|
||||
"backend_type": "vault",
|
||||
"address": "http://127.0.0.1:8200",
|
||||
"auth_method": {
|
||||
"Token": {
|
||||
"token": "dev-root-token",
|
||||
"unexpected_field": true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let err = serde_json::from_value::<ConfigureKmsRequest>(raw).expect_err("unknown auth field should fail");
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_request_rejects_unknown_fields() {
|
||||
let err = serde_json::from_str::<StartKmsRequest>(r#"{"force":true,"unexpected_field":true}"#)
|
||||
.expect_err("unknown start field should fail");
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vault_transit_summary_reports_backend_details() {
|
||||
let config = KmsConfig {
|
||||
|
||||
@@ -34,6 +34,7 @@ use tracing::{error, info};
|
||||
use urlencoding;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CreateKmsKeyRequest {
|
||||
pub key_usage: Option<KeyUsage>,
|
||||
pub description: Option<String>,
|
||||
@@ -49,6 +50,7 @@ pub struct CreateKmsKeyResponse {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CreateKeyApiRequest {
|
||||
pub key_usage: Option<KeyUsage>,
|
||||
pub description: Option<String>,
|
||||
@@ -74,6 +76,7 @@ pub struct ListKeysApiResponse {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct GenerateDataKeyApiRequest {
|
||||
pub key_id: String,
|
||||
pub key_spec: KeySpec,
|
||||
@@ -321,6 +324,7 @@ impl Operation for DescribeKeyHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
CancelKmsKeyDeletionRequest, CreateKeyApiRequest, CreateKmsKeyRequest, DeleteKmsKeyRequest, GenerateDataKeyApiRequest,
|
||||
extract_key_id, kms_create_key_actions, kms_delete_key_actions, kms_describe_key_actions, kms_generate_data_key_actions,
|
||||
kms_list_keys_actions,
|
||||
};
|
||||
@@ -400,6 +404,23 @@ mod tests {
|
||||
assert_has_action(&kms_generate_data_key_actions(), Action::KmsAction(KmsAction::GenerateDataKeyAction));
|
||||
assert_has_action(&kms_delete_key_actions(), Action::KmsAction(KmsAction::DeleteKeyAction));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kms_key_request_bodies_reject_unknown_fields() {
|
||||
let cases = [
|
||||
serde_json::from_str::<CreateKeyApiRequest>(r#"{"unexpected_field":true}"#).map(|_| ()),
|
||||
serde_json::from_str::<CreateKmsKeyRequest>(r#"{"unexpected_field":true}"#).map(|_| ()),
|
||||
serde_json::from_str::<GenerateDataKeyApiRequest>(r#"{"key_id":"key","key_spec":"Aes256","unexpected_field":true}"#)
|
||||
.map(|_| ()),
|
||||
serde_json::from_str::<DeleteKmsKeyRequest>(r#"{"key_id":"key","unexpected_field":true}"#).map(|_| ()),
|
||||
serde_json::from_str::<CancelKmsKeyDeletionRequest>(r#"{"key_id":"key","unexpected_field":true}"#).map(|_| ()),
|
||||
];
|
||||
|
||||
for result in cases {
|
||||
let err = result.expect_err("unknown KMS key request field should fail");
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List KMS keys (legacy endpoint)
|
||||
@@ -650,6 +671,7 @@ impl Operation for CreateKmsKeyHandler {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DeleteKmsKeyRequest {
|
||||
pub key_id: String,
|
||||
pub pending_window_in_days: Option<u32>,
|
||||
@@ -801,6 +823,7 @@ impl Operation for DeleteKmsKeyHandler {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CancelKmsKeyDeletionRequest {
|
||||
pub key_id: String,
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ struct OidcValidationResponse {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct OidcConfigUpsertRequest {
|
||||
enabled: bool,
|
||||
display_name: String,
|
||||
@@ -210,7 +210,7 @@ impl Default for OidcConfigUpsertRequest {
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(default)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct OidcConfigValidateRequest {
|
||||
provider_id: String,
|
||||
enabled: bool,
|
||||
@@ -1244,6 +1244,26 @@ mod tests {
|
||||
assert_eq!(config.roles_claim, OIDC_DEFAULT_ROLES_CLAIM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oidc_config_upsert_request_rejects_unknown_fields() {
|
||||
let err = serde_json::from_str::<OidcConfigUpsertRequest>(
|
||||
r#"{"config_url":"https://example.com/.well-known/openid-configuration","client_id":"client","unexpected_field":true}"#,
|
||||
)
|
||||
.expect_err("unknown upsert field should fail");
|
||||
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oidc_config_validate_request_rejects_unknown_fields() {
|
||||
let err = serde_json::from_str::<OidcConfigValidateRequest>(
|
||||
r#"{"provider_id":"default","config_url":"https://example.com/.well-known/openid-configuration","client_id":"client","unexpected_field":true}"#,
|
||||
)
|
||||
.expect_err("unknown validate field should fail");
|
||||
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_provider_config_uses_custom_roles_claim() {
|
||||
let req = OidcConfigUpsertRequest {
|
||||
|
||||
@@ -273,12 +273,14 @@ struct ResolvedPluginInstanceTarget {
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct KeyValue {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct PluginInstanceBody {
|
||||
key_values: Vec<KeyValue>,
|
||||
}
|
||||
@@ -852,7 +854,7 @@ impl Operation for DeletePluginInstanceHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
PluginContractDomain, PluginInstanceFilters, collect_diagnostic_counts, collect_instance_diagnostics,
|
||||
PluginContractDomain, PluginInstanceBody, PluginInstanceFilters, collect_diagnostic_counts, collect_instance_diagnostics,
|
||||
extract_plugin_instance_filters, filter_plugin_instances, map_instance, paginate_plugin_instances, parse_bool_filter,
|
||||
parse_instance_status, parse_limit_filter, parse_plugin_contract_domain, parse_plugin_instance_diagnostic_code,
|
||||
parse_plugin_instance_id, parse_plugin_instance_source, resolve_plugin_instance_target,
|
||||
@@ -1068,6 +1070,19 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_instance_body_rejects_unknown_fields() {
|
||||
let err = serde_json::from_str::<PluginInstanceBody>(r#"{"key_values":[],"unexpected_field":true}"#)
|
||||
.expect_err("unknown plugin instance body field should fail");
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
|
||||
let err = serde_json::from_str::<PluginInstanceBody>(
|
||||
r#"{"key_values":[{"key":"endpoint","value":"http://example.com","extra":true}]}"#,
|
||||
)
|
||||
.expect_err("unknown plugin instance key-value field should fail");
|
||||
assert!(err.to_string().contains("unknown field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_id_is_stable_and_lowercases_instance_segment() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user