2026-05-15 15:08:20 +02:00
|
|
|
use geth_types::{Capability, PrincipalId, ResourceId, SecretId, UnixMillis};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
pub const RESOURCE_SECRET_SIGNATURE_NAMESPACE: &str = "geth.resource-secret.v1@geth.local";
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct ResourceMasterSecret {
|
|
|
|
|
pub id: SecretId,
|
|
|
|
|
pub resource: ResourceId,
|
|
|
|
|
pub epoch: u64,
|
|
|
|
|
pub created_at: UnixMillis,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct ResourceKeyEnvelope {
|
|
|
|
|
pub secret: SecretId,
|
|
|
|
|
pub recipient: PrincipalId,
|
|
|
|
|
pub epoch: u64,
|
|
|
|
|
pub algorithm: String,
|
|
|
|
|
pub ciphertext: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct BearerAccess {
|
|
|
|
|
pub secret: SecretId,
|
|
|
|
|
pub resource: ResourceId,
|
|
|
|
|
pub capabilities: Vec<Capability>,
|
|
|
|
|
pub expires_at: Option<UnixMillis>,
|
|
|
|
|
pub may_delegate: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BearerAccess {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn resource_scoped(
|
|
|
|
|
secret: SecretId,
|
|
|
|
|
resource: ResourceId,
|
|
|
|
|
capabilities: Vec<Capability>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
secret,
|
|
|
|
|
resource,
|
|
|
|
|
capabilities,
|
|
|
|
|
expires_at: None,
|
|
|
|
|
may_delegate: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-17 02:58:58 +02:00
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum SecretsError {
|
|
|
|
|
#[error("bearer access must grant at least one capability")]
|
|
|
|
|
EmptyBearerCapabilities,
|
|
|
|
|
#[error("capability is not allowed for bearer access: {0}")]
|
|
|
|
|
ForbiddenBearerCapability(String),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn validate_bearer_capabilities(capabilities: &[Capability]) -> Result<(), SecretsError> {
|
|
|
|
|
if capabilities.is_empty() {
|
|
|
|
|
return Err(SecretsError::EmptyBearerCapabilities);
|
|
|
|
|
}
|
|
|
|
|
for capability in capabilities {
|
|
|
|
|
let capability = capability.as_str();
|
|
|
|
|
if matches!(
|
|
|
|
|
capability,
|
|
|
|
|
"auth.delegate"
|
|
|
|
|
| "auth.revoke"
|
|
|
|
|
| "trust.modify"
|
|
|
|
|
| "ssh_proxy.admin_shell"
|
|
|
|
|
| "node.enroll"
|
|
|
|
|
) {
|
|
|
|
|
return Err(SecretsError::ForbiddenBearerCapability(
|
|
|
|
|
capability.to_owned(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bearer_capabilities_reject_trust_mutation() {
|
|
|
|
|
assert!(validate_bearer_capabilities(&["kv.read".into(), "kv.write".into()]).is_ok());
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
validate_bearer_capabilities(&[]),
|
|
|
|
|
Err(SecretsError::EmptyBearerCapabilities)
|
|
|
|
|
));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
validate_bearer_capabilities(&["auth.delegate".into()]),
|
|
|
|
|
Err(SecretsError::ForbiddenBearerCapability(_))
|
|
|
|
|
));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
validate_bearer_capabilities(&["node.enroll".into()]),
|
|
|
|
|
Err(SecretsError::ForbiddenBearerCapability(_))
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|