mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-02 17:15:43 +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
|
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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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"));
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user