use crate::error::ServerError;
use crate::namespace::resolver::{CallerIdentity, GrantSource};
#[derive(Clone, Copy, Debug)]
pub struct GrantWord {
word: &'static str,
header: &'static str,
claim: &'static str,
description: &'static str,
granted: fn(&CallerIdentity) -> bool,
}
impl GrantWord {
#[must_use]
pub const fn word(&self) -> &'static str {
self.word
}
#[must_use]
pub const fn header(&self) -> &'static str {
self.header
}
#[must_use]
pub const fn claim(&self) -> &'static str {
self.claim
}
#[must_use]
pub const fn description(&self) -> &'static str {
self.description
}
#[must_use]
pub fn granted_for(&self, caller: &CallerIdentity) -> bool {
(self.granted)(caller)
}
}
pub const DEPLOY: GrantWord = GrantWord {
word: "deploy",
header: "x-aion-deploy",
claim: "deploy",
description: "load packages, re-point routing, and unload versions on the deployment-wide \
operator deploy surface",
granted: CallerIdentity::deploy_granted,
};
pub const ASSISTANT_SESSIONS: GrantWord = GrantWord {
word: "assistant.sessions",
header: "x-aion-assistant-sessions",
claim: "assistant.sessions",
description: "start and drive server-owned assistant harness sessions",
granted: CallerIdentity::assistant_sessions_granted,
};
pub const GRANT_WORDS: &[GrantWord] = &[DEPLOY, ASSISTANT_SESSIONS];
#[must_use]
pub fn lookup(word: &str) -> Option<&'static GrantWord> {
GRANT_WORDS.iter().find(|grant| grant.word == word)
}
pub fn require_grant(caller: &CallerIdentity, grant: &GrantWord) -> Result<(), ServerError> {
if let Some(reason) = caller.denial_reason() {
return Err(ServerError::grant_denied(reason));
}
if grant.granted_for(caller) {
return Ok(());
}
Err(grant_denied(caller, grant))
}
fn grant_denied(caller: &CallerIdentity, grant: &GrantWord) -> ServerError {
let subject = caller.subject();
let word = grant.word();
let hint = match caller.grant_source() {
GrantSource::NamespacesHeader => {
format!("set `{}`: true for subject `{subject}`", grant.header())
}
GrantSource::TokenClaim => {
format!(
"mint a token whose `{}` claim is true for subject `{subject}`",
grant.claim()
)
}
GrantSource::Operator => {
format!("subject `{subject}` is the operator and already holds `{word}`")
}
};
ServerError::grant_denied(format!(
"subject `{subject}` is not authorized for `{word}`; {hint}"
))
}
#[cfg(test)]
mod tests {
use aion_proto::WireErrorCode;
use super::{ASSISTANT_SESSIONS, DEPLOY, GRANT_WORDS, GrantWord, lookup, require_grant};
use crate::namespace::CallerIdentity;
#[test]
fn the_vocabulary_is_not_empty() {
assert!(
!GRANT_WORDS.is_empty(),
"the grant vocabulary is empty, so every test that walks it measures nothing"
);
}
#[test]
fn the_deploy_row_matches_the_path_deploy_actually_takes() {
assert_eq!(DEPLOY.word(), "deploy");
assert_eq!(DEPLOY.header(), "x-aion-deploy");
assert_eq!(DEPLOY.claim(), "deploy");
}
#[test]
fn the_assistant_sessions_row_carries_its_declared_spellings() {
assert_eq!(ASSISTANT_SESSIONS.word(), "assistant.sessions");
assert_eq!(ASSISTANT_SESSIONS.header(), "x-aion-assistant-sessions");
assert_eq!(ASSISTANT_SESSIONS.claim(), "assistant.sessions");
}
#[test]
fn every_row_is_distinct_in_word_header_and_claim() {
type Field = (&'static str, fn(&GrantWord) -> &'static str);
let fields: [Field; 3] = [
("word", GrantWord::word),
("header", GrantWord::header),
("claim", GrantWord::claim),
];
for (field, read) in fields {
let mut values: Vec<&str> = GRANT_WORDS.iter().map(read).collect();
let total = values.len();
values.sort_unstable();
values.dedup();
assert_eq!(values.len(), total, "two grant words share one {field}");
}
}
#[test]
fn every_row_describes_what_it_authorises() {
for grant in GRANT_WORDS {
assert!(
!grant.description().trim().is_empty(),
"`{}` carries no description",
grant.word()
);
}
}
#[test]
fn lookup_finds_every_word_and_invents_none() {
for grant in GRANT_WORDS {
let found = lookup(grant.word());
assert_eq!(
found.map(GrantWord::word),
Some(grant.word()),
"`{}` is in the vocabulary but not findable in it",
grant.word()
);
}
assert!(lookup("assistant").is_none());
assert!(lookup("").is_none());
}
#[test]
fn the_operator_holds_every_word() {
let operator = CallerIdentity::operator("operator");
for grant in GRANT_WORDS {
assert!(
grant.granted_for(&operator),
"the operator does not hold `{}`",
grant.word()
);
}
}
#[test]
fn an_enumerated_caller_holds_no_word_by_default() {
for caller in [
CallerIdentity::new("ci", [String::from("tenant-a")]),
CallerIdentity::from_token_claims("ci", [String::from("tenant-a")]),
CallerIdentity::denied("ci", "invalid bearer token"),
] {
for grant in GRANT_WORDS {
assert!(
!grant.granted_for(&caller),
"`{}` was granted to a caller that asserted nothing",
grant.word()
);
}
}
}
#[test]
fn one_word_does_not_confer_another() {
let deployer = CallerIdentity::new("ci", [String::from("tenant-a")]).with_deploy(true);
assert!(DEPLOY.granted_for(&deployer));
assert!(!ASSISTANT_SESSIONS.granted_for(&deployer));
let assistant =
CallerIdentity::new("ci", [String::from("tenant-a")]).with_assistant_sessions(true);
assert!(ASSISTANT_SESSIONS.granted_for(&assistant));
assert!(!DEPLOY.granted_for(&assistant));
}
#[test]
fn a_granted_caller_is_authorized() -> Result<(), Box<dyn std::error::Error>> {
let header_caller =
CallerIdentity::new("ci", [String::from("tenant-a")]).with_assistant_sessions(true);
let token_caller = CallerIdentity::from_token_claims("ci", [String::from("tenant-a")])
.with_assistant_sessions(true);
require_grant(&header_caller, &ASSISTANT_SESSIONS)?;
require_grant(&token_caller, &ASSISTANT_SESSIONS)?;
require_grant(&CallerIdentity::operator("operator"), &ASSISTANT_SESSIONS)?;
Ok(())
}
#[test]
fn a_refusal_never_borrows_the_deploy_code() -> Result<(), Box<dyn std::error::Error>> {
let caller = CallerIdentity::new("ci", [String::from("tenant-a")]);
let error = require_grant(&caller, &ASSISTANT_SESSIONS)
.err()
.map(|error| error.to_wire_error())
.ok_or("expected an ungranted caller to be refused")?;
assert_eq!(error.code, WireErrorCode::GrantDenied);
assert!(
error.message.contains("assistant.sessions"),
"the refusal must name the word: {}",
error.message
);
assert!(
error.message.contains("subject `ci`"),
"the refusal must name the subject: {}",
error.message
);
Ok(())
}
#[test]
fn the_hint_names_this_callers_grant_source() -> Result<(), Box<dyn std::error::Error>> {
let header_denial = require_grant(
&CallerIdentity::new("ci", [String::from("tenant-a")]),
&ASSISTANT_SESSIONS,
)
.err()
.map(|error| error.to_wire_error())
.ok_or("expected the header-sourced caller to be denied")?;
assert!(
header_denial.message.contains("x-aion-assistant-sessions"),
"header-path denial must hint the dev header: {}",
header_denial.message
);
assert!(
!header_denial.message.contains("claim"),
"header-path denial must not hint the token claim: {}",
header_denial.message
);
let token_denial = require_grant(
&CallerIdentity::from_token_claims("ci", [String::from("tenant-a")]),
&ASSISTANT_SESSIONS,
)
.err()
.map(|error| error.to_wire_error())
.ok_or("expected the token-sourced caller to be denied")?;
assert!(
token_denial
.message
.contains("`assistant.sessions` claim is true"),
"JWT-path denial must hint the token claim: {}",
token_denial.message
);
assert!(
!token_denial.message.contains("x-aion-assistant-sessions"),
"JWT-path denial must not hint the dev header: {}",
token_denial.message
);
Ok(())
}
#[test]
fn every_words_refusal_quotes_that_words_own_knobs() -> Result<(), Box<dyn std::error::Error>> {
for grant in GRANT_WORDS {
let header_message = require_grant(&CallerIdentity::new("ci", []), grant)
.err()
.map(|error| error.to_wire_error().message)
.ok_or("expected the header-sourced caller to be denied")?;
assert!(
header_message.contains(grant.header()),
"`{}` refusal must name its header `{}`: {header_message}",
grant.word(),
grant.header()
);
let claim_message = require_grant(&CallerIdentity::from_token_claims("ci", []), grant)
.err()
.map(|error| error.to_wire_error().message)
.ok_or("expected the token-sourced caller to be denied")?;
assert!(
claim_message.contains(grant.claim()),
"`{}` refusal must name its claim `{}`: {claim_message}",
grant.word(),
grant.claim()
);
}
Ok(())
}
#[test]
fn a_transport_denied_caller_keeps_its_reason() -> Result<(), Box<dyn std::error::Error>> {
let denied =
CallerIdentity::denied("ci", "invalid bearer token").with_assistant_sessions(true);
let error = require_grant(&denied, &ASSISTANT_SESSIONS)
.err()
.map(|error| error.to_wire_error())
.ok_or("expected the transport-denied caller to be refused")?;
assert_eq!(error.code, WireErrorCode::GrantDenied);
assert!(
error.message.contains("invalid bearer token"),
"the denial must carry the transport reason: {}",
error.message
);
Ok(())
}
#[test]
fn namespace_grants_do_not_imply_any_word() {
let caller = CallerIdentity::new("ci", [String::from("tenant-a"), String::from("b")]);
for grant in GRANT_WORDS {
assert_eq!(
require_grant(&caller, grant)
.err()
.map(|error| error.to_wire_error().code),
Some(WireErrorCode::GrantDenied),
"namespaces implied `{}`",
grant.word()
);
}
}
}