feat(client-registry): separate trusted server-defined client tags from client-reported tags with legacy tag compatibility

This commit is contained in:
2026-03-30 09:42:04 +00:00
parent a757a4bb73
commit 2d7a507cf2
5 changed files with 41 additions and 7 deletions

View File

@@ -44,7 +44,12 @@ pub struct ClientEntry {
pub priority: Option<u32>,
/// Whether this client is enabled (default: true).
pub enabled: Option<bool>,
/// Tags for grouping.
/// Tags assigned by the server admin — trusted, used for access control.
pub server_defined_client_tags: Option<Vec<String>>,
/// Tags reported by the connecting client — informational only.
pub client_defined_client_tags: Option<Vec<String>>,
/// Legacy tags field — treated as serverDefinedClientTags during deserialization.
#[serde(default)]
pub tags: Option<Vec<String>>,
/// Optional description.
pub description: Option<String>,
@@ -90,7 +95,11 @@ impl ClientRegistry {
/// Build a registry from a list of client entries.
pub fn from_entries(entries: Vec<ClientEntry>) -> Result<Self> {
let mut registry = Self::new();
for entry in entries {
for mut entry in entries {
// Migrate legacy `tags` → `serverDefinedClientTags`
if entry.server_defined_client_tags.is_none() && entry.tags.is_some() {
entry.server_defined_client_tags = entry.tags.take();
}
registry.add(entry)?;
}
Ok(registry)
@@ -193,6 +202,8 @@ mod tests {
security: None,
priority: None,
enabled: None,
server_defined_client_tags: None,
client_defined_client_tags: None,
tags: None,
description: None,
expires_at: None,

View File

@@ -551,9 +551,16 @@ impl VpnServer {
).ok(),
priority: partial.get("priority").and_then(|v| v.as_u64()).map(|v| v as u32),
enabled: partial.get("enabled").and_then(|v| v.as_bool()).or(Some(true)),
tags: partial.get("tags").and_then(|v| {
server_defined_client_tags: partial.get("serverDefinedClientTags").and_then(|v| {
v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())
}).or_else(|| {
// Legacy: accept "tags" as serverDefinedClientTags
partial.get("tags").and_then(|v| {
v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(String::from)).collect())
})
}),
client_defined_client_tags: None, // Only set by connecting client
tags: None, // Legacy field — not used for new entries
description: partial.get("description").and_then(|v| v.as_str()).map(String::from),
expires_at: partial.get("expiresAt").and_then(|v| v.as_str()).map(String::from),
assigned_ip: Some(assigned_ip.to_string()),
@@ -648,8 +655,11 @@ impl VpnServer {
if let Some(enabled) = update.get("enabled").and_then(|v| v.as_bool()) {
entry.enabled = Some(enabled);
}
if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) {
entry.tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
if let Some(tags) = update.get("serverDefinedClientTags").and_then(|v| v.as_array()) {
entry.server_defined_client_tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
} else if let Some(tags) = update.get("tags").and_then(|v| v.as_array()) {
// Legacy: accept "tags" as serverDefinedClientTags
entry.server_defined_client_tags = Some(tags.iter().filter_map(|s| s.as_str().map(String::from)).collect());
}
if let Some(desc) = update.get("description").and_then(|v| v.as_str()) {
entry.description = Some(desc.to_string());