use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use super::glob::KeyGlob;
pub type SubjectName = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Op {
Get,
List,
GetPublicKey,
Verify,
Sign,
Encrypt,
Decrypt,
Mint,
SignNatsJwt,
ValidateNatsJwt,
EncryptNatsCurve,
DecryptNatsCurve,
Validate,
Set,
Rotate,
Import,
NewKey,
Reload,
Explain,
Revoke,
Watch,
UseSoftwareCustody,
}
pub(crate) const ALL_OPS: [Op; 17] = [
Op::Get,
Op::List,
Op::GetPublicKey,
Op::Verify,
Op::Sign,
Op::Encrypt,
Op::Decrypt,
Op::Mint,
Op::SignNatsJwt,
Op::ValidateNatsJwt,
Op::EncryptNatsCurve,
Op::DecryptNatsCurve,
Op::Validate,
Op::Set,
Op::Rotate,
Op::Import,
Op::NewKey,
];
impl Op {
#[must_use]
pub const fn is_write(self) -> bool {
matches!(self, Self::Set | Self::Rotate | Self::Import | Self::NewKey)
}
#[must_use]
pub const fn token(self) -> &'static str {
match self {
Self::Get => "get",
Self::List => "list",
Self::GetPublicKey => "get_public_key",
Self::Verify => "verify",
Self::Sign => "sign",
Self::Encrypt => "encrypt",
Self::Decrypt => "decrypt",
Self::Mint => "mint",
Self::SignNatsJwt => "sign_nats_jwt",
Self::ValidateNatsJwt => "validate_nats_jwt",
Self::EncryptNatsCurve => "encrypt_nats_curve",
Self::DecryptNatsCurve => "decrypt_nats_curve",
Self::Validate => "validate",
Self::Set => "set",
Self::Rotate => "rotate",
Self::Import => "import",
Self::NewKey => "new_key",
Self::Reload => "reload",
Self::Explain => "explain",
Self::Revoke => "revoke",
Self::Watch => "watch",
Self::UseSoftwareCustody => "use_software_custody",
}
}
pub fn parse(token: &str) -> Result<Self, ActionTermError> {
let op = match token {
"get" => Self::Get,
"list" => Self::List,
"get_public_key" => Self::GetPublicKey,
"verify" => Self::Verify,
"sign" => Self::Sign,
"encrypt" => Self::Encrypt,
"decrypt" => Self::Decrypt,
"mint" => Self::Mint,
"sign_nats_jwt" => Self::SignNatsJwt,
"validate_nats_jwt" => Self::ValidateNatsJwt,
"encrypt_nats_curve" => Self::EncryptNatsCurve,
"decrypt_nats_curve" => Self::DecryptNatsCurve,
"validate" => Self::Validate,
"set" => Self::Set,
"rotate" => Self::Rotate,
"import" => Self::Import,
"new_key" => Self::NewKey,
"reload" => Self::Reload,
"explain" => Self::Explain,
"revoke" => Self::Revoke,
"watch" => Self::Watch,
"use_software_custody" => Self::UseSoftwareCustody,
other => return Err(ActionTermError::UnknownOp(other.to_string())),
};
Ok(op)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum SignatureKeyAlgorithm {
Ed25519,
NatsNkey,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "kind", rename_all = "kebab-case", deny_unknown_fields)]
pub enum PrincipalSpec {
Unix {
#[serde(default, skip_serializing_if = "Option::is_none")]
uid: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
gid: Option<u32>,
},
Unauthenticated,
SignatureKey {
algorithm: SignatureKeyAlgorithm,
public: String,
},
}
impl PrincipalSpec {
#[must_use]
pub fn matches_unix(&self, uid: u32, gids: &[u32]) -> bool {
match self {
Self::Unix { uid: want_uid, gid } => {
let uid_matches = want_uid.is_none_or(|want| want == uid);
let gid_matches = gid.is_none_or(|want| gids.contains(&want));
uid_matches && gid_matches
}
Self::Unauthenticated | Self::SignatureKey { .. } => false,
}
}
#[must_use]
pub const fn matches_unauthenticated(&self) -> bool {
matches!(self, Self::Unauthenticated)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubjectMatch {
AllOf(Vec<PrincipalSpec>),
AnyOf(Vec<PrincipalSpec>),
}
impl SubjectMatch {
#[must_use]
pub fn matches_unix(&self, uid: u32, gids: &[u32]) -> bool {
match self {
Self::AllOf(specs) => specs.iter().all(|spec| spec.matches_unix(uid, gids)),
Self::AnyOf(specs) => specs.iter().any(|spec| spec.matches_unix(uid, gids)),
}
}
#[must_use]
pub fn matches_unauthenticated(&self) -> bool {
match self {
Self::AllOf(specs) => specs.iter().all(PrincipalSpec::matches_unauthenticated),
Self::AnyOf(specs) => specs.iter().any(PrincipalSpec::matches_unauthenticated),
}
}
#[must_use]
pub fn specs(&self) -> &[PrincipalSpec] {
match self {
Self::AllOf(specs) | Self::AnyOf(specs) => specs,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubjectDefinition {
pub break_glass: bool,
pub match_: SubjectMatch,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionTerm {
Role(String),
Op(Op),
AnyOp,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ActionTermError {
#[error("bad action term `{0}` (expected `role:<name>`, `op:<op>`, or `*`)")]
BadAction(String),
#[error("unknown op `{0}`")]
UnknownOp(String),
}
impl ActionTerm {
pub fn parse(term: &str) -> Result<Self, ActionTermError> {
if term == "*" {
return Ok(Self::AnyOp);
}
if let Some(role) = term.strip_prefix("role:") {
return Ok(Self::Role(role.to_string()));
}
if let Some(op) = term.strip_prefix("op:") {
return Ok(Self::Op(Op::parse(op)?));
}
Err(ActionTermError::BadAction(term.to_string()))
}
}
#[derive(Debug, Clone)]
pub struct Rule {
pub id: String,
pub subjects: Vec<SubjectName>,
pub action: Vec<ActionTerm>,
pub target: Vec<KeyGlob>,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NameTable {
#[serde(default)]
pub users: BTreeMap<u32, String>,
#[serde(default)]
pub groups: BTreeMap<u32, String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub names: NameTable,
#[serde(default)]
pub memberships: BTreeMap<u32, BTreeSet<u32>>,
}
impl Config {
#[must_use]
pub fn user_name_num(&self, uid: u32) -> String {
Self::name_num(self.names.users.get(&uid), uid)
}
#[must_use]
pub fn group_name_num(&self, gid: u32) -> String {
Self::name_num(self.names.groups.get(&gid), gid)
}
#[must_use]
pub fn groups_of(&self, uid: u32) -> Option<&BTreeSet<u32>> {
self.memberships.get(&uid)
}
fn name_num(name: Option<&String>, num: u32) -> String {
name.map_or_else(|| num.to_string(), |n| format!("{n}({num})"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Grant {
pub op: Op,
pub target: KeyGlob,
pub rule_id: String,
pub action: String,
}
#[derive(Debug, Clone)]
pub struct ResolvedRule {
pub subjects: Vec<SubjectName>,
pub grants: Vec<Grant>,
}
#[derive(Debug, Clone, Default)]
pub struct ResolvedPolicy {
pub subjects: BTreeMap<SubjectName, SubjectDefinition>,
pub unauthenticated_subject: Option<SubjectName>,
pub rules: Vec<ResolvedRule>,
}
impl ResolvedPolicy {
#[must_use]
pub fn grant_count(&self) -> usize {
self.rules.iter().map(|r| r.grants.len()).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn op_is_write_only_for_write_ops() {
for op in [Op::Set, Op::Rotate, Op::Import, Op::NewKey] {
assert!(op.is_write(), "{op:?} should be a write");
}
for op in [
Op::Get,
Op::List,
Op::GetPublicKey,
Op::Verify,
Op::Sign,
Op::Encrypt,
Op::Decrypt,
Op::Mint,
Op::SignNatsJwt,
Op::ValidateNatsJwt,
Op::EncryptNatsCurve,
Op::DecryptNatsCurve,
Op::Validate,
] {
assert!(!op.is_write(), "{op:?} should not be a write");
}
}
#[test]
fn op_parse_round_trips_tokens() {
assert_eq!(Op::parse("get_public_key").unwrap(), Op::GetPublicKey);
assert_eq!(Op::parse("new_key").unwrap(), Op::NewKey);
assert_eq!(Op::parse("mint").unwrap(), Op::Mint);
assert_eq!(Op::parse("sign_nats_jwt").unwrap(), Op::SignNatsJwt);
assert_eq!(Op::parse("validate_nats_jwt").unwrap(), Op::ValidateNatsJwt);
assert_eq!(
Op::parse("encrypt_nats_curve").unwrap(),
Op::EncryptNatsCurve
);
assert_eq!(
Op::parse("decrypt_nats_curve").unwrap(),
Op::DecryptNatsCurve
);
assert_eq!(Op::parse("validate").unwrap(), Op::Validate);
assert_eq!(Op::parse("reload").unwrap(), Op::Reload);
assert_eq!(Op::parse("explain").unwrap(), Op::Explain);
assert_eq!(Op::parse("revoke").unwrap(), Op::Revoke);
assert_eq!(Op::parse("watch").unwrap(), Op::Watch);
assert_eq!(Op::Reload.token(), "reload");
assert_eq!(Op::Explain.token(), "explain");
assert_eq!(Op::Revoke.token(), "revoke");
assert_eq!(Op::Watch.token(), "watch");
assert!(matches!(
Op::parse("nope"),
Err(ActionTermError::UnknownOp(_))
));
}
#[test]
fn admin_ops_are_not_write_and_excluded_from_any_op_expansion() {
for op in [Op::Reload, Op::Explain, Op::Revoke, Op::Watch] {
assert!(!op.is_write());
assert!(
!ALL_OPS.contains(&op),
"{op:?} must stay out of the `*`/effective-sweep op set"
);
}
}
#[test]
fn action_parses_role_op_and_any() {
assert_eq!(
ActionTerm::parse("role:minter").unwrap(),
ActionTerm::Role("minter".into())
);
assert_eq!(
ActionTerm::parse("op:sign").unwrap(),
ActionTerm::Op(Op::Sign)
);
assert_eq!(ActionTerm::parse("*").unwrap(), ActionTerm::AnyOp);
assert!(matches!(
ActionTerm::parse("get"),
Err(ActionTermError::BadAction(_))
));
assert!(matches!(
ActionTerm::parse("op:bogus"),
Err(ActionTermError::UnknownOp(_))
));
}
#[test]
fn unauthenticated_principal_matches_only_unauthenticated_actor() {
let subject = SubjectMatch::AnyOf(vec![PrincipalSpec::Unauthenticated]);
assert!(subject.matches_unauthenticated());
assert!(!subject.matches_unix(42, &[42]));
}
#[test]
fn config_logging_helpers_fall_back_to_number() {
let mut cfg = Config::default();
cfg.names.users.insert(9002, "svc-nats".into());
cfg.names.groups.insert(10, "wheel".into());
assert_eq!(cfg.user_name_num(9002), "svc-nats(9002)");
assert_eq!(cfg.group_name_num(10), "wheel(10)");
assert_eq!(cfg.user_name_num(123), "123"); assert_eq!(cfg.group_name_num(456), "456"); }
#[test]
fn config_groups_of_reads_memberships() {
let mut cfg = Config::default();
cfg.memberships.insert(9002, BTreeSet::from([9002, 10]));
assert_eq!(cfg.groups_of(9002), Some(&BTreeSet::from([9002, 10])));
assert_eq!(cfg.groups_of(123), None);
}
}