diff --git a/crates/kms/src/api_types.rs b/crates/kms/src/api_types.rs index 1921ec9de..5a5ac8531 100644 --- a/crates/kms/src/api_types.rs +++ b/crates/kms/src/api_types.rs @@ -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, @@ -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, @@ -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, } +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +enum StrictVaultAuthMethod { + Token { token: String }, + AppRole { role_id: String, secret_id: String }, +} + +impl From 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 +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::(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::(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::(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 { diff --git a/rustfs/src/admin/handlers/kms_keys.rs b/rustfs/src/admin/handlers/kms_keys.rs index 7b5064737..dc61980c7 100644 --- a/rustfs/src/admin/handlers/kms_keys.rs +++ b/rustfs/src/admin/handlers/kms_keys.rs @@ -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, pub description: Option, @@ -49,6 +50,7 @@ pub struct CreateKmsKeyResponse { } #[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct CreateKeyApiRequest { pub key_usage: Option, pub description: Option, @@ -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::(r#"{"unexpected_field":true}"#).map(|_| ()), + serde_json::from_str::(r#"{"unexpected_field":true}"#).map(|_| ()), + serde_json::from_str::(r#"{"key_id":"key","key_spec":"Aes256","unexpected_field":true}"#) + .map(|_| ()), + serde_json::from_str::(r#"{"key_id":"key","unexpected_field":true}"#).map(|_| ()), + serde_json::from_str::(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, @@ -801,6 +823,7 @@ impl Operation for DeleteKmsKeyHandler { } #[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct CancelKmsKeyDeletionRequest { pub key_id: String, } diff --git a/rustfs/src/admin/handlers/oidc.rs b/rustfs/src/admin/handlers/oidc.rs index 120bc1636..cf33f5d48 100644 --- a/rustfs/src/admin/handlers/oidc.rs +++ b/rustfs/src/admin/handlers/oidc.rs @@ -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::( + 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::( + 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 { diff --git a/rustfs/src/admin/handlers/plugins_instances.rs b/rustfs/src/admin/handlers/plugins_instances.rs index 5fe3bbbc3..8103129f2 100644 --- a/rustfs/src/admin/handlers/plugins_instances.rs +++ b/rustfs/src/admin/handlers/plugins_instances.rs @@ -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, } @@ -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::(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::( + 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!(