0.34.13_cleaup_code
This commit is contained in:
1 parent
ec46a96a29
commit
5c2e50ec06
87 files changed
+4890
-1338
No files matched your search
@@ -21,13 +21,16 @@ fn general_api_token_allowed(method: &Method, path: &str, permission: &str) -> b
|
||||
| "/api/metrics/json"
|
||||
| "/api/metrics/summary"
|
||||
| "/api/cache/bans"
|
||||
) || (method == Method::GET
|
||||
&& path.strip_prefix("/api/config/")
|
||||
)
|
||||
|| (method == Method::GET
|
||||
&& path
|
||||
.strip_prefix("/api/config/")
|
||||
.is_some_and(|section| ConfigSection::from_name(section).is_some()));
|
||||
let write = permission == "rw"
|
||||
&& ((method == Method::PUT
|
||||
&& (path == "/api/config"
|
||||
|| path.strip_prefix("/api/config/")
|
||||
|| path
|
||||
.strip_prefix("/api/config/")
|
||||
.is_some_and(|section| ConfigSection::from_name(section).is_some())))
|
||||
|| (method == Method::POST
|
||||
&& matches!(
|
||||
|
||||
+108
-62
@@ -1,6 +1,3 @@
|
||||
// GET and PUT resolve exactly the same named sections. All writes go through
|
||||
// App::update_as: full validation, revision check under the update lock, and
|
||||
// one durable transaction. These handlers do not implement partial patches.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ConfigSection {
|
||||
Listeners,
|
||||
@@ -93,66 +90,109 @@ impl ConfigSection {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace(self, config: &mut crate::config::Config, name: &str, value: Value) -> Result<(), ApiError> {
|
||||
fn replace(
|
||||
self,
|
||||
config: &mut crate::config::Config,
|
||||
name: &str,
|
||||
value: Value,
|
||||
) -> Result<(), ApiError> {
|
||||
match self {
|
||||
Self::Listeners => config.listeners = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::DockerAgents => config.docker_agents = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Http2 => config.http2 = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Pages => config.pages = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Cache => config.cache = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Transform => config.transform = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Forwarding => config.forwarding = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Headers => config.headers = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Prometheus => config.prometheus = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Geoip => config.geoip_updates = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Administration => config.administration = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Tls => config.tls = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Vhosts => config.vhosts = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::WebServices => config.web_services = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::PhpFpmProfiles => config.php_fpm_profiles = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Redirects => config.redirects = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::NotFoundHosts => config.not_found_hosts = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Streams => config.streams = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::AclProfiles => config.acl_profiles = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::HostPolicies => config.host_policies = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?,
|
||||
Self::Listeners => {
|
||||
config.listeners =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::DockerAgents => {
|
||||
config.docker_agents =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Http2 => {
|
||||
config.http2 =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Pages => {
|
||||
config.pages =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Cache => {
|
||||
config.cache =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Transform => {
|
||||
config.transform =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Forwarding => {
|
||||
config.forwarding =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Headers => {
|
||||
config.headers =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Prometheus => {
|
||||
config.prometheus =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Geoip => {
|
||||
config.geoip_updates =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Administration => {
|
||||
config.administration =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Tls => {
|
||||
config.tls =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Vhosts => {
|
||||
config.vhosts =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::WebServices => {
|
||||
config.web_services =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::PhpFpmProfiles => {
|
||||
config.php_fpm_profiles =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Redirects => {
|
||||
config.redirects =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::NotFoundHosts => {
|
||||
config.not_found_hosts =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Streams => {
|
||||
config.streams =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::AclProfiles => {
|
||||
config.acl_profiles =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::HostPolicies => {
|
||||
config.host_policies =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?
|
||||
}
|
||||
Self::Response => {
|
||||
let part: ResponseSection = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?;
|
||||
let part: ResponseSection =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?;
|
||||
config.server_header = part.server_header;
|
||||
config.diagnostic_headers = part.diagnostic_headers;
|
||||
config.compression = part.compression;
|
||||
}
|
||||
Self::Logging => {
|
||||
// A logging save must include both sinks; there is no implicit fallback
|
||||
// to a single-field payload that could reset error_log accidentally.
|
||||
let part: LoggingSection = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?;
|
||||
let part: LoggingSection =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?;
|
||||
config.logging = part.logging;
|
||||
config.error_log = part.error_log;
|
||||
}
|
||||
Self::GlobalSecurity => {
|
||||
let part: crate::config::GlobalSecurity = serde_json::from_value(value)
|
||||
.map_err(|error| invalid_section(name, error))?;
|
||||
let part: crate::config::GlobalSecurity =
|
||||
serde_json::from_value(value).map_err(|error| invalid_section(name, error))?;
|
||||
if part.user_agent_blocking.is_none() {
|
||||
return Err(ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -167,10 +207,12 @@ impl ConfigSection {
|
||||
}
|
||||
|
||||
fn requested_config_section(name: &str) -> Result<ConfigSection, ApiError> {
|
||||
ConfigSection::from_name(name).ok_or_else(|| ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown configuration section".into(),
|
||||
))
|
||||
ConfigSection::from_name(name).ok_or_else(|| {
|
||||
ApiError(
|
||||
StatusCode::NOT_FOUND,
|
||||
"unknown configuration section".into(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn configuration_section(
|
||||
@@ -192,7 +234,6 @@ async fn update_section(
|
||||
Path(section): Path<String>,
|
||||
Json(input): Json<SectionUpdate>,
|
||||
) -> Result<Json<crate::config::Envelope>, ApiError> {
|
||||
// Resolve the name before taking the snapshot, including for stale requests.
|
||||
let selected = requested_config_section(§ion)?;
|
||||
let current = state.config.load_full();
|
||||
if current.envelope.revision != input.expected_revision {
|
||||
@@ -203,12 +244,18 @@ async fn update_section(
|
||||
}
|
||||
let mut config = current.envelope.config.clone();
|
||||
selected.replace(&mut config, §ion, input.value)?;
|
||||
state.update_as(Update { expected_revision: input.expected_revision, config }, session.username)
|
||||
.await.map(Json)
|
||||
state
|
||||
.update_as(
|
||||
Update {
|
||||
expected_revision: input.expected_revision,
|
||||
config,
|
||||
},
|
||||
session.username,
|
||||
)
|
||||
.await
|
||||
.map(Json)
|
||||
}
|
||||
|
||||
// GeoIP is an explicit side-effecting action within the geoip settings section.
|
||||
// GET and PUT are only for desired settings; POST refreshes external data.
|
||||
async fn geoip_update(
|
||||
State(state): State<Arc<App>>,
|
||||
Extension(session): Extension<Session>,
|
||||
@@ -239,4 +286,3 @@ async fn geoip_update(
|
||||
.map_err(auth::internal)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
+41
-22
@@ -95,32 +95,37 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
|
||||
.ok()
|
||||
.is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink());
|
||||
if directory_safe {
|
||||
let entries = newest_log_files(std::fs::read_dir(&directory)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flat_map(|iter| iter.filter_map(|entry| entry.ok()))
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let plain = name.ends_with(".jsonl");
|
||||
let rotated = name.contains(".jsonl.") && name.ends_with(".gz");
|
||||
if (!plain && !rotated) || name.len() > 180 {
|
||||
return None;
|
||||
}
|
||||
let file_type = entry.file_type().ok()?;
|
||||
if !file_type.is_file() || file_type.is_symlink() {
|
||||
return None;
|
||||
}
|
||||
let meta = entry.metadata().ok()?;
|
||||
Some((name, path, file_modified_ms(&meta)))
|
||||
}), 256);
|
||||
let entries = newest_log_files(
|
||||
std::fs::read_dir(&directory)
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flat_map(|iter| iter.filter_map(|entry| entry.ok()))
|
||||
.filter_map(|entry| {
|
||||
let path = entry.path();
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
let plain = name.ends_with(".jsonl");
|
||||
let rotated = name.contains(".jsonl.") && name.ends_with(".gz");
|
||||
if (!plain && !rotated) || name.len() > 180 {
|
||||
return None;
|
||||
}
|
||||
let file_type = entry.file_type().ok()?;
|
||||
if !file_type.is_file() || file_type.is_symlink() {
|
||||
return None;
|
||||
}
|
||||
let meta = entry.metadata().ok()?;
|
||||
Some((name, path, file_modified_ms(&meta)))
|
||||
}),
|
||||
256,
|
||||
);
|
||||
for (name, path, _) in entries {
|
||||
let id = if name.ends_with(".jsonl") {
|
||||
format!("vhost:{}", name.trim_end_matches(".jsonl"))
|
||||
} else {
|
||||
format!("vhost-gz:{name}")
|
||||
};
|
||||
let vhost_name = name.split_once(".jsonl").map_or(name.as_str(), |(prefix, _)| prefix);
|
||||
let vhost_name = name
|
||||
.split_once(".jsonl")
|
||||
.map_or(name.as_str(), |(prefix, _)| prefix);
|
||||
let group_id = format!("vhost:{vhost_name}");
|
||||
sources.push(source_from_path(
|
||||
id,
|
||||
@@ -144,7 +149,14 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
|
||||
config.logging.file.enabled,
|
||||
"JSONL",
|
||||
));
|
||||
sources.extend(rotated_gzip_sources(&access_path, "access", "Access log", "access", config.logging.file.enabled, "JSONL"));
|
||||
sources.extend(rotated_gzip_sources(
|
||||
&access_path,
|
||||
"access",
|
||||
"Access log",
|
||||
"access",
|
||||
config.logging.file.enabled,
|
||||
"JSONL",
|
||||
));
|
||||
}
|
||||
let error_format = match config.error_log.format {
|
||||
crate::logging::ErrorLogFormat::Json => "JSONL",
|
||||
@@ -160,7 +172,14 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
|
||||
config.error_log.enabled,
|
||||
error_format,
|
||||
));
|
||||
sources.extend(rotated_gzip_sources(&error_path, "error", "Error log", "error", config.error_log.enabled, error_format));
|
||||
sources.extend(rotated_gzip_sources(
|
||||
&error_path,
|
||||
"error",
|
||||
"Error log",
|
||||
"error",
|
||||
config.error_log.enabled,
|
||||
error_format,
|
||||
));
|
||||
sources
|
||||
}
|
||||
fn file_log_sources_json(sources: &[FileLogSource]) -> Value {
|
||||
|
||||
@@ -14305,4 +14305,4 @@
|
||||
}
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -260,7 +260,10 @@ pub fn router(state: Arc<App>) -> Router {
|
||||
post(restore_config_history).layer(DefaultBodyLimit::max(4096)),
|
||||
)
|
||||
.route("/api/config/geoip/update", post(geoip_update))
|
||||
.route("/api/config/{section}", get(configuration_section).put(update_section))
|
||||
.route(
|
||||
"/api/config/{section}",
|
||||
get(configuration_section).put(update_section),
|
||||
)
|
||||
.route("/api/logs", get(logs))
|
||||
.route("/api/live-logs", get(live_logs_stream))
|
||||
.route("/api/file-logs", get(file_logs))
|
||||
|
||||
+180
-67
@@ -654,11 +654,29 @@ mod tests {
|
||||
#[test]
|
||||
fn config_section_registry_read_and_replace_are_symmetric() {
|
||||
let names = [
|
||||
"listeners", "docker-agents", "http2", "pages", "cache", "transform",
|
||||
"forwarding", "headers", "prometheus", "geoip", "administration",
|
||||
"tls", "vhosts", "web-services", "php-fpm-profiles", "redirects",
|
||||
"not-found-hosts", "streams", "acl-profiles", "host-policies",
|
||||
"response", "logging", "global-security",
|
||||
"listeners",
|
||||
"docker-agents",
|
||||
"http2",
|
||||
"pages",
|
||||
"cache",
|
||||
"transform",
|
||||
"forwarding",
|
||||
"headers",
|
||||
"prometheus",
|
||||
"geoip",
|
||||
"administration",
|
||||
"tls",
|
||||
"vhosts",
|
||||
"web-services",
|
||||
"php-fpm-profiles",
|
||||
"redirects",
|
||||
"not-found-hosts",
|
||||
"streams",
|
||||
"acl-profiles",
|
||||
"host-policies",
|
||||
"response",
|
||||
"logging",
|
||||
"global-security",
|
||||
];
|
||||
let mut original = Config::default();
|
||||
original.global_security.compile().unwrap();
|
||||
@@ -671,7 +689,14 @@ mod tests {
|
||||
assert_eq!(serde_json::to_value(&updated).unwrap(), expected, "{name}");
|
||||
assert_eq!(section.read(&updated), value, "{name}");
|
||||
}
|
||||
for name in ["user-agent-blocking", "error_log", "history", "storage", "unknown", "tls/nested"] {
|
||||
for name in [
|
||||
"user-agent-blocking",
|
||||
"error_log",
|
||||
"history",
|
||||
"storage",
|
||||
"unknown",
|
||||
"tls/nested",
|
||||
] {
|
||||
assert!(ConfigSection::from_name(name).is_none(), "{name}");
|
||||
}
|
||||
}
|
||||
@@ -681,17 +706,40 @@ mod tests {
|
||||
let (router, store, _dir, secret, _csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
for name in [
|
||||
"listeners", "docker-agents", "http2", "pages", "cache", "transform",
|
||||
"forwarding", "headers", "prometheus", "geoip", "administration", "tls",
|
||||
"vhosts", "web-services", "php-fpm-profiles", "redirects", "not-found-hosts",
|
||||
"streams", "acl-profiles", "host-policies", "response", "logging",
|
||||
"listeners",
|
||||
"docker-agents",
|
||||
"http2",
|
||||
"pages",
|
||||
"cache",
|
||||
"transform",
|
||||
"forwarding",
|
||||
"headers",
|
||||
"prometheus",
|
||||
"geoip",
|
||||
"administration",
|
||||
"tls",
|
||||
"vhosts",
|
||||
"web-services",
|
||||
"php-fpm-profiles",
|
||||
"redirects",
|
||||
"not-found-hosts",
|
||||
"streams",
|
||||
"acl-profiles",
|
||||
"host-policies",
|
||||
"response",
|
||||
"logging",
|
||||
"global-security",
|
||||
] {
|
||||
let response = router.clone().oneshot(
|
||||
request(&format!("/api/config/{name}"))
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap(),
|
||||
).await.unwrap();
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(&format!("/api/config/{name}"))
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK, "{name}");
|
||||
let data = json_body(response).await;
|
||||
assert_eq!(data["section"], name);
|
||||
@@ -708,24 +756,57 @@ mod tests {
|
||||
assert!(data["value"]["user_agent_blocking"].is_object());
|
||||
}
|
||||
}
|
||||
let unknown = router.clone().oneshot(
|
||||
request("/api/config/user-agent-blocking")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap(),
|
||||
).await.unwrap();
|
||||
let unknown = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config/user-agent-blocking")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(unknown.status(), StatusCode::NOT_FOUND);
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geoip_update_is_scoped_to_settings_for_general_api_tokens() {
|
||||
assert!(general_api_token_allowed(&Method::GET, "/api/config/geoip", "ro"));
|
||||
assert!(general_api_token_allowed(&Method::PUT, "/api/config/geoip", "rw"));
|
||||
assert!(!general_api_token_allowed(&Method::PUT, "/api/config/geoip", "ro"));
|
||||
assert!(general_api_token_allowed(&Method::POST, "/api/config/geoip/update", "rw"));
|
||||
assert!(!general_api_token_allowed(&Method::POST, "/api/config/geoip/update", "ro"));
|
||||
assert!(!general_api_token_allowed(&Method::POST, "/api/geoip/update", "rw"));
|
||||
assert!(!general_api_token_allowed(&Method::GET, "/api/config/geoip/update", "rw"));
|
||||
assert!(general_api_token_allowed(
|
||||
&Method::GET,
|
||||
"/api/config/geoip",
|
||||
"ro"
|
||||
));
|
||||
assert!(general_api_token_allowed(
|
||||
&Method::PUT,
|
||||
"/api/config/geoip",
|
||||
"rw"
|
||||
));
|
||||
assert!(!general_api_token_allowed(
|
||||
&Method::PUT,
|
||||
"/api/config/geoip",
|
||||
"ro"
|
||||
));
|
||||
assert!(general_api_token_allowed(
|
||||
&Method::POST,
|
||||
"/api/config/geoip/update",
|
||||
"rw"
|
||||
));
|
||||
assert!(!general_api_token_allowed(
|
||||
&Method::POST,
|
||||
"/api/config/geoip/update",
|
||||
"ro"
|
||||
));
|
||||
assert!(!general_api_token_allowed(
|
||||
&Method::POST,
|
||||
"/api/geoip/update",
|
||||
"rw"
|
||||
));
|
||||
assert!(!general_api_token_allowed(
|
||||
&Method::GET,
|
||||
"/api/config/geoip/update",
|
||||
"rw"
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -736,9 +817,16 @@ mod tests {
|
||||
("/api/config/geoip/update", StatusCode::METHOD_NOT_ALLOWED),
|
||||
("/api/geoip/update", StatusCode::NOT_FOUND),
|
||||
] {
|
||||
let response = router.clone().oneshot(
|
||||
request(path).header("cookie", &cookie).body(Body::empty()).unwrap(),
|
||||
).await.unwrap();
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request(path)
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), expected, "{path}");
|
||||
}
|
||||
store.close().await;
|
||||
@@ -813,12 +901,10 @@ mod tests {
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(
|
||||
serde_json::to_vec(
|
||||
&json!({"expected_revision":0,"value":{
|
||||
"logging":Config::default().logging,
|
||||
"error_log":Config::default().error_log
|
||||
}}),
|
||||
)
|
||||
serde_json::to_vec(&json!({"expected_revision":0,"value":{
|
||||
"logging":Config::default().logging,
|
||||
"error_log":Config::default().error_log
|
||||
}}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
@@ -846,7 +932,6 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn global_security_section_saves_rules_and_existing_ua_atomically() {
|
||||
let (router, _store, _dir, secret, csrf) = fixture().await;
|
||||
@@ -858,53 +943,81 @@ mod tests {
|
||||
"user_agent_blocking": {"enabled":true, "patterns":["bad-bot"]}
|
||||
});
|
||||
let body = serde_json::to_vec(&json!({"expected_revision":0, "value":value})).unwrap();
|
||||
let response = router.clone().oneshot(
|
||||
request("/api/config/global-security")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(body)).unwrap(),
|
||||
).await.unwrap();
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config/global-security")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(body))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let json = json_body(response).await;
|
||||
assert_eq!(json["revision"], 1);
|
||||
assert_eq!(json["config"]["global_security"]["country_access"]["countries"][0], "RU");
|
||||
assert_eq!(json["config"]["global_security"]["security_headers"]["nosniff"], true);
|
||||
assert_eq!(json["config"]["global_security"]["user_agent_blocking"]["patterns"][0], "bad-bot");
|
||||
assert_eq!(
|
||||
json["config"]["global_security"]["country_access"]["countries"][0],
|
||||
"RU"
|
||||
);
|
||||
assert_eq!(
|
||||
json["config"]["global_security"]["security_headers"]["nosniff"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
json["config"]["global_security"]["user_agent_blocking"]["patterns"][0],
|
||||
"bad-bot"
|
||||
);
|
||||
assert!(json["config"].get("user_agent_blocking").is_none());
|
||||
|
||||
// A partial section update must not silently reset the existing UA policy.
|
||||
let body = serde_json::to_vec(&json!({
|
||||
"expected_revision":1,
|
||||
"value": {"method_policy":{"mode":"off","methods":[]}}
|
||||
})).unwrap();
|
||||
let response = router.clone().oneshot(
|
||||
request("/api/config/global-security")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(body)).unwrap(),
|
||||
).await.unwrap();
|
||||
}))
|
||||
.unwrap();
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config/global-security")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(body))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
// GET returns the same standalone section shape accepted by PUT, at the
|
||||
// matching revision and without unrequested configuration fields.
|
||||
let read = router.clone().oneshot(
|
||||
request("/api/config/global-security")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty()).unwrap(),
|
||||
).await.unwrap();
|
||||
let read = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config/global-security")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(read.status(), StatusCode::OK);
|
||||
let view = json_body(read).await;
|
||||
assert_eq!(view["revision"], 1);
|
||||
assert_eq!(view["section"], "global-security");
|
||||
assert_eq!(view["value"]["user_agent_blocking"]["patterns"][0], "bad-bot");
|
||||
assert_eq!(
|
||||
view["value"]["user_agent_blocking"]["patterns"][0],
|
||||
"bad-bot"
|
||||
);
|
||||
assert!(view.get("config").is_none());
|
||||
assert!(view["value"].get("global_security").is_none());
|
||||
}
|
||||
|
||||
@@ -1505,8 +1505,22 @@ html[data-table-density=ultra] :is(table th, table td) {
|
||||
gap: 6px;
|
||||
max-width: 260px;
|
||||
}
|
||||
|
||||
/* Permission guidance belongs to its generated command block, not the path inputs. */
|
||||
#certificate-external-acl-example.path-permissions { margin-top: 20px; border-top: 1px solid var(--line); padding-top: 15px; }
|
||||
.path-permissions-commands { margin-bottom: 0; }
|
||||
.path-permissions-commands + .path-permissions-description { margin: 7px 0 0; }
|
||||
.path-permissions-actions { margin-top: 10px; }
|
||||
#certificate-external-acl-example.path-permissions {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.path-permissions-commands {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.path-permissions-commands+.path-permissions-description {
|
||||
margin: 7px 0 0;
|
||||
}
|
||||
|
||||
.path-permissions-actions {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@@ -770,4 +770,4 @@ body[data-density=compact] .page-intro .lead {
|
||||
to {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user