mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-09 22:59:59 +00:00
feat(admin): add audit target APIs and harden target source handling (#2350)
Signed-off-by: houseme <housemecn@gmail.com> Co-authored-by: 安正超 <anzhengchao@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
This commit is contained in:
@@ -16,8 +16,8 @@ use crate::config::{KV, KVS};
|
||||
use rustfs_config::{
|
||||
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
||||
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN,
|
||||
WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT, WEBHOOK_MAX_RETRY,
|
||||
WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_RETRY_INTERVAL,
|
||||
WEBHOOK_BATCH_SIZE, WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_HTTP_TIMEOUT,
|
||||
WEBHOOK_MAX_RETRY, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT, WEBHOOK_RETRY_INTERVAL, WEBHOOK_SKIP_TLS_VERIFY,
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -51,6 +51,16 @@ pub static DEFAULT_AUDIT_WEBHOOK_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||
value: "".to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: WEBHOOK_CLIENT_CA.to_owned(),
|
||||
value: "".to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: WEBHOOK_SKIP_TLS_VERIFY.to_owned(),
|
||||
value: EnableState::Off.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: WEBHOOK_BATCH_SIZE.to_owned(),
|
||||
value: "1".to_owned(),
|
||||
@@ -81,6 +91,11 @@ pub static DEFAULT_AUDIT_WEBHOOK_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||
value: "5s".to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: COMMENT_KEY.to_owned(),
|
||||
value: "".to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
])
|
||||
});
|
||||
|
||||
|
||||
@@ -12,12 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::config::{Config, GLOBAL_STORAGE_CLASS, KVS, oidc, storageclass};
|
||||
use crate::config::{Config, GLOBAL_STORAGE_CLASS, KVS, audit, notify, oidc, storageclass};
|
||||
use crate::disk::{MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::global::is_first_cluster_node_local;
|
||||
use crate::store_api::{ObjectInfo, ObjectOptions, PutObjReader, StorageAPI};
|
||||
use http::HeaderMap;
|
||||
use rustfs_config::audit::{AUDIT_MQTT_KEYS, AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_KEYS, AUDIT_WEBHOOK_SUB_SYS};
|
||||
use rustfs_config::notify::{NOTIFY_MQTT_KEYS, NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_KEYS, NOTIFY_WEBHOOK_SUB_SYS};
|
||||
use rustfs_config::oidc::{IDENTITY_OPENID_KEYS, IDENTITY_OPENID_SUB_SYS, OIDC_REDIRECT_URI_DYNAMIC};
|
||||
use rustfs_config::{COMMENT_KEY, DEFAULT_DELIMITER, ENABLE_KEY, EnableState, RUSTFS_REGION};
|
||||
use rustfs_utils::path::SLASH_SEPARATOR;
|
||||
@@ -261,6 +263,159 @@ fn apply_external_oidc_map(cfg: &mut Config, root: &Map<String, Value>) -> bool
|
||||
applied
|
||||
}
|
||||
|
||||
fn parse_notify_scalar_value(key: &str, value: &Value) -> Option<String> {
|
||||
match value {
|
||||
Value::String(v) => Some(v.trim().to_string()),
|
||||
Value::Bool(v) if key == ENABLE_KEY || key == rustfs_config::WEBHOOK_SKIP_TLS_VERIFY => Some(if *v {
|
||||
EnableState::On.to_string()
|
||||
} else {
|
||||
EnableState::Off.to_string()
|
||||
}),
|
||||
Value::Bool(v) => Some(v.to_string()),
|
||||
Value::Number(v) => Some(v.to_string()),
|
||||
Value::Null => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_notify_instance_object(instance: &Map<String, Value>, valid_keys: &[&str]) -> KVS {
|
||||
let mut kvs = KVS::new();
|
||||
|
||||
for (key, value) in instance {
|
||||
if !valid_keys.contains(&key.as_str()) || key == COMMENT_KEY {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parsed) = parse_notify_scalar_value(key, value) {
|
||||
kvs.insert(key.clone(), parsed);
|
||||
}
|
||||
}
|
||||
|
||||
kvs
|
||||
}
|
||||
|
||||
fn decode_notify_instance_value(value: &Value, valid_keys: &[&str]) -> Option<KVS> {
|
||||
match value {
|
||||
Value::Object(instance) => Some(decode_notify_instance_object(instance, valid_keys)),
|
||||
Value::Array(_) => serde_json::from_value::<KVS>(value.clone()).ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_notify_instance_shorthand(section: &Map<String, Value>, valid_keys: &[&str]) -> bool {
|
||||
section
|
||||
.iter()
|
||||
.any(|(key, value)| valid_keys.contains(&key.as_str()) && parse_notify_scalar_value(key, value).is_some())
|
||||
}
|
||||
|
||||
fn apply_external_notify_section(
|
||||
cfg: &mut Config,
|
||||
notify_obj: &Map<String, Value>,
|
||||
external_key: &str,
|
||||
subsystem_key: &str,
|
||||
default_kvs: &KVS,
|
||||
valid_keys: &[&str],
|
||||
) -> bool {
|
||||
let Some(Value::Object(section_obj)) = notify_obj.get(external_key).or_else(|| notify_obj.get(subsystem_key)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if section_obj.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let subsystem = cfg.0.entry(subsystem_key.to_string()).or_default();
|
||||
let mut applied = false;
|
||||
|
||||
if is_notify_instance_shorthand(section_obj, valid_keys) {
|
||||
let kvs = decode_notify_instance_object(section_obj, valid_keys);
|
||||
if !kvs.is_empty() {
|
||||
let mut merged = default_kvs.clone();
|
||||
merged.extend(kvs);
|
||||
subsystem.insert(DEFAULT_DELIMITER.to_string(), merged);
|
||||
applied = true;
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
for (raw_instance, value) in section_obj {
|
||||
let Some(mut kvs) = decode_notify_instance_value(value, valid_keys) else {
|
||||
continue;
|
||||
};
|
||||
if kvs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let instance_key = if raw_instance == "default" {
|
||||
DEFAULT_DELIMITER.to_string()
|
||||
} else {
|
||||
raw_instance.to_string()
|
||||
};
|
||||
|
||||
if instance_key == DEFAULT_DELIMITER {
|
||||
let mut merged = default_kvs.clone();
|
||||
merged.extend(kvs);
|
||||
kvs = merged;
|
||||
}
|
||||
|
||||
subsystem.insert(instance_key, kvs);
|
||||
applied = true;
|
||||
}
|
||||
|
||||
applied
|
||||
}
|
||||
|
||||
fn apply_external_notify_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
||||
let Some(Value::Object(notify_obj)) = root.get("notify") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut applied = false;
|
||||
applied |= apply_external_notify_section(
|
||||
cfg,
|
||||
notify_obj,
|
||||
"webhook",
|
||||
NOTIFY_WEBHOOK_SUB_SYS,
|
||||
¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS,
|
||||
NOTIFY_WEBHOOK_KEYS,
|
||||
);
|
||||
applied |= apply_external_notify_section(
|
||||
cfg,
|
||||
notify_obj,
|
||||
"mqtt",
|
||||
NOTIFY_MQTT_SUB_SYS,
|
||||
¬ify::DEFAULT_NOTIFY_MQTT_KVS,
|
||||
NOTIFY_MQTT_KEYS,
|
||||
);
|
||||
applied
|
||||
}
|
||||
|
||||
fn apply_external_audit_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
||||
let audit_root = root.get("audit").or_else(|| root.get("logger")).and_then(Value::as_object);
|
||||
let Some(audit_obj) = audit_root else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let mut applied = false;
|
||||
applied |= apply_external_notify_section(
|
||||
cfg,
|
||||
audit_obj,
|
||||
"webhook",
|
||||
AUDIT_WEBHOOK_SUB_SYS,
|
||||
&audit::DEFAULT_AUDIT_WEBHOOK_KVS,
|
||||
AUDIT_WEBHOOK_KEYS,
|
||||
);
|
||||
applied |= apply_external_notify_section(
|
||||
cfg,
|
||||
audit_obj,
|
||||
"mqtt",
|
||||
AUDIT_MQTT_SUB_SYS,
|
||||
&audit::DEFAULT_AUDIT_MQTT_KVS,
|
||||
AUDIT_MQTT_KEYS,
|
||||
);
|
||||
applied
|
||||
}
|
||||
|
||||
fn apply_external_storage_class_map(cfg: &mut Config, root: &Map<String, Value>) -> bool {
|
||||
let sc = root.get("storageclass").or_else(|| root.get("storage_class"));
|
||||
let Some(Value::Object(sc_obj)) = sc else {
|
||||
@@ -305,8 +460,10 @@ fn decode_server_config_blob(data: &[u8]) -> Result<Config> {
|
||||
let mut cfg = Config::new();
|
||||
let has_storage = apply_external_storage_class_map(&mut cfg, &root);
|
||||
let has_oidc = apply_external_oidc_map(&mut cfg, &root);
|
||||
let has_notify = apply_external_notify_map(&mut cfg, &root);
|
||||
let has_audit = apply_external_audit_map(&mut cfg, &root);
|
||||
let has_header = root.contains_key("version") || root.contains_key("region") || root.contains_key("credential");
|
||||
if !has_storage && !has_oidc && !has_header {
|
||||
if !has_storage && !has_oidc && !has_notify && !has_audit && !has_header {
|
||||
return Err(Error::other("unrecognized external server config shape"));
|
||||
}
|
||||
Ok(cfg)
|
||||
@@ -449,6 +606,139 @@ fn build_semantic_oidc_object(cfg: &Config) -> Map<String, Value> {
|
||||
oidc_obj
|
||||
}
|
||||
|
||||
fn is_notify_bool_key(key: &str) -> bool {
|
||||
key == ENABLE_KEY || key == rustfs_config::WEBHOOK_SKIP_TLS_VERIFY
|
||||
}
|
||||
|
||||
fn encode_notify_scalar_value(key: &str, value: &str) -> Value {
|
||||
if is_notify_bool_key(key) {
|
||||
if let Ok(state) = value.parse::<EnableState>() {
|
||||
return Value::Bool(state.is_enabled());
|
||||
}
|
||||
if let Ok(boolean) = value.parse::<bool>() {
|
||||
return Value::Bool(boolean);
|
||||
}
|
||||
}
|
||||
|
||||
Value::String(value.to_string())
|
||||
}
|
||||
|
||||
fn is_hidden_if_empty(default_kvs: &KVS, key: &str) -> bool {
|
||||
default_kvs
|
||||
.0
|
||||
.iter()
|
||||
.find(|kv| kv.key == key)
|
||||
.map(|kv| kv.hidden_if_empty)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn build_notify_instance_diff_object(kvs: &KVS, baseline: &KVS, valid_keys: &[&str], default_kvs: &KVS) -> Map<String, Value> {
|
||||
let mut instance = Map::new();
|
||||
|
||||
for key in valid_keys {
|
||||
if *key == COMMENT_KEY {
|
||||
continue;
|
||||
}
|
||||
|
||||
let baseline_value = baseline.lookup(key).unwrap_or_default();
|
||||
let effective_value = kvs.lookup(key).unwrap_or_else(|| baseline_value.clone());
|
||||
|
||||
if effective_value == baseline_value {
|
||||
continue;
|
||||
}
|
||||
|
||||
if effective_value.trim().is_empty() && baseline_value.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_hidden_if_empty(default_kvs, key) && effective_value.trim().is_empty() && baseline_value.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
instance.insert((*key).to_string(), encode_notify_scalar_value(key, &effective_value));
|
||||
}
|
||||
|
||||
instance
|
||||
}
|
||||
|
||||
fn merged_notify_default_kvs(subsystem: &HashMap<String, KVS>, default_kvs: &KVS) -> KVS {
|
||||
let mut merged = default_kvs.clone();
|
||||
if let Some(kvs) = subsystem.get(DEFAULT_DELIMITER) {
|
||||
merged.extend(kvs.clone());
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
fn build_notify_subsystem_object(
|
||||
cfg: &Config,
|
||||
subsystem_key: &str,
|
||||
default_kvs: &KVS,
|
||||
valid_keys: &[&str],
|
||||
) -> Map<String, Value> {
|
||||
let Some(subsystem) = cfg.0.get(subsystem_key) else {
|
||||
return Map::new();
|
||||
};
|
||||
|
||||
let effective_default = merged_notify_default_kvs(subsystem, default_kvs);
|
||||
let mut subsystem_obj = Map::new();
|
||||
|
||||
if let Some(default_instance) = subsystem.get(DEFAULT_DELIMITER) {
|
||||
let default_obj = build_notify_instance_diff_object(default_instance, default_kvs, valid_keys, default_kvs);
|
||||
if !default_obj.is_empty() {
|
||||
subsystem_obj.insert("default".to_string(), Value::Object(default_obj));
|
||||
}
|
||||
}
|
||||
|
||||
let mut instances = subsystem
|
||||
.iter()
|
||||
.filter(|(instance_key, _)| instance_key.as_str() != DEFAULT_DELIMITER)
|
||||
.collect::<Vec<_>>();
|
||||
instances.sort_by(|(lhs, _), (rhs, _)| lhs.cmp(rhs));
|
||||
|
||||
for (instance_key, kvs) in instances {
|
||||
let instance_obj = build_notify_instance_diff_object(kvs, &effective_default, valid_keys, default_kvs);
|
||||
if !instance_obj.is_empty() {
|
||||
subsystem_obj.insert(instance_key.clone(), Value::Object(instance_obj));
|
||||
}
|
||||
}
|
||||
|
||||
subsystem_obj
|
||||
}
|
||||
|
||||
fn build_notify_object(cfg: &Config) -> Map<String, Value> {
|
||||
let mut notify_obj = Map::new();
|
||||
|
||||
let webhook_obj =
|
||||
build_notify_subsystem_object(cfg, NOTIFY_WEBHOOK_SUB_SYS, ¬ify::DEFAULT_NOTIFY_WEBHOOK_KVS, NOTIFY_WEBHOOK_KEYS);
|
||||
if !webhook_obj.is_empty() {
|
||||
notify_obj.insert("webhook".to_string(), Value::Object(webhook_obj));
|
||||
}
|
||||
|
||||
let mqtt_obj = build_notify_subsystem_object(cfg, NOTIFY_MQTT_SUB_SYS, ¬ify::DEFAULT_NOTIFY_MQTT_KVS, NOTIFY_MQTT_KEYS);
|
||||
if !mqtt_obj.is_empty() {
|
||||
notify_obj.insert("mqtt".to_string(), Value::Object(mqtt_obj));
|
||||
}
|
||||
|
||||
notify_obj
|
||||
}
|
||||
|
||||
fn build_audit_object(cfg: &Config) -> Map<String, Value> {
|
||||
let mut audit_obj = Map::new();
|
||||
|
||||
let webhook_obj =
|
||||
build_notify_subsystem_object(cfg, AUDIT_WEBHOOK_SUB_SYS, &audit::DEFAULT_AUDIT_WEBHOOK_KVS, AUDIT_WEBHOOK_KEYS);
|
||||
if !webhook_obj.is_empty() {
|
||||
audit_obj.insert("webhook".to_string(), Value::Object(webhook_obj));
|
||||
}
|
||||
|
||||
let mqtt_obj = build_notify_subsystem_object(cfg, AUDIT_MQTT_SUB_SYS, &audit::DEFAULT_AUDIT_MQTT_KVS, AUDIT_MQTT_KEYS);
|
||||
if !mqtt_obj.is_empty() {
|
||||
audit_obj.insert("mqtt".to_string(), Value::Object(mqtt_obj));
|
||||
}
|
||||
|
||||
audit_obj
|
||||
}
|
||||
|
||||
fn encode_server_config_blob(cfg: &Config, seed: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
let mut root = seed.and_then(parse_object_seed).unwrap_or_default();
|
||||
|
||||
@@ -478,6 +768,73 @@ fn encode_server_config_blob(cfg: &Config, seed: Option<&[u8]>) -> Result<Vec<u8
|
||||
root.remove(IDENTITY_OPENID_SUB_SYS);
|
||||
}
|
||||
|
||||
let mut notify_obj = match root.remove("notify") {
|
||||
Some(Value::Object(v)) => v,
|
||||
_ => Map::new(),
|
||||
};
|
||||
let rendered_notify = build_notify_object(cfg);
|
||||
match rendered_notify.get("webhook") {
|
||||
Some(Value::Object(v)) => {
|
||||
notify_obj.insert("webhook".to_string(), Value::Object(v.clone()));
|
||||
notify_obj.remove(NOTIFY_WEBHOOK_SUB_SYS);
|
||||
}
|
||||
_ => {
|
||||
notify_obj.remove("webhook");
|
||||
notify_obj.remove(NOTIFY_WEBHOOK_SUB_SYS);
|
||||
}
|
||||
}
|
||||
match rendered_notify.get("mqtt") {
|
||||
Some(Value::Object(v)) => {
|
||||
notify_obj.insert("mqtt".to_string(), Value::Object(v.clone()));
|
||||
notify_obj.remove(NOTIFY_MQTT_SUB_SYS);
|
||||
}
|
||||
_ => {
|
||||
notify_obj.remove("mqtt");
|
||||
notify_obj.remove(NOTIFY_MQTT_SUB_SYS);
|
||||
}
|
||||
}
|
||||
if notify_obj.is_empty() {
|
||||
root.remove("notify");
|
||||
} else {
|
||||
root.insert("notify".to_string(), Value::Object(notify_obj));
|
||||
}
|
||||
root.remove(NOTIFY_WEBHOOK_SUB_SYS);
|
||||
root.remove(NOTIFY_MQTT_SUB_SYS);
|
||||
|
||||
let mut logger_obj = match root.remove("logger") {
|
||||
Some(Value::Object(v)) => v,
|
||||
_ => Map::new(),
|
||||
};
|
||||
let rendered_audit = build_audit_object(cfg);
|
||||
match rendered_audit.get("webhook") {
|
||||
Some(Value::Object(v)) => {
|
||||
logger_obj.insert("webhook".to_string(), Value::Object(v.clone()));
|
||||
logger_obj.remove(AUDIT_WEBHOOK_SUB_SYS);
|
||||
}
|
||||
_ => {
|
||||
logger_obj.remove("webhook");
|
||||
logger_obj.remove(AUDIT_WEBHOOK_SUB_SYS);
|
||||
}
|
||||
}
|
||||
match rendered_audit.get("mqtt") {
|
||||
Some(Value::Object(v)) => {
|
||||
logger_obj.insert("mqtt".to_string(), Value::Object(v.clone()));
|
||||
logger_obj.remove(AUDIT_MQTT_SUB_SYS);
|
||||
}
|
||||
_ => {
|
||||
logger_obj.remove("mqtt");
|
||||
logger_obj.remove(AUDIT_MQTT_SUB_SYS);
|
||||
}
|
||||
}
|
||||
if logger_obj.is_empty() {
|
||||
root.remove("logger");
|
||||
} else {
|
||||
root.insert("logger".to_string(), Value::Object(logger_obj));
|
||||
}
|
||||
root.remove("audit");
|
||||
root.remove(AUDIT_WEBHOOK_SUB_SYS);
|
||||
root.remove(AUDIT_MQTT_SUB_SYS);
|
||||
|
||||
Ok(serde_json::to_vec(&Value::Object(root))?)
|
||||
}
|
||||
|
||||
@@ -496,6 +853,8 @@ fn is_standard_object_server_config(data: &[u8]) -> bool {
|
||||
fn configs_semantically_equal(lhs: &Config, rhs: &Config) -> bool {
|
||||
build_storageclass_object(lhs) == build_storageclass_object(rhs)
|
||||
&& build_semantic_oidc_object(lhs) == build_semantic_oidc_object(rhs)
|
||||
&& build_notify_object(lhs) == build_notify_object(rhs)
|
||||
&& build_audit_object(lhs) == build_audit_object(rhs)
|
||||
}
|
||||
|
||||
fn is_object_not_found(err: &Error) -> bool {
|
||||
@@ -712,7 +1071,7 @@ mod tests {
|
||||
configs_semantically_equal, decode_server_config_blob, encode_server_config_blob, is_standard_object_server_config,
|
||||
read_config_with_metadata, storage_class_kvs_mut,
|
||||
};
|
||||
use crate::config::{Config, oidc};
|
||||
use crate::config::{Config, audit, notify, oidc};
|
||||
use crate::disk::endpoint::Endpoint;
|
||||
use crate::endpoints::SetupType;
|
||||
use crate::error::{Error, Result};
|
||||
@@ -725,6 +1084,8 @@ mod tests {
|
||||
ObjectOptions, ObjectToDelete, PartInfo, PutObjReader, StorageAPI, WalkOptions,
|
||||
};
|
||||
use http::HeaderMap;
|
||||
use rustfs_config::audit::{AUDIT_MQTT_SUB_SYS, AUDIT_WEBHOOK_SUB_SYS};
|
||||
use rustfs_config::notify::{NOTIFY_MQTT_SUB_SYS, NOTIFY_WEBHOOK_SUB_SYS};
|
||||
use rustfs_config::oidc::IDENTITY_OPENID_SUB_SYS;
|
||||
use rustfs_config::{DEFAULT_DELIMITER, ENABLE_KEY, EnableState};
|
||||
use rustfs_filemeta::FileInfo;
|
||||
@@ -1332,6 +1693,152 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_server_config_reads_notify_targets() {
|
||||
let input = r#"{
|
||||
"version":"33",
|
||||
"storageclass":{"standard":"EC:2","rrs":"EC:1"},
|
||||
"notify":{
|
||||
"webhook":{
|
||||
"primary":{
|
||||
"enable":true,
|
||||
"endpoint":"https://example.com/hook",
|
||||
"queue_dir":"/tmp/webhook-queue"
|
||||
}
|
||||
},
|
||||
"mqtt":{
|
||||
"default":{
|
||||
"enable":true,
|
||||
"topic":"events"
|
||||
},
|
||||
"analytics":{
|
||||
"enable":true,
|
||||
"broker":"tcp://127.0.0.1:1883",
|
||||
"topic":"events",
|
||||
"queue_dir":""
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let cfg = decode_server_config_blob(input.as_bytes()).expect("decode should succeed");
|
||||
|
||||
let webhook = cfg
|
||||
.get_value(NOTIFY_WEBHOOK_SUB_SYS, "primary")
|
||||
.expect("webhook target should be decoded");
|
||||
assert_eq!(webhook.get(ENABLE_KEY), EnableState::On.to_string());
|
||||
assert_eq!(webhook.get(rustfs_config::WEBHOOK_ENDPOINT), "https://example.com/hook");
|
||||
assert_eq!(webhook.get(rustfs_config::WEBHOOK_QUEUE_DIR), "/tmp/webhook-queue");
|
||||
|
||||
let mqtt_default = cfg
|
||||
.get_value(NOTIFY_MQTT_SUB_SYS, DEFAULT_DELIMITER)
|
||||
.expect("mqtt default should be decoded");
|
||||
assert_eq!(mqtt_default.get(ENABLE_KEY), EnableState::On.to_string());
|
||||
assert_eq!(mqtt_default.get(rustfs_config::MQTT_TOPIC), "events");
|
||||
assert_eq!(
|
||||
mqtt_default.get(rustfs_config::MQTT_QUEUE_DIR),
|
||||
notify::DEFAULT_NOTIFY_MQTT_KVS.get(rustfs_config::MQTT_QUEUE_DIR)
|
||||
);
|
||||
|
||||
let mqtt = cfg
|
||||
.get_value(NOTIFY_MQTT_SUB_SYS, "analytics")
|
||||
.expect("mqtt target should be decoded");
|
||||
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER), "tcp://127.0.0.1:1883");
|
||||
assert_eq!(mqtt.get(rustfs_config::MQTT_QUEUE_DIR), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_server_config_reads_notify_shorthand_default() {
|
||||
let input = r#"{
|
||||
"version":"33",
|
||||
"storageclass":{"standard":"EC:2","rrs":"EC:1"},
|
||||
"notify":{
|
||||
"webhook":{
|
||||
"enable":true,
|
||||
"endpoint":"https://example.com/shorthand"
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let cfg = decode_server_config_blob(input.as_bytes()).expect("decode should succeed");
|
||||
let webhook_default = cfg
|
||||
.get_value(NOTIFY_WEBHOOK_SUB_SYS, DEFAULT_DELIMITER)
|
||||
.expect("default webhook config should be decoded");
|
||||
assert_eq!(webhook_default.get(ENABLE_KEY), EnableState::On.to_string());
|
||||
assert_eq!(webhook_default.get(rustfs_config::WEBHOOK_ENDPOINT), "https://example.com/shorthand");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_server_config_keeps_instance_named_like_field() {
|
||||
let input = r#"{
|
||||
"version":"33",
|
||||
"storageclass":{"standard":"EC:2","rrs":"EC:1"},
|
||||
"notify":{
|
||||
"webhook":{
|
||||
"enable":{
|
||||
"enable":true,
|
||||
"endpoint":"https://example.com/instance-enable"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let cfg = decode_server_config_blob(input.as_bytes()).expect("decode should succeed");
|
||||
let named = cfg
|
||||
.get_value(NOTIFY_WEBHOOK_SUB_SYS, "enable")
|
||||
.expect("instance named 'enable' should be decoded");
|
||||
assert_eq!(named.get(ENABLE_KEY), EnableState::On.to_string());
|
||||
assert_eq!(named.get(rustfs_config::WEBHOOK_ENDPOINT), "https://example.com/instance-enable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_server_config_reads_audit_targets() {
|
||||
let input = r#"{
|
||||
"version":"33",
|
||||
"storageclass":{"standard":"EC:2","rrs":"EC:1"},
|
||||
"logger":{
|
||||
"webhook":{
|
||||
"primary":{
|
||||
"enable":true,
|
||||
"endpoint":"https://example.com/audit-hook",
|
||||
"queue_dir":"/tmp/audit-queue"
|
||||
}
|
||||
},
|
||||
"mqtt":{
|
||||
"default":{
|
||||
"enable":true,
|
||||
"topic":"audit-events"
|
||||
},
|
||||
"analytics":{
|
||||
"enable":true,
|
||||
"broker":"tcp://127.0.0.1:1883",
|
||||
"topic":"audit-events"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let cfg = decode_server_config_blob(input.as_bytes()).expect("decode should succeed");
|
||||
|
||||
let webhook = cfg
|
||||
.get_value(AUDIT_WEBHOOK_SUB_SYS, "primary")
|
||||
.expect("audit webhook target should be decoded");
|
||||
assert_eq!(webhook.get(ENABLE_KEY), EnableState::On.to_string());
|
||||
assert_eq!(webhook.get(rustfs_config::WEBHOOK_ENDPOINT), "https://example.com/audit-hook");
|
||||
assert_eq!(webhook.get(rustfs_config::WEBHOOK_QUEUE_DIR), "/tmp/audit-queue");
|
||||
|
||||
let mqtt_default = cfg
|
||||
.get_value(AUDIT_MQTT_SUB_SYS, DEFAULT_DELIMITER)
|
||||
.expect("audit mqtt default should be decoded");
|
||||
assert_eq!(mqtt_default.get(ENABLE_KEY), EnableState::On.to_string());
|
||||
assert_eq!(mqtt_default.get(rustfs_config::MQTT_TOPIC), "audit-events");
|
||||
|
||||
let mqtt = cfg
|
||||
.get_value(AUDIT_MQTT_SUB_SYS, "analytics")
|
||||
.expect("audit mqtt target should be decoded");
|
||||
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER), "tcp://127.0.0.1:1883");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_server_config_writes_external_object_shape() {
|
||||
let mut cfg = Config::new();
|
||||
@@ -1388,6 +1895,174 @@ mod tests {
|
||||
assert_eq!(default_provider.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_server_config_writes_notify_object_shape() {
|
||||
let mut cfg = Config::new();
|
||||
let mut webhook_section = std::collections::HashMap::new();
|
||||
webhook_section.insert(DEFAULT_DELIMITER.to_string(), notify::DEFAULT_NOTIFY_WEBHOOK_KVS.clone());
|
||||
webhook_section.insert(
|
||||
"primary".to_string(),
|
||||
crate::config::KVS(vec![
|
||||
crate::config::KV {
|
||||
key: ENABLE_KEY.to_string(),
|
||||
value: EnableState::On.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::WEBHOOK_ENDPOINT.to_string(),
|
||||
value: "https://example.com/hook".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::WEBHOOK_QUEUE_DIR.to_string(),
|
||||
value: "/tmp/webhook-queue".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
]),
|
||||
);
|
||||
cfg.0.insert(NOTIFY_WEBHOOK_SUB_SYS.to_string(), webhook_section);
|
||||
|
||||
let mut mqtt_default = notify::DEFAULT_NOTIFY_MQTT_KVS.clone();
|
||||
mqtt_default.insert(ENABLE_KEY.to_string(), EnableState::On.to_string());
|
||||
mqtt_default.insert(rustfs_config::MQTT_TOPIC.to_string(), "events".to_string());
|
||||
let mut mqtt_section = std::collections::HashMap::new();
|
||||
mqtt_section.insert(DEFAULT_DELIMITER.to_string(), mqtt_default);
|
||||
mqtt_section.insert(
|
||||
"analytics".to_string(),
|
||||
crate::config::KVS(vec![
|
||||
crate::config::KV {
|
||||
key: ENABLE_KEY.to_string(),
|
||||
value: EnableState::On.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::MQTT_BROKER.to_string(),
|
||||
value: "tcp://127.0.0.1:1883".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::MQTT_QUEUE_DIR.to_string(),
|
||||
value: "".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
]),
|
||||
);
|
||||
cfg.0.insert(NOTIFY_MQTT_SUB_SYS.to_string(), mqtt_section);
|
||||
|
||||
let out = encode_server_config_blob(&cfg, None).expect("encode should succeed");
|
||||
let v: Value = serde_json::from_slice(&out).expect("output should be json");
|
||||
let notify = v
|
||||
.get("notify")
|
||||
.and_then(Value::as_object)
|
||||
.expect("notify object should be present");
|
||||
let webhook = notify
|
||||
.get("webhook")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|targets| targets.get("primary"))
|
||||
.and_then(Value::as_object)
|
||||
.expect("webhook target should be encoded");
|
||||
assert_eq!(
|
||||
webhook.get(rustfs_config::WEBHOOK_ENDPOINT).and_then(Value::as_str),
|
||||
Some("https://example.com/hook")
|
||||
);
|
||||
assert_eq!(webhook.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
||||
|
||||
let mqtt_default = notify
|
||||
.get("mqtt")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|targets| targets.get("default"))
|
||||
.and_then(Value::as_object)
|
||||
.expect("mqtt default should be encoded");
|
||||
assert_eq!(mqtt_default.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
||||
assert_eq!(mqtt_default.get(rustfs_config::MQTT_TOPIC).and_then(Value::as_str), Some("events"));
|
||||
|
||||
let mqtt = notify
|
||||
.get("mqtt")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|targets| targets.get("analytics"))
|
||||
.and_then(Value::as_object)
|
||||
.expect("mqtt target should be encoded");
|
||||
assert_eq!(mqtt.get(rustfs_config::MQTT_BROKER).and_then(Value::as_str), Some("tcp://127.0.0.1:1883"));
|
||||
assert_eq!(mqtt.get(rustfs_config::MQTT_QUEUE_DIR).and_then(Value::as_str), Some(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_server_config_writes_audit_object_shape() {
|
||||
let mut cfg = Config::new();
|
||||
let mut webhook_section = std::collections::HashMap::new();
|
||||
webhook_section.insert(DEFAULT_DELIMITER.to_string(), audit::DEFAULT_AUDIT_WEBHOOK_KVS.clone());
|
||||
webhook_section.insert(
|
||||
"primary".to_string(),
|
||||
crate::config::KVS(vec![
|
||||
crate::config::KV {
|
||||
key: ENABLE_KEY.to_string(),
|
||||
value: EnableState::On.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::WEBHOOK_ENDPOINT.to_string(),
|
||||
value: "https://example.com/audit-hook".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::WEBHOOK_QUEUE_DIR.to_string(),
|
||||
value: "/tmp/audit-queue".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
]),
|
||||
);
|
||||
cfg.0.insert(AUDIT_WEBHOOK_SUB_SYS.to_string(), webhook_section);
|
||||
|
||||
let mut mqtt_default = audit::DEFAULT_AUDIT_MQTT_KVS.clone();
|
||||
mqtt_default.insert(ENABLE_KEY.to_string(), EnableState::On.to_string());
|
||||
mqtt_default.insert(rustfs_config::MQTT_TOPIC.to_string(), "audit-events".to_string());
|
||||
let mut mqtt_section = std::collections::HashMap::new();
|
||||
mqtt_section.insert(DEFAULT_DELIMITER.to_string(), mqtt_default);
|
||||
mqtt_section.insert(
|
||||
"analytics".to_string(),
|
||||
crate::config::KVS(vec![
|
||||
crate::config::KV {
|
||||
key: ENABLE_KEY.to_string(),
|
||||
value: EnableState::On.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
crate::config::KV {
|
||||
key: rustfs_config::MQTT_BROKER.to_string(),
|
||||
value: "tcp://127.0.0.1:1883".to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
]),
|
||||
);
|
||||
cfg.0.insert(AUDIT_MQTT_SUB_SYS.to_string(), mqtt_section);
|
||||
|
||||
let out = encode_server_config_blob(&cfg, None).expect("encode should succeed");
|
||||
let v: Value = serde_json::from_slice(&out).expect("output should be json");
|
||||
let logger = v
|
||||
.get("logger")
|
||||
.and_then(Value::as_object)
|
||||
.expect("logger object should be present");
|
||||
let webhook = logger
|
||||
.get("webhook")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|targets| targets.get("primary"))
|
||||
.and_then(Value::as_object)
|
||||
.expect("audit webhook target should be encoded");
|
||||
assert_eq!(
|
||||
webhook.get(rustfs_config::WEBHOOK_ENDPOINT).and_then(Value::as_str),
|
||||
Some("https://example.com/audit-hook")
|
||||
);
|
||||
assert_eq!(webhook.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
||||
|
||||
let mqtt_default = logger
|
||||
.get("mqtt")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|targets| targets.get("default"))
|
||||
.and_then(Value::as_object)
|
||||
.expect("audit mqtt default should be encoded");
|
||||
assert_eq!(mqtt_default.get(ENABLE_KEY).and_then(Value::as_bool), Some(true));
|
||||
assert_eq!(mqtt_default.get(rustfs_config::MQTT_TOPIC).and_then(Value::as_str), Some("audit-events"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_standard_object_server_config_detection() {
|
||||
let external = br#"{"version":"33","storageclass":{"standard":"EC:2","rrs":"EC:1"}}"#;
|
||||
@@ -1441,6 +2116,113 @@ mod tests {
|
||||
assert!(configs_semantically_equal(&lhs, &rhs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_configs_semantically_equal_accounts_for_notify() {
|
||||
let external = br#"{
|
||||
"version":"33",
|
||||
"storageclass":{"standard":"EC:2","rrs":"EC:1","optimize":"availability"},
|
||||
"notify":{
|
||||
"webhook":{
|
||||
"primary":{
|
||||
"enable":true,
|
||||
"endpoint":"https://example.com/hook"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let legacy = br#"{
|
||||
"storage_class":{"_":[
|
||||
{"key":"standard","value":"EC:2"},
|
||||
{"key":"rrs","value":"EC:1"},
|
||||
{"key":"optimize","value":"availability"}
|
||||
]},
|
||||
"notify_webhook":{
|
||||
"_":[
|
||||
{"key":"enable","value":"off"},
|
||||
{"key":"endpoint","value":""},
|
||||
{"key":"queue_limit","value":"100000"},
|
||||
{"key":"queue_dir","value":"/opt/rustfs/events"},
|
||||
{"key":"client_cert","value":""},
|
||||
{"key":"client_key","value":""},
|
||||
{"key":"comment","value":""},
|
||||
{"key":"client_ca","value":""},
|
||||
{"key":"skip_tls_verify","value":"off"}
|
||||
],
|
||||
"primary":[
|
||||
{"key":"enable","value":"on"},
|
||||
{"key":"endpoint","value":"https://example.com/hook"}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let lhs = decode_server_config_blob(external).expect("decode external");
|
||||
let rhs = decode_server_config_blob(legacy).expect("decode legacy");
|
||||
assert!(configs_semantically_equal(&lhs, &rhs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_configs_semantically_equal_detects_notify_changes() {
|
||||
let lhs = decode_server_config_blob(
|
||||
br#"{"version":"33","storageclass":{"standard":"EC:2","rrs":"EC:1"},"notify":{"webhook":{"primary":{"enable":true,"endpoint":"https://example.com/a"}}}}"#,
|
||||
)
|
||||
.expect("decode lhs");
|
||||
let rhs = decode_server_config_blob(
|
||||
br#"{"version":"33","storageclass":{"standard":"EC:2","rrs":"EC:1"},"notify":{"webhook":{"primary":{"enable":true,"endpoint":"https://example.com/b"}}}}"#,
|
||||
)
|
||||
.expect("decode rhs");
|
||||
|
||||
assert!(!configs_semantically_equal(&lhs, &rhs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_configs_semantically_equal_accounts_for_audit() {
|
||||
let external = br#"{
|
||||
"version":"33",
|
||||
"storageclass":{"standard":"EC:2","rrs":"EC:1","optimize":"availability"},
|
||||
"logger":{
|
||||
"webhook":{
|
||||
"primary":{
|
||||
"enable":true,
|
||||
"endpoint":"https://example.com/audit-hook"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let legacy = br#"{
|
||||
"storage_class":{"_":[
|
||||
{"key":"standard","value":"EC:2"},
|
||||
{"key":"rrs","value":"EC:1"},
|
||||
{"key":"optimize","value":"availability"}
|
||||
]},
|
||||
"audit_webhook":{
|
||||
"_":[
|
||||
{"key":"enable","value":"off"},
|
||||
{"key":"endpoint","value":""},
|
||||
{"key":"auth_token","value":""},
|
||||
{"key":"client_cert","value":""},
|
||||
{"key":"client_key","value":""},
|
||||
{"key":"client_ca","value":""},
|
||||
{"key":"skip_tls_verify","value":"off"},
|
||||
{"key":"batch_size","value":"1"},
|
||||
{"key":"queue_limit","value":"100000"},
|
||||
{"key":"queue_dir","value":"/opt/rustfs/events"},
|
||||
{"key":"max_retry","value":"0"},
|
||||
{"key":"retry_interval","value":"3s"},
|
||||
{"key":"http_timeout","value":"5s"},
|
||||
{"key":"comment","value":""}
|
||||
],
|
||||
"primary":[
|
||||
{"key":"enable","value":"on"},
|
||||
{"key":"endpoint","value":"https://example.com/audit-hook"}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let lhs = decode_server_config_blob(external).expect("decode external");
|
||||
let rhs = decode_server_config_blob(legacy).expect("decode legacy");
|
||||
assert!(configs_semantically_equal(&lhs, &rhs));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn test_read_config_with_metadata_succeeds_with_one_healthy_locker_in_two_node_dist_setup() {
|
||||
|
||||
@@ -16,7 +16,8 @@ use crate::config::{KV, KVS};
|
||||
use rustfs_config::{
|
||||
COMMENT_KEY, DEFAULT_LIMIT, ENABLE_KEY, EVENT_DEFAULT_DIR, EnableState, MQTT_BROKER, MQTT_KEEP_ALIVE_INTERVAL, MQTT_PASSWORD,
|
||||
MQTT_QOS, MQTT_QUEUE_DIR, MQTT_QUEUE_LIMIT, MQTT_RECONNECT_INTERVAL, MQTT_TOPIC, MQTT_USERNAME, WEBHOOK_AUTH_TOKEN,
|
||||
WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
|
||||
WEBHOOK_CLIENT_CA, WEBHOOK_CLIENT_CERT, WEBHOOK_CLIENT_KEY, WEBHOOK_ENDPOINT, WEBHOOK_QUEUE_DIR, WEBHOOK_QUEUE_LIMIT,
|
||||
WEBHOOK_SKIP_TLS_VERIFY,
|
||||
};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -60,6 +61,16 @@ pub static DEFAULT_NOTIFY_WEBHOOK_KVS: LazyLock<KVS> = LazyLock::new(|| {
|
||||
value: "".to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: WEBHOOK_CLIENT_CA.to_owned(),
|
||||
value: "".to_owned(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: WEBHOOK_SKIP_TLS_VERIFY.to_owned(),
|
||||
value: EnableState::Off.to_string(),
|
||||
hidden_if_empty: false,
|
||||
},
|
||||
KV {
|
||||
key: COMMENT_KEY.to_owned(),
|
||||
value: "".to_owned(),
|
||||
|
||||
Reference in New Issue
Block a user