#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::collections::BTreeMap;
pub mod human;
pub mod id;
pub mod resolver;
#[cfg(any(test, feature = "testing"))]
#[cfg_attr(docsrs, doc(cfg(feature = "testing")))]
pub mod testing;
pub mod workload;
pub use human::HumanPrincipal;
pub use id::*;
pub use resolver::{CliResolver, CliResolverBuilder, PrincipalResolver};
pub use workload::{TrustDomain, WorkloadId, WorkloadPrincipal};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Principal {
Human(HumanPrincipal),
Workload(WorkloadPrincipal),
}
impl Principal {
pub fn tenant_id(&self) -> &TenantId {
match self {
Self::Human(h) => &h.tenant_id,
Self::Workload(w) => &w.tenant_id,
}
}
pub fn attributes(&self) -> &BTreeMap<String, serde_json::Value> {
match self {
Self::Human(h) => &h.attributes,
Self::Workload(w) => &w.attributes,
}
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Issuer {
Cli,
JwtSvid,
Mtls,
OAuth,
Custom(String),
}
impl Issuer {
pub fn custom(label: impl AsRef<str>) -> Result<Self, IdentityError> {
let s = label.as_ref();
if s.is_empty() || s.len() > 32 {
return Err(IdentityError::InvalidComponent(format!(
"Issuer::custom label length must be 1..=32, got {}",
s.len()
)));
}
if !s
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
{
return Err(IdentityError::InvalidComponent(format!(
"Issuer::custom label must match [a-z0-9_], got {s:?}"
)));
}
Ok(Self::Custom(s.to_string()))
}
pub fn as_str(&self) -> &str {
match self {
Self::Cli => "cli",
Self::JwtSvid => "jwt_svid",
Self::Mtls => "mtls",
Self::OAuth => "oauth",
Self::Custom(s) => s.as_str(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum IdentityError {
#[error("invalid SPIFFE identifier: {0}")]
InvalidSpiffeId(String),
#[error("invalid trust domain: {0}")]
InvalidTrustDomain(String),
#[error("invalid workload identifier component: {0}")]
InvalidComponent(String),
#[error("no authenticated identity available")]
NotAuthenticated,
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_tenant() -> TenantId {
TenantId::from_bytes([1u8; 16])
}
fn sample_user() -> UserId {
UserId::from_bytes([2u8; 16])
}
#[test]
fn human_principal_tenant_id_accessor() {
let tenant = sample_tenant();
let human = HumanPrincipal {
user_id: sample_user(),
tenant_id: tenant,
session_id: None,
attributes: BTreeMap::new(),
};
let p = Principal::Human(human);
assert_eq!(p.tenant_id(), &tenant);
}
#[test]
fn workload_principal_tenant_id_accessor() {
let tenant = sample_tenant();
let trust = TrustDomain::new("gnomes.local").unwrap();
let wid = WorkloadId::build(&trust, "compute-worker", "ekekrantz").unwrap();
let workload = WorkloadPrincipal {
workload_id: wid,
trust_domain: trust,
issuer: Issuer::Cli,
tenant_id: tenant,
tenant_slug: "ekekrantz".to_string(),
service_name: "compute-worker".to_string(),
attributes: BTreeMap::new(),
};
let p = Principal::Workload(workload);
assert_eq!(p.tenant_id(), &tenant);
}
#[test]
fn attributes_accessor_returns_empty_by_default() {
let trust = TrustDomain::new("gnomes.local").unwrap();
let wid = WorkloadId::build(&trust, "feed-worker", "ekekrantz").unwrap();
let workload = WorkloadPrincipal {
workload_id: wid,
trust_domain: trust,
issuer: Issuer::Cli,
tenant_id: sample_tenant(),
tenant_slug: "ekekrantz".to_string(),
service_name: "feed-worker".to_string(),
attributes: BTreeMap::new(),
};
let p = Principal::Workload(workload);
assert!(p.attributes().is_empty());
}
#[test]
fn attributes_accessor_returns_populated_human_map() {
let mut attrs = BTreeMap::new();
attrs.insert("amr".to_string(), serde_json::json!(["pwd", "mfa"]));
let human = HumanPrincipal {
user_id: sample_user(),
tenant_id: sample_tenant(),
session_id: None,
attributes: attrs,
};
let p = Principal::Human(human);
let seen = p.attributes();
assert_eq!(seen.len(), 1);
assert_eq!(seen.get("amr"), Some(&serde_json::json!(["pwd", "mfa"])));
}
#[test]
fn attributes_accessor_returns_populated_workload_map() {
let trust = TrustDomain::new("gnomes.local").unwrap();
let wid = WorkloadId::build(&trust, "feed-worker", "ekekrantz").unwrap();
let mut attrs = BTreeMap::new();
attrs.insert("region".to_string(), serde_json::json!("eu-west-1"));
let workload = WorkloadPrincipal {
workload_id: wid,
trust_domain: trust,
issuer: Issuer::Cli,
tenant_id: sample_tenant(),
tenant_slug: "ekekrantz".to_string(),
service_name: "feed-worker".to_string(),
attributes: attrs,
};
let p = Principal::Workload(workload);
assert_eq!(
p.attributes().get("region"),
Some(&serde_json::json!("eu-west-1"))
);
}
#[test]
fn issuer_as_str_is_stable_lowercase() {
assert_eq!(Issuer::Cli.as_str(), "cli");
assert_eq!(Issuer::JwtSvid.as_str(), "jwt_svid");
assert_eq!(Issuer::Mtls.as_str(), "mtls");
assert_eq!(Issuer::OAuth.as_str(), "oauth");
assert_eq!(
Issuer::custom("github_actions").unwrap().as_str(),
"github_actions"
);
}
#[test]
fn issuer_custom_validation_rejects_bad_labels() {
assert!(Issuer::custom("").is_err());
assert!(Issuer::custom("a".repeat(33)).is_err());
assert!(Issuer::custom("GitHubActions").is_err());
assert!(Issuer::custom("github-actions").is_err());
assert!(Issuer::custom("github.actions").is_err());
assert!(Issuer::custom("github actions").is_err());
assert!(Issuer::custom("k8s").is_ok());
assert!(Issuer::custom("github_actions").is_ok());
assert!(Issuer::custom("circleci_2_1").is_ok());
}
#[test]
fn issuer_custom_accepts_exactly_32_chars() {
let label = "a".repeat(32);
assert!(
Issuer::custom(&label).is_ok(),
"32-char label must be accepted (kills `> -> >=` boundary mutation)"
);
let too_long = "a".repeat(33);
assert!(
Issuer::custom(&too_long).is_err(),
"33-char label must be rejected"
);
}
#[cfg(feature = "serde")]
#[test]
fn principal_serde_round_trip_human() {
let human = HumanPrincipal {
user_id: sample_user(),
tenant_id: sample_tenant(),
session_id: None,
attributes: BTreeMap::new(),
};
let p = Principal::Human(human);
let json = serde_json::to_string(&p).unwrap();
let back: Principal = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
}
#[cfg(feature = "serde")]
#[test]
fn principal_serde_round_trip_workload() {
let trust = TrustDomain::new("gnomes.local").unwrap();
let wid = WorkloadId::build(&trust, "market-worker", "world_cup").unwrap();
let workload = WorkloadPrincipal {
workload_id: wid,
trust_domain: trust,
issuer: Issuer::Cli,
tenant_id: sample_tenant(),
tenant_slug: "world_cup".to_string(),
service_name: "market-worker".to_string(),
attributes: BTreeMap::new(),
};
let p = Principal::Workload(workload);
let json = serde_json::to_string(&p).unwrap();
let back: Principal = serde_json::from_str(&json).unwrap();
assert_eq!(p, back);
}
}