refactor(credentials): centralize grant policy (#2517)

* refactor(credentials): centralize grant policy

Represent ACL groups, relay permission, proxy CIDRs, and reuse as one
internal credential grant shared by generated, imported, managed, and
attached credentials. Normalize proxy CIDRs at construction while
preserving the flat credential storage schema.

Reuse one managed credential adapter for protobuf/TOML projection and
patching so defaults and future fields have a single mapping authority.

* fix(credentials): normalize grants loaded from storage

Run persisted grants through the same CIDR normalization used by new and
managed credentials. Reject invalid stored CIDRs through the existing
storage-unavailable path and include the credential ID in the error.

Cover whitespace migration and invalid legacy data with regression tests.
This commit is contained in:
KKRainbow
2026-08-22 23:55:29 +08:00
committed by GitHub
parent 3fe427bc99
commit 9f231ee76c
8 changed files with 313 additions and 175 deletions
+8
View File
@@ -22,6 +22,14 @@ operation transition.
Host capability operations use a separate seam. They turn Host readiness into Host capability operations use a separate seam. They turn Host readiness into
Rust task wakeups and do not share the caller-to-core broker state machine. Rust task wakeups and do not share the caller-to-core broker state machine.
## Credential grant
A credential grant contains the authorization constraints shared by generated,
imported, managed, and attached-peer credentials: ACL groups, relay permission,
allowed proxy CIDRs, and whether concurrent reuse is allowed. It does not own
credential identity, key material, lifetime, persistence, or runtime ownership.
Each credential intake path normalizes the grant before installing it.
## Attached peer ## Attached peer
An attached peer is an ordinary `PeerManagerCore` connected to another An attached peer is an ordinary `PeerManagerCore` connected to another
+5 -10
View File
@@ -4,7 +4,10 @@ use easytier_proto::api::manage::{
self, NetworkConfig, NetworkingMethod, PortForwardConfig as ApiPortForwardConfig, self, NetworkConfig, NetworkingMethod, PortForwardConfig as ApiPortForwardConfig,
}; };
use super::toml::{ConfigLoader as _, TomlConfig}; use super::{
api_input::managed_credential_to_proto,
toml::{ConfigLoader as _, TomlConfig},
};
pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig { pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig {
let default_config = TomlConfig::default(); let default_config = TomlConfig::default();
@@ -121,15 +124,7 @@ pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig {
result.managed_credentials = config result.managed_credentials = config
.get_managed_credentials() .get_managed_credentials()
.into_iter() .into_iter()
.map(|credential| manage::ManagedCredentialConfig { .map(managed_credential_to_proto)
credential_id: credential.credential_id,
credential_secret: credential.credential_secret,
groups: credential.groups,
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs,
expiry_unix: credential.expiry_unix,
reusable: Some(credential.reusable),
})
.collect(); .collect();
let flags = config.get_flags(); let flags = config.get_flags();
+30 -18
View File
@@ -51,6 +51,34 @@ pub fn add_proxy_network_to_config(
pub type NetworkingMethod = easytier_proto::api::manage::NetworkingMethod; pub type NetworkingMethod = easytier_proto::api::manage::NetworkingMethod;
pub type NetworkConfig = easytier_proto::api::manage::NetworkConfig; pub type NetworkConfig = easytier_proto::api::manage::NetworkConfig;
pub(crate) fn managed_credential_from_proto(
credential: &manage::ManagedCredentialConfig,
) -> ManagedCredentialConfig {
ManagedCredentialConfig {
credential_id: credential.credential_id.clone(),
credential_secret: credential.credential_secret.clone(),
groups: credential.groups.clone(),
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs.clone(),
expiry_unix: credential.expiry_unix,
reusable: credential.reusable.unwrap_or(true),
}
}
pub(crate) fn managed_credential_to_proto(
credential: ManagedCredentialConfig,
) -> manage::ManagedCredentialConfig {
manage::ManagedCredentialConfig {
credential_id: credential.credential_id,
credential_secret: credential.credential_secret,
groups: credential.groups,
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs,
expiry_unix: credential.expiry_unix,
reusable: Some(credential.reusable),
}
}
pub trait NetworkConfigExt { pub trait NetworkConfigExt {
fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error>; fn gen_config(&self) -> Result<TomlConfigLoader, anyhow::Error>;
fn new_from_config(config: impl ConfigLoader) -> Result<NetworkConfig, anyhow::Error>; fn new_from_config(config: impl ConfigLoader) -> Result<NetworkConfig, anyhow::Error>;
@@ -301,15 +329,7 @@ impl NetworkConfigExt for NetworkConfig {
cfg.set_managed_credentials( cfg.set_managed_credentials(
self.managed_credentials self.managed_credentials
.iter() .iter()
.map(|credential| ManagedCredentialConfig { .map(managed_credential_from_proto)
credential_id: credential.credential_id.clone(),
credential_secret: credential.credential_secret.clone(),
groups: credential.groups.clone(),
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs.clone(),
expiry_unix: credential.expiry_unix,
reusable: credential.reusable.unwrap_or(true),
})
.collect(), .collect(),
); );
@@ -624,15 +644,7 @@ impl NetworkConfigExt for NetworkConfig {
result.managed_credentials = config result.managed_credentials = config
.get_managed_credentials() .get_managed_credentials()
.into_iter() .into_iter()
.map(|credential| manage::ManagedCredentialConfig { .map(managed_credential_to_proto)
credential_id: credential.credential_id,
credential_secret: credential.credential_secret,
groups: credential.groups,
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs,
expiry_unix: credential.expiry_unix,
reusable: Some(credential.reusable),
})
.collect(); .collect();
let flags = config.get_flags(); let flags = config.get_flags();
let default_flags = default_config.get_flags(); let default_flags = default_config.get_flags();
+1 -8
View File
@@ -157,14 +157,7 @@ where
let generated = self let generated = self
.peer_manager .peer_manager
.credential_manager() .credential_manager()
.generate_credential_with_options( .generate_credential_with_options(options)
options.groups,
options.allow_relay,
options.allowed_proxy_cidrs,
options.ttl,
options.credential_id,
options.reusable,
)
.map_err(anyhow::Error::msg)?; .map_err(anyhow::Error::msg)?;
self.peer_manager.notify_credential_changed(); self.peer_manager.notify_credential_changed();
Ok(generated) Ok(generated)
@@ -8,9 +8,10 @@ use easytier_proto::api::config::{
use crate::{ use crate::{
config::{ config::{
api_input::managed_credential_from_proto,
peers::AclRuleConfig, peers::AclRuleConfig,
runtime::CoreInstanceRuntimeConfig, runtime::CoreInstanceRuntimeConfig,
toml::{ConfigLoader as _, ManagedCredentialConfig, TomlConfig}, toml::{ConfigLoader as _, TomlConfig},
}, },
instance::{CoreInstance, CoreInstanceConfig, CoreInstanceHost, CoreInstanceState}, instance::{CoreInstance, CoreInstanceConfig, CoreInstanceHost, CoreInstanceState},
peers::credential_manager::CredentialManager, peers::credential_manager::CredentialManager,
@@ -164,15 +165,7 @@ where
let entries = managed let entries = managed
.entries .entries
.iter() .iter()
.map(|credential| ManagedCredentialConfig { .map(managed_credential_from_proto)
credential_id: credential.credential_id.clone(),
credential_secret: credential.credential_secret.clone(),
groups: credential.groups.clone(),
allow_relay: credential.allow_relay,
allowed_proxy_cidrs: credential.allowed_proxy_cidrs.clone(),
expiry_unix: credential.expiry_unix,
reusable: credential.reusable.unwrap_or(true),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let replacement = credential_manager let replacement = credential_manager
.validate_managed_credentials(&entries) .validate_managed_credentials(&entries)
+2 -7
View File
@@ -75,13 +75,8 @@ impl AttachedCredentialRegistration {
network_runtime_config.snapshot().as_ref(), network_runtime_config.snapshot().as_ref(),
&configured_groups, &configured_groups,
); );
let credential_id = network_peer_manager.register_ephemeral_credential( let credential_id =
public_key, network_peer_manager.register_ephemeral_credential(public_key, groups)?;
groups,
false,
Vec::new(),
false,
)?;
let task_peer_manager = network_peer_manager.clone(); let task_peer_manager = network_peer_manager.clone();
let policy_task = tokio::spawn(async move { let policy_task = tokio::spawn(async move {
while peer_changes.changed().await.is_ok() { while peer_changes.changed().await.is_ok() {
+263 -112
View File
@@ -47,15 +47,68 @@ pub struct CredentialUpsertOptions {
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct CredentialEntry { struct CredentialGrant {
pubkey: String,
#[serde(default)]
secret: String,
groups: Vec<String>, groups: Vec<String>,
allow_relay: bool, allow_relay: bool,
allowed_proxy_cidrs: Vec<String>, allowed_proxy_cidrs: Vec<String>,
#[serde(default = "default_true")] #[serde(default = "default_true")]
reusable: bool, reusable: bool,
}
#[derive(Debug)]
struct InvalidAllowedProxyCidr(String);
impl std::fmt::Display for InvalidAllowedProxyCidr {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "invalid allowed_proxy_cidr: {}", self.0)
}
}
impl CredentialGrant {
fn new(
groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
reusable: bool,
) -> Result<Self, InvalidAllowedProxyCidr> {
let mut grant = Self {
groups,
allow_relay,
allowed_proxy_cidrs,
reusable,
};
grant.normalize()?;
Ok(grant)
}
fn normalize(&mut self) -> Result<(), InvalidAllowedProxyCidr> {
for cidr in &mut self.allowed_proxy_cidrs {
let normalized = cidr.trim().to_owned();
normalized
.parse::<cidr::IpCidr>()
.map_err(|_| InvalidAllowedProxyCidr(normalized.clone()))?;
*cidr = normalized;
}
Ok(())
}
fn for_attached_peer(groups: Vec<String>) -> Self {
Self {
groups,
allow_relay: false,
allowed_proxy_cidrs: Vec::new(),
reusable: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct CredentialEntry {
pubkey: String,
#[serde(default)]
secret: String,
#[serde(flatten)]
grant: CredentialGrant,
expiry_unix: i64, expiry_unix: i64,
created_at_unix: i64, created_at_unix: i64,
} }
@@ -68,22 +121,22 @@ impl CredentialEntry {
fn to_trusted_credential(&self) -> Option<TrustedCredentialPubkey> { fn to_trusted_credential(&self) -> Option<TrustedCredentialPubkey> {
Some(TrustedCredentialPubkey { Some(TrustedCredentialPubkey {
pubkey: CredentialManager::decode_pubkey_b64(&self.pubkey)?, pubkey: CredentialManager::decode_pubkey_b64(&self.pubkey)?,
groups: self.groups.clone(), groups: self.grant.groups.clone(),
allow_relay: self.allow_relay, allow_relay: self.grant.allow_relay,
expiry_unix: self.expiry_unix, expiry_unix: self.expiry_unix,
allowed_proxy_cidrs: self.allowed_proxy_cidrs.clone(), allowed_proxy_cidrs: self.grant.allowed_proxy_cidrs.clone(),
reusable: Some(self.reusable), reusable: Some(self.grant.reusable),
}) })
} }
fn to_credential_info(&self, credential_id: &str) -> CredentialInfo { fn to_credential_info(&self, credential_id: &str) -> CredentialInfo {
CredentialInfo { CredentialInfo {
credential_id: credential_id.to_string(), credential_id: credential_id.to_string(),
groups: self.groups.clone(), groups: self.grant.groups.clone(),
allow_relay: self.allow_relay, allow_relay: self.grant.allow_relay,
expiry_unix: self.expiry_unix, expiry_unix: self.expiry_unix,
allowed_proxy_cidrs: self.allowed_proxy_cidrs.clone(), allowed_proxy_cidrs: self.grant.allowed_proxy_cidrs.clone(),
reusable: Some(self.reusable), reusable: Some(self.grant.reusable),
public_key_fingerprint: CredentialManager::public_key_fingerprint(&self.pubkey) public_key_fingerprint: CredentialManager::public_key_fingerprint(&self.pubkey)
.unwrap_or_default(), .unwrap_or_default(),
} }
@@ -97,20 +150,22 @@ impl CredentialEntry {
.try_into() .try_into()
.map_err(|_| format!("credential_secret for {credential_id} must contain 32 bytes"))?; .map_err(|_| format!("credential_secret for {credential_id} must contain 32 bytes"))?;
let private = StaticSecret::from(private_bytes); let private = StaticSecret::from(private_bytes);
let mut allowed_proxy_cidrs = Vec::with_capacity(entry.allowed_proxy_cidrs.len()); let grant = CredentialGrant::new(
for cidr in &entry.allowed_proxy_cidrs { entry.groups.clone(),
let cidr = cidr.trim(); entry.allow_relay,
cidr.parse::<cidr::IpCidr>() entry.allowed_proxy_cidrs.clone(),
.map_err(|_| format!("invalid allowed_proxy_cidr for {credential_id}: {cidr}"))?; entry.reusable,
allowed_proxy_cidrs.push(cidr.to_owned()); )
} .map_err(|error| {
format!(
"invalid allowed_proxy_cidr for {credential_id}: {}",
error.0
)
})?;
Ok(Self { Ok(Self {
pubkey: BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()), pubkey: BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()),
secret: BASE64_STANDARD.encode(private.as_bytes()), secret: BASE64_STANDARD.encode(private.as_bytes()),
groups: entry.groups.clone(), grant,
allow_relay: entry.allow_relay,
allowed_proxy_cidrs,
reusable: entry.reusable,
expiry_unix: entry.expiry_unix, expiry_unix: entry.expiry_unix,
created_at_unix: 0, created_at_unix: 0,
}) })
@@ -192,7 +247,9 @@ impl CredentialManager {
pub fn from_storage(storage: Arc<dyn CredentialStorage>) -> Self { pub fn from_storage(storage: Arc<dyn CredentialStorage>) -> Self {
let loaded = match storage.load() { let loaded = match storage.load() {
Ok(Some(serialized)) => serde_json::from_str(&serialized).map_err(anyhow::Error::from), Ok(Some(serialized)) => serde_json::from_str(&serialized)
.map_err(anyhow::Error::from)
.and_then(Self::normalize_loaded_entries),
Ok(None) => Ok(HashMap::new()), Ok(None) => Ok(HashMap::new()),
Err(error) => Err(error), Err(error) => Err(error),
}; };
@@ -213,15 +270,29 @@ impl CredentialManager {
} }
} }
fn normalize_loaded_entries(
mut entries: HashMap<String, CredentialEntry>,
) -> anyhow::Result<HashMap<String, CredentialEntry>> {
for (credential_id, entry) in &mut entries {
entry.grant.normalize().map_err(|error| {
anyhow::anyhow!("invalid stored credential {credential_id}: {error}")
})?;
}
Ok(entries)
}
pub fn generate_credential_with_options( pub fn generate_credential_with_options(
&self, &self,
groups: Vec<String>, options: CredentialCreateOptions,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
ttl: Duration,
credential_id: Option<String>,
reusable: bool,
) -> Result<GeneratedCredential, String> { ) -> Result<GeneratedCredential, String> {
let CredentialCreateOptions {
groups,
allow_relay,
allowed_proxy_cidrs,
ttl,
credential_id,
reusable,
} = options;
self.ensure_storage_available() self.ensure_storage_available()
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
@@ -254,15 +325,11 @@ impl CredentialManager {
} }
} }
}; };
let grant = CredentialGrant::new(groups, allow_relay, allowed_proxy_cidrs, reusable)
.map_err(|error| error.to_string())?;
let (entry, secret) = loop { let (entry, secret) = loop {
let generated = Self::build_entry( let generated = Self::build_entry(grant.clone(), ttl);
groups.clone(),
allow_relay,
allowed_proxy_cidrs.clone(),
reusable,
ttl,
);
let public_key_in_use = updated let public_key_in_use = updated
.values() .values()
.chain(Self::managed_values(&state)) .chain(Self::managed_values(&state))
@@ -285,13 +352,7 @@ impl CredentialManager {
}) })
} }
fn build_entry( fn build_entry(grant: CredentialGrant, ttl: Duration) -> (CredentialEntry, String) {
groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
reusable: bool,
ttl: Duration,
) -> (CredentialEntry, String) {
let private = StaticSecret::random_from_rng(rand::rngs::OsRng); let private = StaticSecret::random_from_rng(rand::rngs::OsRng);
let public = PublicKey::from(&private); let public = PublicKey::from(&private);
let pubkey = BASE64_STANDARD.encode(public.as_bytes()); let pubkey = BASE64_STANDARD.encode(public.as_bytes());
@@ -306,10 +367,7 @@ impl CredentialManager {
let entry = CredentialEntry { let entry = CredentialEntry {
pubkey, pubkey,
secret: secret.clone(), secret: secret.clone(),
groups, grant,
allow_relay,
allowed_proxy_cidrs,
reusable,
expiry_unix, expiry_unix,
created_at_unix: now, created_at_unix: now,
}; };
@@ -335,17 +393,11 @@ impl CredentialManager {
&self, &self,
public_key: [u8; 32], public_key: [u8; 32],
groups: Vec<String>, groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
reusable: bool,
) -> Result<uuid::Uuid, String> { ) -> Result<uuid::Uuid, String> {
let entry = CredentialEntry { let entry = CredentialEntry {
pubkey: BASE64_STANDARD.encode(public_key), pubkey: BASE64_STANDARD.encode(public_key),
secret: String::new(), secret: String::new(),
groups, grant: CredentialGrant::for_attached_peer(groups),
allow_relay,
allowed_proxy_cidrs,
reusable,
expiry_unix: i64::MAX, expiry_unix: i64::MAX,
created_at_unix: current_unix_timestamp(), created_at_unix: current_unix_timestamp(),
}; };
@@ -376,10 +428,10 @@ impl CredentialManager {
) -> Option<bool> { ) -> Option<bool> {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
let credential = state.ephemeral.get_mut(&credential_id)?; let credential = state.ephemeral.get_mut(&credential_id)?;
if credential.groups == groups { if credential.grant.groups == groups {
return Some(false); return Some(false);
} }
credential.groups = groups; credential.grant.groups = groups;
Some(true) Some(true)
} }
@@ -416,13 +468,12 @@ impl CredentialManager {
.try_into() .try_into()
.map_err(|_| "credential_secret must contain 32 bytes".to_string())?; .map_err(|_| "credential_secret must contain 32 bytes".to_string())?;
let private = StaticSecret::from(private_bytes); let private = StaticSecret::from(private_bytes);
let grant = CredentialGrant::new(groups, allow_relay, allowed_proxy_cidrs, reusable)
.map_err(|error| error.to_string())?;
let entry = CredentialEntry { let entry = CredentialEntry {
pubkey: BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()), pubkey: BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()),
secret: BASE64_STANDARD.encode(private.as_bytes()), secret: BASE64_STANDARD.encode(private.as_bytes()),
groups, grant,
allow_relay,
allowed_proxy_cidrs,
reusable,
expiry_unix, expiry_unix,
created_at_unix: current_unix_timestamp(), created_at_unix: current_unix_timestamp(),
}; };
@@ -455,10 +506,7 @@ impl CredentialManager {
let changed = state.base.get(&credential_id).is_none_or(|existing| { let changed = state.base.get(&credential_id).is_none_or(|existing| {
existing.secret != entry.secret existing.secret != entry.secret
|| existing.pubkey != entry.pubkey || existing.pubkey != entry.pubkey
|| existing.groups != entry.groups || existing.grant != entry.grant
|| existing.allow_relay != entry.allow_relay
|| existing.allowed_proxy_cidrs != entry.allowed_proxy_cidrs
|| existing.reusable != entry.reusable
|| existing.expiry_unix != entry.expiry_unix || existing.expiry_unix != entry.expiry_unix
}); });
if !changed { if !changed {
@@ -722,7 +770,43 @@ mod tests {
let entry = CredentialEntry::from_managed(&credential).unwrap(); let entry = CredentialEntry::from_managed(&credential).unwrap();
assert_eq!(entry.allowed_proxy_cidrs, ["10.0.0.0/24"]); assert_eq!(entry.grant.allowed_proxy_cidrs, ["10.0.0.0/24"]);
}
#[test]
fn generated_and_imported_credentials_normalize_allowed_proxy_cidrs() {
let source = CredentialManager::new();
let generated = source
.generate_credential_with_options(CredentialCreateOptions {
groups: Vec::new(),
allow_relay: false,
allowed_proxy_cidrs: vec![" 10.0.0.0/24 ".to_owned()],
ttl: Duration::from_secs(3600),
credential_id: None,
reusable: true,
})
.unwrap();
assert_eq!(
source.list_credentials()[0].allowed_proxy_cidrs,
["10.0.0.0/24"]
);
let target = CredentialManager::new();
target
.upsert_credential(CredentialUpsertOptions {
credential_id: "imported".to_owned(),
credential_secret: generated.secret,
groups: Vec::new(),
allow_relay: false,
allowed_proxy_cidrs: vec![" 192.168.0.0/16 ".to_owned()],
expiry_unix: generated.expiry_unix,
reusable: true,
})
.unwrap();
assert_eq!(
target.list_credentials()[0].allowed_proxy_cidrs,
["192.168.0.0/16"]
);
} }
impl CredentialManager { impl CredentialManager {
@@ -733,14 +817,14 @@ mod tests {
allowed_proxy_cidrs: Vec<String>, allowed_proxy_cidrs: Vec<String>,
ttl: Duration, ttl: Duration,
) -> GeneratedCredential { ) -> GeneratedCredential {
self.generate_credential_with_options( self.generate_credential_with_options(CredentialCreateOptions {
groups, groups,
allow_relay, allow_relay,
allowed_proxy_cidrs, allowed_proxy_cidrs,
ttl, ttl,
None, credential_id: None,
true, reusable: true,
) })
.unwrap() .unwrap()
} }
@@ -752,14 +836,14 @@ mod tests {
ttl: Duration, ttl: Duration,
credential_id: Option<String>, credential_id: Option<String>,
) -> GeneratedCredential { ) -> GeneratedCredential {
self.generate_credential_with_options( self.generate_credential_with_options(CredentialCreateOptions {
groups, groups,
allow_relay, allow_relay,
allowed_proxy_cidrs, allowed_proxy_cidrs,
ttl, ttl,
credential_id, credential_id,
true, reusable: true,
) })
.unwrap() .unwrap()
} }
} }
@@ -780,6 +864,24 @@ mod tests {
} }
} }
fn credential_storage_with_proxy_cidr(
allowed_proxy_cidr: &str,
) -> (Arc<MemoryCredentialStorage>, String) {
let storage = Arc::new(MemoryCredentialStorage::default());
let manager = CredentialManager::from_storage(storage.clone());
let generated = manager.generate_credential(
Vec::new(),
false,
vec!["10.0.0.0/24".to_owned()],
Duration::from_secs(3600),
);
let serialized = storage.serialized.lock().unwrap().clone().unwrap();
let mut snapshot: serde_json::Value = serde_json::from_str(&serialized).unwrap();
snapshot[&generated.credential_id]["allowed_proxy_cidrs"][0] = allowed_proxy_cidr.into();
*storage.serialized.lock().unwrap() = Some(serde_json::to_string(&snapshot).unwrap());
(storage, generated.credential_id)
}
struct FailOnceCredentialStorage { struct FailOnceCredentialStorage {
serialized: Mutex<Option<String>>, serialized: Mutex<Option<String>>,
fail_next_store: Mutex<bool>, fail_next_store: Mutex<bool>,
@@ -887,14 +989,14 @@ mod tests {
fn upsert_credential_preserves_key_attributes_and_storage() { fn upsert_credential_preserves_key_attributes_and_storage() {
let source = CredentialManager::new(); let source = CredentialManager::new();
let generated = source let generated = source
.generate_credential_with_options( .generate_credential_with_options(CredentialCreateOptions {
vec!["users".to_string()], groups: vec!["users".to_string()],
false, allow_relay: false,
vec!["10.0.0.0/8".to_string()], allowed_proxy_cidrs: vec!["10.0.0.0/8".to_string()],
Duration::from_secs(3600), ttl: Duration::from_secs(3600),
Some("shared-id".to_string()), credential_id: Some("shared-id".to_string()),
false, reusable: false,
) })
.unwrap(); .unwrap();
let source_info = source.list_credentials().remove(0); let source_info = source.list_credentials().remove(0);
let options = CredentialUpsertOptions { let options = CredentialUpsertOptions {
@@ -1011,6 +1113,67 @@ mod tests {
assert!(reloaded.list_credentials().is_empty()); assert!(reloaded.list_credentials().is_empty());
} }
#[test]
fn stored_credentials_normalize_allowed_proxy_cidrs_on_load() {
let (storage, credential_id) = credential_storage_with_proxy_cidr(" 10.0.0.0/24 ");
let manager = CredentialManager::from_storage(storage);
assert_eq!(manager.list_credentials()[0].credential_id, credential_id);
assert_eq!(
manager.list_credentials()[0].allowed_proxy_cidrs,
["10.0.0.0/24"]
);
}
#[test]
fn stored_credentials_with_invalid_proxy_cidr_fail_closed() {
let (storage, credential_id) = credential_storage_with_proxy_cidr("not-a-cidr");
let manager = CredentialManager::from_storage(storage);
assert!(manager.list_credentials().is_empty());
let error = manager
.install_initial_managed_credentials(&[])
.unwrap_err();
assert!(error.contains(&credential_id), "{error}");
assert!(error.contains("invalid allowed_proxy_cidr"), "{error}");
}
#[test]
fn credential_storage_schema_remains_flat() {
let storage = Arc::new(MemoryCredentialStorage::default());
let manager = CredentialManager::from_storage(storage.clone());
manager.generate_credential(
vec!["ops".to_owned()],
true,
vec!["10.0.0.0/24".to_owned()],
Duration::from_secs(3600),
);
let serialized = storage.serialized.lock().unwrap().clone().unwrap();
let mut snapshot: serde_json::Value = serde_json::from_str(&serialized).unwrap();
let entry = snapshot.as_object().unwrap().values().next().unwrap();
assert!(entry.get("grant").is_none());
assert_eq!(entry["groups"][0], "ops");
assert_eq!(entry["allowed_proxy_cidrs"][0], "10.0.0.0/24");
assert_eq!(entry["allow_relay"], true);
assert_eq!(entry["reusable"], true);
snapshot
.as_object_mut()
.unwrap()
.values_mut()
.next()
.unwrap()
.as_object_mut()
.unwrap()
.remove("reusable");
let legacy: HashMap<String, CredentialEntry> = serde_json::from_value(snapshot).unwrap();
assert!(legacy.values().next().unwrap().grant.reusable);
}
#[test] #[test]
fn malformed_storage_is_fail_closed() { fn malformed_storage_is_fail_closed() {
let storage = Arc::new(MemoryCredentialStorage { let storage = Arc::new(MemoryCredentialStorage {
@@ -1031,7 +1194,7 @@ mod tests {
let public = *PublicKey::from(&private).as_bytes(); let public = *PublicKey::from(&private).as_bytes();
let credential_id = manager let credential_id = manager
.register_ephemeral_credential(public, vec!["ops".to_owned()], false, Vec::new(), false) .register_ephemeral_credential(public, vec!["ops".to_owned()])
.unwrap(); .unwrap();
assert!(manager.is_pubkey_trusted(&public)); assert!(manager.is_pubkey_trusted(&public));
@@ -1096,25 +1259,19 @@ mod tests {
.unwrap(); .unwrap();
let error = manager let error = manager
.generate_credential_with_options( .generate_credential_with_options(CredentialCreateOptions {
Vec::new(), groups: Vec::new(),
false, allow_relay: false,
Vec::new(), allowed_proxy_cidrs: Vec::new(),
Duration::from_secs(60), ttl: Duration::from_secs(60),
Some("pending".to_owned()), credential_id: Some("pending".to_owned()),
true, reusable: true,
) })
.unwrap_err(); .unwrap_err();
assert!(error.contains("managed by configuration")); assert!(error.contains("managed by configuration"));
assert!( assert!(
manager manager
.register_ephemeral_credential( .register_ephemeral_credential(*public.as_bytes(), Vec::new())
*public.as_bytes(),
Vec::new(),
false,
Vec::new(),
false,
)
.is_err() .is_err()
); );
assert!(!manager.is_pubkey_trusted(public.as_bytes())); assert!(!manager.is_pubkey_trusted(public.as_bytes()));
@@ -1122,13 +1279,7 @@ mod tests {
drop(replacement); drop(replacement);
assert!( assert!(
manager manager
.register_ephemeral_credential( .register_ephemeral_credential(*public.as_bytes(), Vec::new())
*public.as_bytes(),
Vec::new(),
false,
Vec::new(),
false,
)
.is_ok() .is_ok()
); );
} }
@@ -1146,14 +1297,14 @@ mod tests {
.unwrap(); .unwrap();
let error = manager let error = manager
.generate_credential_with_options( .generate_credential_with_options(CredentialCreateOptions {
Vec::new(), groups: Vec::new(),
false, allow_relay: false,
Vec::new(), allowed_proxy_cidrs: Vec::new(),
Duration::from_secs(60), ttl: Duration::from_secs(60),
Some("managed".to_owned()), credential_id: Some("managed".to_owned()),
true, reusable: true,
) })
.unwrap_err(); .unwrap_err();
assert!(error.contains("managed by configuration")); assert!(error.contains("managed by configuration"));
+1 -10
View File
@@ -1166,19 +1166,10 @@ impl PeerManagerCore {
&self, &self,
public_key: [u8; 32], public_key: [u8; 32],
groups: Vec<String>, groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
reusable: bool,
) -> anyhow::Result<uuid::Uuid> { ) -> anyhow::Result<uuid::Uuid> {
let credential_id = self let credential_id = self
.credential_manager() .credential_manager()
.register_ephemeral_credential( .register_ephemeral_credential(public_key, groups)
public_key,
groups,
allow_relay,
allowed_proxy_cidrs,
reusable,
)
.map_err(anyhow::Error::msg)?; .map_err(anyhow::Error::msg)?;
self.notify_credential_changed(); self.notify_credential_changed();
Ok(credential_id) Ok(credential_id)