mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-29 15:29:14 +00:00
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:
@@ -22,6 +22,14 @@ operation transition.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
An attached peer is an ordinary `PeerManagerCore` connected to another
|
||||
|
||||
@@ -4,7 +4,10 @@ use easytier_proto::api::manage::{
|
||||
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 {
|
||||
let default_config = TomlConfig::default();
|
||||
@@ -121,15 +124,7 @@ pub fn network_config_from_toml(config: &TomlConfig) -> NetworkConfig {
|
||||
result.managed_credentials = config
|
||||
.get_managed_credentials()
|
||||
.into_iter()
|
||||
.map(|credential| 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),
|
||||
})
|
||||
.map(managed_credential_to_proto)
|
||||
.collect();
|
||||
|
||||
let flags = config.get_flags();
|
||||
|
||||
@@ -51,6 +51,34 @@ pub fn add_proxy_network_to_config(
|
||||
pub type NetworkingMethod = easytier_proto::api::manage::NetworkingMethod;
|
||||
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 {
|
||||
fn gen_config(&self) -> Result<TomlConfigLoader, 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(
|
||||
self.managed_credentials
|
||||
.iter()
|
||||
.map(|credential| 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),
|
||||
})
|
||||
.map(managed_credential_from_proto)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
@@ -624,15 +644,7 @@ impl NetworkConfigExt for NetworkConfig {
|
||||
result.managed_credentials = config
|
||||
.get_managed_credentials()
|
||||
.into_iter()
|
||||
.map(|credential| 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),
|
||||
})
|
||||
.map(managed_credential_to_proto)
|
||||
.collect();
|
||||
let flags = config.get_flags();
|
||||
let default_flags = default_config.get_flags();
|
||||
|
||||
@@ -157,14 +157,7 @@ where
|
||||
let generated = self
|
||||
.peer_manager
|
||||
.credential_manager()
|
||||
.generate_credential_with_options(
|
||||
options.groups,
|
||||
options.allow_relay,
|
||||
options.allowed_proxy_cidrs,
|
||||
options.ttl,
|
||||
options.credential_id,
|
||||
options.reusable,
|
||||
)
|
||||
.generate_credential_with_options(options)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
self.peer_manager.notify_credential_changed();
|
||||
Ok(generated)
|
||||
|
||||
@@ -8,9 +8,10 @@ use easytier_proto::api::config::{
|
||||
|
||||
use crate::{
|
||||
config::{
|
||||
api_input::managed_credential_from_proto,
|
||||
peers::AclRuleConfig,
|
||||
runtime::CoreInstanceRuntimeConfig,
|
||||
toml::{ConfigLoader as _, ManagedCredentialConfig, TomlConfig},
|
||||
toml::{ConfigLoader as _, TomlConfig},
|
||||
},
|
||||
instance::{CoreInstance, CoreInstanceConfig, CoreInstanceHost, CoreInstanceState},
|
||||
peers::credential_manager::CredentialManager,
|
||||
@@ -164,15 +165,7 @@ where
|
||||
let entries = managed
|
||||
.entries
|
||||
.iter()
|
||||
.map(|credential| 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),
|
||||
})
|
||||
.map(managed_credential_from_proto)
|
||||
.collect::<Vec<_>>();
|
||||
let replacement = credential_manager
|
||||
.validate_managed_credentials(&entries)
|
||||
|
||||
@@ -75,13 +75,8 @@ impl AttachedCredentialRegistration {
|
||||
network_runtime_config.snapshot().as_ref(),
|
||||
&configured_groups,
|
||||
);
|
||||
let credential_id = network_peer_manager.register_ephemeral_credential(
|
||||
public_key,
|
||||
groups,
|
||||
false,
|
||||
Vec::new(),
|
||||
false,
|
||||
)?;
|
||||
let credential_id =
|
||||
network_peer_manager.register_ephemeral_credential(public_key, groups)?;
|
||||
let task_peer_manager = network_peer_manager.clone();
|
||||
let policy_task = tokio::spawn(async move {
|
||||
while peer_changes.changed().await.is_ok() {
|
||||
|
||||
@@ -47,15 +47,68 @@ pub struct CredentialUpsertOptions {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct CredentialEntry {
|
||||
pubkey: String,
|
||||
#[serde(default)]
|
||||
secret: String,
|
||||
struct CredentialGrant {
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
#[serde(default = "default_true")]
|
||||
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,
|
||||
created_at_unix: i64,
|
||||
}
|
||||
@@ -68,22 +121,22 @@ impl CredentialEntry {
|
||||
fn to_trusted_credential(&self) -> Option<TrustedCredentialPubkey> {
|
||||
Some(TrustedCredentialPubkey {
|
||||
pubkey: CredentialManager::decode_pubkey_b64(&self.pubkey)?,
|
||||
groups: self.groups.clone(),
|
||||
allow_relay: self.allow_relay,
|
||||
groups: self.grant.groups.clone(),
|
||||
allow_relay: self.grant.allow_relay,
|
||||
expiry_unix: self.expiry_unix,
|
||||
allowed_proxy_cidrs: self.allowed_proxy_cidrs.clone(),
|
||||
reusable: Some(self.reusable),
|
||||
allowed_proxy_cidrs: self.grant.allowed_proxy_cidrs.clone(),
|
||||
reusable: Some(self.grant.reusable),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_credential_info(&self, credential_id: &str) -> CredentialInfo {
|
||||
CredentialInfo {
|
||||
credential_id: credential_id.to_string(),
|
||||
groups: self.groups.clone(),
|
||||
allow_relay: self.allow_relay,
|
||||
groups: self.grant.groups.clone(),
|
||||
allow_relay: self.grant.allow_relay,
|
||||
expiry_unix: self.expiry_unix,
|
||||
allowed_proxy_cidrs: self.allowed_proxy_cidrs.clone(),
|
||||
reusable: Some(self.reusable),
|
||||
allowed_proxy_cidrs: self.grant.allowed_proxy_cidrs.clone(),
|
||||
reusable: Some(self.grant.reusable),
|
||||
public_key_fingerprint: CredentialManager::public_key_fingerprint(&self.pubkey)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
@@ -97,20 +150,22 @@ impl CredentialEntry {
|
||||
.try_into()
|
||||
.map_err(|_| format!("credential_secret for {credential_id} must contain 32 bytes"))?;
|
||||
let private = StaticSecret::from(private_bytes);
|
||||
let mut allowed_proxy_cidrs = Vec::with_capacity(entry.allowed_proxy_cidrs.len());
|
||||
for cidr in &entry.allowed_proxy_cidrs {
|
||||
let cidr = cidr.trim();
|
||||
cidr.parse::<cidr::IpCidr>()
|
||||
.map_err(|_| format!("invalid allowed_proxy_cidr for {credential_id}: {cidr}"))?;
|
||||
allowed_proxy_cidrs.push(cidr.to_owned());
|
||||
}
|
||||
let grant = CredentialGrant::new(
|
||||
entry.groups.clone(),
|
||||
entry.allow_relay,
|
||||
entry.allowed_proxy_cidrs.clone(),
|
||||
entry.reusable,
|
||||
)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"invalid allowed_proxy_cidr for {credential_id}: {}",
|
||||
error.0
|
||||
)
|
||||
})?;
|
||||
Ok(Self {
|
||||
pubkey: BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()),
|
||||
secret: BASE64_STANDARD.encode(private.as_bytes()),
|
||||
groups: entry.groups.clone(),
|
||||
allow_relay: entry.allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
reusable: entry.reusable,
|
||||
grant,
|
||||
expiry_unix: entry.expiry_unix,
|
||||
created_at_unix: 0,
|
||||
})
|
||||
@@ -192,7 +247,9 @@ impl CredentialManager {
|
||||
|
||||
pub fn from_storage(storage: Arc<dyn CredentialStorage>) -> Self {
|
||||
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()),
|
||||
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(
|
||||
&self,
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
ttl: Duration,
|
||||
credential_id: Option<String>,
|
||||
reusable: bool,
|
||||
options: CredentialCreateOptions,
|
||||
) -> Result<GeneratedCredential, String> {
|
||||
let CredentialCreateOptions {
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
ttl,
|
||||
credential_id,
|
||||
reusable,
|
||||
} = options;
|
||||
self.ensure_storage_available()
|
||||
.map_err(|error| error.to_string())?;
|
||||
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 generated = Self::build_entry(
|
||||
groups.clone(),
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs.clone(),
|
||||
reusable,
|
||||
ttl,
|
||||
);
|
||||
let generated = Self::build_entry(grant.clone(), ttl);
|
||||
let public_key_in_use = updated
|
||||
.values()
|
||||
.chain(Self::managed_values(&state))
|
||||
@@ -285,13 +352,7 @@ impl CredentialManager {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_entry(
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
reusable: bool,
|
||||
ttl: Duration,
|
||||
) -> (CredentialEntry, String) {
|
||||
fn build_entry(grant: CredentialGrant, ttl: Duration) -> (CredentialEntry, String) {
|
||||
let private = StaticSecret::random_from_rng(rand::rngs::OsRng);
|
||||
let public = PublicKey::from(&private);
|
||||
let pubkey = BASE64_STANDARD.encode(public.as_bytes());
|
||||
@@ -306,10 +367,7 @@ impl CredentialManager {
|
||||
let entry = CredentialEntry {
|
||||
pubkey,
|
||||
secret: secret.clone(),
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
reusable,
|
||||
grant,
|
||||
expiry_unix,
|
||||
created_at_unix: now,
|
||||
};
|
||||
@@ -335,17 +393,11 @@ impl CredentialManager {
|
||||
&self,
|
||||
public_key: [u8; 32],
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
reusable: bool,
|
||||
) -> Result<uuid::Uuid, String> {
|
||||
let entry = CredentialEntry {
|
||||
pubkey: BASE64_STANDARD.encode(public_key),
|
||||
secret: String::new(),
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
reusable,
|
||||
grant: CredentialGrant::for_attached_peer(groups),
|
||||
expiry_unix: i64::MAX,
|
||||
created_at_unix: current_unix_timestamp(),
|
||||
};
|
||||
@@ -376,10 +428,10 @@ impl CredentialManager {
|
||||
) -> Option<bool> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
let credential = state.ephemeral.get_mut(&credential_id)?;
|
||||
if credential.groups == groups {
|
||||
if credential.grant.groups == groups {
|
||||
return Some(false);
|
||||
}
|
||||
credential.groups = groups;
|
||||
credential.grant.groups = groups;
|
||||
Some(true)
|
||||
}
|
||||
|
||||
@@ -416,13 +468,12 @@ impl CredentialManager {
|
||||
.try_into()
|
||||
.map_err(|_| "credential_secret must contain 32 bytes".to_string())?;
|
||||
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 {
|
||||
pubkey: BASE64_STANDARD.encode(PublicKey::from(&private).as_bytes()),
|
||||
secret: BASE64_STANDARD.encode(private.as_bytes()),
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
reusable,
|
||||
grant,
|
||||
expiry_unix,
|
||||
created_at_unix: current_unix_timestamp(),
|
||||
};
|
||||
@@ -455,10 +506,7 @@ impl CredentialManager {
|
||||
let changed = state.base.get(&credential_id).is_none_or(|existing| {
|
||||
existing.secret != entry.secret
|
||||
|| existing.pubkey != entry.pubkey
|
||||
|| existing.groups != entry.groups
|
||||
|| existing.allow_relay != entry.allow_relay
|
||||
|| existing.allowed_proxy_cidrs != entry.allowed_proxy_cidrs
|
||||
|| existing.reusable != entry.reusable
|
||||
|| existing.grant != entry.grant
|
||||
|| existing.expiry_unix != entry.expiry_unix
|
||||
});
|
||||
if !changed {
|
||||
@@ -722,7 +770,43 @@ mod tests {
|
||||
|
||||
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 {
|
||||
@@ -733,14 +817,14 @@ mod tests {
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
ttl: Duration,
|
||||
) -> GeneratedCredential {
|
||||
self.generate_credential_with_options(
|
||||
self.generate_credential_with_options(CredentialCreateOptions {
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
ttl,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
credential_id: None,
|
||||
reusable: true,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -752,14 +836,14 @@ mod tests {
|
||||
ttl: Duration,
|
||||
credential_id: Option<String>,
|
||||
) -> GeneratedCredential {
|
||||
self.generate_credential_with_options(
|
||||
self.generate_credential_with_options(CredentialCreateOptions {
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
ttl,
|
||||
credential_id,
|
||||
true,
|
||||
)
|
||||
reusable: true,
|
||||
})
|
||||
.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 {
|
||||
serialized: Mutex<Option<String>>,
|
||||
fail_next_store: Mutex<bool>,
|
||||
@@ -887,14 +989,14 @@ mod tests {
|
||||
fn upsert_credential_preserves_key_attributes_and_storage() {
|
||||
let source = CredentialManager::new();
|
||||
let generated = source
|
||||
.generate_credential_with_options(
|
||||
vec!["users".to_string()],
|
||||
false,
|
||||
vec!["10.0.0.0/8".to_string()],
|
||||
Duration::from_secs(3600),
|
||||
Some("shared-id".to_string()),
|
||||
false,
|
||||
)
|
||||
.generate_credential_with_options(CredentialCreateOptions {
|
||||
groups: vec!["users".to_string()],
|
||||
allow_relay: false,
|
||||
allowed_proxy_cidrs: vec!["10.0.0.0/8".to_string()],
|
||||
ttl: Duration::from_secs(3600),
|
||||
credential_id: Some("shared-id".to_string()),
|
||||
reusable: false,
|
||||
})
|
||||
.unwrap();
|
||||
let source_info = source.list_credentials().remove(0);
|
||||
let options = CredentialUpsertOptions {
|
||||
@@ -1011,6 +1113,67 @@ mod tests {
|
||||
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]
|
||||
fn malformed_storage_is_fail_closed() {
|
||||
let storage = Arc::new(MemoryCredentialStorage {
|
||||
@@ -1031,7 +1194,7 @@ mod tests {
|
||||
let public = *PublicKey::from(&private).as_bytes();
|
||||
|
||||
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();
|
||||
|
||||
assert!(manager.is_pubkey_trusted(&public));
|
||||
@@ -1096,25 +1259,19 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let error = manager
|
||||
.generate_credential_with_options(
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
Duration::from_secs(60),
|
||||
Some("pending".to_owned()),
|
||||
true,
|
||||
)
|
||||
.generate_credential_with_options(CredentialCreateOptions {
|
||||
groups: Vec::new(),
|
||||
allow_relay: false,
|
||||
allowed_proxy_cidrs: Vec::new(),
|
||||
ttl: Duration::from_secs(60),
|
||||
credential_id: Some("pending".to_owned()),
|
||||
reusable: true,
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(error.contains("managed by configuration"));
|
||||
assert!(
|
||||
manager
|
||||
.register_ephemeral_credential(
|
||||
*public.as_bytes(),
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
false,
|
||||
)
|
||||
.register_ephemeral_credential(*public.as_bytes(), Vec::new())
|
||||
.is_err()
|
||||
);
|
||||
assert!(!manager.is_pubkey_trusted(public.as_bytes()));
|
||||
@@ -1122,13 +1279,7 @@ mod tests {
|
||||
drop(replacement);
|
||||
assert!(
|
||||
manager
|
||||
.register_ephemeral_credential(
|
||||
*public.as_bytes(),
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
false,
|
||||
)
|
||||
.register_ephemeral_credential(*public.as_bytes(), Vec::new())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
@@ -1146,14 +1297,14 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let error = manager
|
||||
.generate_credential_with_options(
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
Duration::from_secs(60),
|
||||
Some("managed".to_owned()),
|
||||
true,
|
||||
)
|
||||
.generate_credential_with_options(CredentialCreateOptions {
|
||||
groups: Vec::new(),
|
||||
allow_relay: false,
|
||||
allowed_proxy_cidrs: Vec::new(),
|
||||
ttl: Duration::from_secs(60),
|
||||
credential_id: Some("managed".to_owned()),
|
||||
reusable: true,
|
||||
})
|
||||
.unwrap_err();
|
||||
assert!(error.contains("managed by configuration"));
|
||||
|
||||
|
||||
@@ -1166,19 +1166,10 @@ impl PeerManagerCore {
|
||||
&self,
|
||||
public_key: [u8; 32],
|
||||
groups: Vec<String>,
|
||||
allow_relay: bool,
|
||||
allowed_proxy_cidrs: Vec<String>,
|
||||
reusable: bool,
|
||||
) -> anyhow::Result<uuid::Uuid> {
|
||||
let credential_id = self
|
||||
.credential_manager()
|
||||
.register_ephemeral_credential(
|
||||
public_key,
|
||||
groups,
|
||||
allow_relay,
|
||||
allowed_proxy_cidrs,
|
||||
reusable,
|
||||
)
|
||||
.register_ephemeral_credential(public_key, groups)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
self.notify_credential_changed();
|
||||
Ok(credential_id)
|
||||
|
||||
Reference in New Issue
Block a user