mirror of
https://github.com/telemt/telemt.git
synced 2026-06-13 14:31:44 +03:00
fix(api): GET /v1/config returns only editable sections; tolerate commented TOML headers; doc fixes
This commit is contained in:
@@ -277,11 +277,10 @@ An empty request body is accepted and generates a new secret automatically.
|
|||||||
|
|
||||||
### `ConfigData`
|
### `ConfigData`
|
||||||
|
|
||||||
Returned by `GET /v1/config`. The top-level fields mirror the editable TOML sections; `access.*` is intentionally omitted.
|
Returned by `GET /v1/config` as the envelope `data`. The fields are exactly the editable TOML sections. The current revision is returned in the envelope `revision` field (same value as `config_hash` in `SystemInfoData`), **not** inside `data`.
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `revision` | `string` | SHA-256 hex of the current on-disk config content (same value as `config_hash` in `SystemInfoData` and the envelope `revision`). |
|
|
||||||
| `general` | `object?` | `[general]` section, if present in config. |
|
| `general` | `object?` | `[general]` section, if present in config. |
|
||||||
| `timeouts` | `object?` | `[timeouts]` section, if present. |
|
| `timeouts` | `object?` | `[timeouts]` section, if present. |
|
||||||
| `censorship` | `object?` | `[censorship]` section, if present. |
|
| `censorship` | `object?` | `[censorship]` section, if present. |
|
||||||
@@ -289,7 +288,7 @@ Returned by `GET /v1/config`. The top-level fields mirror the editable TOML sect
|
|||||||
| `show_link` | `object?` | `[show_link]` section, if present. |
|
| `show_link` | `object?` | `[show_link]` section, if present. |
|
||||||
| `dc_overrides` | `object?` | `[dc_overrides]` section, if present. |
|
| `dc_overrides` | `object?` | `[dc_overrides]` section, if present. |
|
||||||
|
|
||||||
Sections absent from the config file are absent from the response (not `null`). The `access` section is always stripped — users and secrets are never exposed here.
|
Sections absent from the config file are absent from the response (not `null`). Only the editable sections above are returned; `access` (users/secrets), `server` (carries the API `auth_header` and per-node identity), and `network` (per-node addresses) are always excluded.
|
||||||
|
|
||||||
### `PatchConfigResponse`
|
### `PatchConfigResponse`
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ pub(super) async fn apply_patch_to_path(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the editable config sections (no `access.*`) + current revision.
|
/// Return only the editable config sections + current revision.
|
||||||
pub(super) async fn read_managed_config(config_path: &Path) -> Result<(Toml, String), ApiFailure> {
|
pub(super) async fn read_managed_config(config_path: &Path) -> Result<(Toml, String), ApiFailure> {
|
||||||
let original = tokio::fs::read_to_string(config_path)
|
let original = tokio::fs::read_to_string(config_path)
|
||||||
.await
|
.await
|
||||||
@@ -133,11 +133,19 @@ pub(super) async fn read_managed_config(config_path: &Path) -> Result<(Toml, Str
|
|||||||
let parsed: Toml = toml::from_str(&original)
|
let parsed: Toml = toml::from_str(&original)
|
||||||
.map_err(|e| ApiFailure::internal(format!("failed to parse config: {}", e)))?;
|
.map_err(|e| ApiFailure::internal(format!("failed to parse config: {}", e)))?;
|
||||||
|
|
||||||
let mut table = parsed
|
let parsed_table = parsed
|
||||||
.as_table()
|
.as_table()
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(toml::value::Table::new);
|
.unwrap_or_else(toml::value::Table::new);
|
||||||
table.remove("access"); // never expose users/secrets via this endpoint
|
// Whitelist: return ONLY the editable sections. A blacklist (just removing
|
||||||
|
// `access`) would leak `server` (carries the API `auth_header` + per-node
|
||||||
|
// identity) and `network` (per-node addresses). Mirror the PATCH contract.
|
||||||
|
let mut table = toml::value::Table::new();
|
||||||
|
for section in EDITABLE_SECTIONS {
|
||||||
|
if let Some(value) = parsed_table.get(*section) {
|
||||||
|
table.insert((*section).to_string(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let revision = compute_revision(&original);
|
let revision = compute_revision(&original);
|
||||||
Ok((Toml::Table(table), revision))
|
Ok((Toml::Table(table), revision))
|
||||||
@@ -279,6 +287,24 @@ mod tests {
|
|||||||
assert_eq!(revision, current_revision(&path).await.unwrap());
|
assert_eq!(revision, current_revision(&path).await.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn read_managed_config_returns_only_editable_sections() {
|
||||||
|
// server carries the API auth_header + per-node identity; network carries
|
||||||
|
// per-node addresses. Neither must be exposed by GET /v1/config.
|
||||||
|
let (path, _d) = temp_config(concat!(
|
||||||
|
"[censorship]\ntls_domain = \"a\"\n",
|
||||||
|
"[server]\nport = 443\n[server.api]\nauth_header = \"SECRET\"\n",
|
||||||
|
"[network]\nipv4 = \"1.2.3.4\"\n",
|
||||||
|
"[access.users]\nbob = \"deadbeef\"\n",
|
||||||
|
));
|
||||||
|
let (value, _rev) = read_managed_config(&path).await.unwrap();
|
||||||
|
let table = value.as_table().unwrap();
|
||||||
|
assert!(table.contains_key("censorship"));
|
||||||
|
assert!(!table.contains_key("server")); // no API auth_header / identity leak
|
||||||
|
assert!(!table.contains_key("network")); // no per-node identity leak
|
||||||
|
assert!(!table.contains_key("access")); // no users/secrets
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn patch_rejects_server_section() {
|
async fn patch_rejects_server_section() {
|
||||||
let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
|
let (path, _d) = temp_config("[censorship]\ntls_domain = \"a\"\n");
|
||||||
|
|||||||
@@ -354,14 +354,16 @@ fn find_toml_table_bounds(source: &str, table_name: &str) -> Option<(usize, usiz
|
|||||||
let mut start = None;
|
let mut start = None;
|
||||||
|
|
||||||
for line in source.split_inclusive('\n') {
|
for line in source.split_inclusive('\n') {
|
||||||
let trimmed = line.trim();
|
// Drop any inline comment so a hand-edited header like
|
||||||
|
// `[censorship] # note` still matches. Section names never contain `#`.
|
||||||
|
let header = line.trim().split('#').next().unwrap_or("").trim();
|
||||||
if let Some(start_offset) = start {
|
if let Some(start_offset) = start {
|
||||||
let is_same_array = trimmed == array;
|
let is_same_array = header == array;
|
||||||
let is_new_header = trimmed.starts_with('[');
|
let is_new_header = header.starts_with('[');
|
||||||
if is_new_header && !is_same_array {
|
if is_new_header && !is_same_array {
|
||||||
return Some((start_offset, offset));
|
return Some((start_offset, offset));
|
||||||
}
|
}
|
||||||
} else if trimmed == single || trimmed == array {
|
} else if header == single || header == array {
|
||||||
start = Some(offset);
|
start = Some(offset);
|
||||||
}
|
}
|
||||||
offset = offset.saturating_add(line.len());
|
offset = offset.saturating_add(line.len());
|
||||||
@@ -453,6 +455,18 @@ mod tests {
|
|||||||
assert!(slice.contains("kind = \"b\"")); // spans through the last upstream block
|
assert!(slice.contains("kind = \"b\"")); // spans through the last upstream block
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_bounds_matches_header_with_inline_comment() {
|
||||||
|
let src = "[censorship] # notes\ntls_domain = \"a\"\n\n[server]\nport = 1\n";
|
||||||
|
let bounds = find_toml_table_bounds(src, "censorship");
|
||||||
|
assert!(bounds.is_some(), "commented header must still match");
|
||||||
|
let (start, end) = bounds.unwrap();
|
||||||
|
let slice = &src[start..end];
|
||||||
|
assert!(slice.starts_with("[censorship] # notes"));
|
||||||
|
assert!(slice.contains("tls_domain"));
|
||||||
|
assert!(!slice.contains("[server]")); // terminates at the next header
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_user_rate_limits_section() {
|
fn render_user_rate_limits_section() {
|
||||||
let mut cfg = ProxyConfig::default();
|
let mut cfg = ProxyConfig::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user