use std::collections::HashMap;
pub type SecurityScheme = String;
pub type Entitlements = HashMap<SecurityScheme, Vec<String>>;
pub type RequirementSet = HashMap<SecurityScheme, Vec<String>>;
pub type Requirements = Vec<RequirementSet>;
pub type Binding = HashMap<String, String>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BindError {
UnboundPlaceholder(String),
WildcardRequirement(String),
InvalidBoundValue(String),
}
impl std::fmt::Display for BindError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnboundPlaceholder(s) => write!(f, "unbound placeholder in requirement {s:?}"),
Self::WildcardRequirement(s) => {
write!(f, "wildcard resourceName is not allowed in requirement {s:?}")
}
Self::InvalidBoundValue(s) => {
write!(f, "bound value must not be empty, a wildcard, or contain ':', in requirement {s:?}")
}
}
}
}
impl std::error::Error for BindError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pattern {
Structured {
resource: String,
name: String,
verb: String,
},
Opaque(String),
}
impl Pattern {
pub fn parse(s: &str) -> Self {
let parts: Vec<&str> = s.split(':').collect();
match parts.len() {
3 => Self::Structured {
resource: parts[0].to_string(),
name: parts[1].to_string(),
verb: parts[2].to_string(),
},
2 => Self::Structured {
resource: parts[0].to_string(),
name: "*".to_string(),
verb: parts[1].to_string(),
},
_ => Self::Opaque(s.to_string()),
}
}
pub fn placeholder(&self) -> Option<&str> {
match self {
Self::Structured { name, .. }
if name.len() > 2 && name.starts_with('{') && name.ends_with('}') =>
{
Some(&name[1..name.len() - 1])
}
_ => None,
}
}
pub fn is_wildcard_name(&self) -> bool {
matches!(self, Self::Structured { name, .. } if name.is_empty() || name == "*")
}
pub fn satisfies(&self, required: &Pattern) -> bool {
match (self, required) {
(Self::Opaque(e), Self::Opaque(r)) => e == r,
(
Self::Structured {
resource: er,
name: en,
verb: ev,
},
Self::Structured {
resource: rr,
name: rn,
verb: rv,
},
) => {
if er != rr {
return false;
}
if ev != rv && ev != "all" {
return false;
}
if en != rn && en != "*" && !en.is_empty() && rn != "*" && !rn.is_empty() {
return false;
}
true
}
_ => false,
}
}
pub fn dominates(&self, requested: &Pattern) -> bool {
match (self, requested) {
(Self::Opaque(h), Self::Opaque(r)) => h == r,
(
Self::Structured {
resource: hr,
name: hn,
verb: hv,
},
Self::Structured {
resource: rr,
name: rn,
verb: rv,
},
) => {
if hr != rr {
return false;
}
if hv != "all" && hv != rv {
return false;
}
if hn.is_empty() || hn == "*" {
return true;
}
hn == rn
}
_ => false,
}
}
pub fn verify_attenuation(held: &[String], requested: &[String]) -> Option<String> {
let held_patterns: Vec<Pattern> = held.iter().map(|s| Pattern::parse(s)).collect();
for req in requested {
let req_pattern = Pattern::parse(req);
let dominated = held_patterns.iter().any(|h| h.dominates(&req_pattern));
if !dominated {
return Some(req.clone());
}
}
None
}
pub fn compact(entitlements: &[String]) -> Vec<String> {
let patterns: Vec<Pattern> = entitlements.iter().map(|s| Pattern::parse(s)).collect();
let mut survivors: Vec<String> = Vec::new();
let mut survivor_patterns: Vec<Pattern> = Vec::new();
for (i, ep) in patterns.iter().enumerate() {
let strictly_dominated = patterns
.iter()
.enumerate()
.any(|(j, op)| i != j && op.dominates(ep) && !ep.dominates(op));
if strictly_dominated {
continue;
}
let dup = survivor_patterns
.iter()
.any(|sp| sp.dominates(ep) && ep.dominates(sp));
if !dup {
survivors.push(entitlements[i].clone());
survivor_patterns.push(ep.clone());
}
}
survivors
}
}
pub struct EntitlementsChecker {
anonymous_entitlements: Vec<Pattern>,
base_entitlements: Vec<Pattern>,
default_scheme: String,
strict_requirements: bool,
}
impl EntitlementsChecker {
pub fn new(anonymous_entitlements: Vec<String>, default_scheme: String) -> Self {
let parsed_anon = anonymous_entitlements.iter().map(|s| Pattern::parse(s)).collect();
Self {
anonymous_entitlements: parsed_anon,
base_entitlements: Vec::new(),
default_scheme,
strict_requirements: false,
}
}
pub fn with_base_entitlements(mut self, patterns: Vec<String>) -> Self {
self.base_entitlements = patterns.iter().map(|s| Pattern::parse(s)).collect();
self
}
pub fn with_strict_requirements(mut self, strict: bool) -> Self {
self.strict_requirements = strict;
self
}
pub fn bind_requirements(
&self,
reqs: &Requirements,
b: &Binding,
) -> Result<Requirements, BindError> {
if self.strict_requirements {
for set in reqs {
for list in set.values() {
for s in list {
let p = Pattern::parse(s);
if p.placeholder().is_none() && p.is_wildcard_name() {
return Err(BindError::WildcardRequirement(s.clone()));
}
}
}
}
}
let mut out = Requirements::with_capacity(reqs.len());
for set in reqs {
let mut new_set = RequirementSet::new();
for (scheme, list) in set {
let mut new_list = Vec::with_capacity(list.len());
for s in list {
let p = Pattern::parse(s);
match p.placeholder() {
None => new_list.push(s.clone()),
Some(key) => {
let v = b
.get(key)
.ok_or_else(|| BindError::UnboundPlaceholder(s.clone()))?;
if v.is_empty() || v == "*" || v.contains(':') {
return Err(BindError::InvalidBoundValue(s.clone()));
}
match &p {
Pattern::Structured { resource, verb, .. } => {
new_list.push(format!("{resource}:{v}:{verb}"))
}
Pattern::Opaque(_) => unreachable!("placeholder implies Structured"),
}
}
}
}
new_set.insert(scheme.clone(), new_list);
}
out.push(new_set);
}
Ok(out)
}
pub fn wildcard_requirements(&self, reqs: &Requirements) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for set in reqs {
for list in set.values() {
for s in list {
let p = Pattern::parse(s);
if p.placeholder().is_none() && p.is_wildcard_name() && !out.contains(s) {
out.push(s.clone());
}
}
}
}
out
}
pub fn verify(&self, user_entitlements: &Entitlements, requirements: &Requirements) -> bool {
if requirements.is_empty() {
return true;
}
let parsed: HashMap<String, Vec<Pattern>> = user_entitlements
.iter()
.map(|(scheme, list)| (scheme.clone(), list.iter().map(|s| Pattern::parse(s)).collect()))
.collect();
let is_anonymous = parsed.is_empty() || parsed.values().all(|v| v.is_empty());
for req_set in requirements {
if self.verify_set(&parsed, req_set, is_anonymous) {
return true;
}
}
false
}
fn verify_set(
&self,
user_patterns: &HashMap<String, Vec<Pattern>>,
req_set: &RequirementSet,
is_anonymous: bool,
) -> bool {
for (scheme, required_patterns) in req_set {
let user_list_present = user_patterns.contains_key(scheme);
let has_fallback = scheme == &self.default_scheme
&& (!self.base_entitlements.is_empty()
|| (is_anonymous && !self.anonymous_entitlements.is_empty()));
if !user_list_present && !has_fallback {
return false;
}
let empty: Vec<Pattern> = Vec::new();
let user_list = user_patterns.get(scheme).unwrap_or(&empty);
for req_str in required_patterns {
let req_p = Pattern::parse(req_str);
if self.strict_requirements
&& (req_p.placeholder().is_some() || req_p.is_wildcard_name())
{
return false;
}
let satisfied_by_user = user_list.iter().any(|p| p.satisfies(&req_p));
let satisfied_by_base = scheme == &self.default_scheme
&& self.base_entitlements.iter().any(|p| p.satisfies(&req_p));
let satisfied_by_anon = scheme == &self.default_scheme
&& is_anonymous
&& self.anonymous_entitlements.iter().any(|p| p.satisfies(&req_p));
if !satisfied_by_user && !satisfied_by_base && !satisfied_by_anon {
return false;
}
}
}
true
}
pub fn verify_resource(
&self,
user_entitlements: &Entitlements,
resource: &str,
name: &str,
verb: &str,
additional_requirements: &Requirements,
) -> bool {
let identity_req = format!("{}:{}:{}", resource, name, verb);
if additional_requirements.is_empty() {
let mut set = RequirementSet::new();
set.insert(self.default_scheme.clone(), vec![identity_req]);
return self.verify(user_entitlements, &vec![set]);
}
let mut combined_requirements = Vec::new();
for set in additional_requirements {
let mut new_set = set.clone();
new_set
.entry(self.default_scheme.clone())
.or_default()
.push(identity_req.clone());
combined_requirements.push(new_set);
}
self.verify(user_entitlements, &combined_requirements)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pattern_parse() {
assert_eq!(
Pattern::parse("pages:/foo:read"),
Pattern::Structured {
resource: "pages".to_string(),
name: "/foo".to_string(),
verb: "read".to_string()
}
);
assert_eq!(
Pattern::parse("pages:read"),
Pattern::Structured {
resource: "pages".to_string(),
name: "*".to_string(),
verb: "read".to_string()
}
);
assert_eq!(Pattern::parse("admin"), Pattern::Opaque("admin".to_string()));
}
#[test]
fn test_verify() {
let checker = EntitlementsChecker::new(vec!["anonymous:read".to_string()], "bearer".to_string());
let mut entitlements = Entitlements::new();
entitlements.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
let requirements = vec![req_set];
assert!(checker.verify(&entitlements, &requirements));
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["pages:*:read".to_string()]);
let requirements = vec![req_set];
assert!(checker.verify(&entitlements, &requirements));
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["anonymous:*:read".to_string()]);
let requirements = vec![req_set];
assert!(!checker.verify(&entitlements, &requirements));
let empty_entitlements = Entitlements::new();
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["anonymous:*:read".to_string()]);
let requirements = vec![req_set];
assert!(checker.verify(&empty_entitlements, &requirements));
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["pages:foo:write".to_string()]);
let requirements = vec![req_set];
assert!(!checker.verify(&entitlements, &requirements));
let mut req_set1 = RequirementSet::new();
req_set1.insert("bearer".to_string(), vec!["pages:foo:write".to_string()]);
let mut req_set2 = RequirementSet::new();
req_set2.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
let requirements = vec![req_set1, req_set2];
assert!(checker.verify(&entitlements, &requirements));
}
#[test]
fn test_verify_resource() {
let checker = EntitlementsChecker::new(vec![], "bearer".to_string());
let mut entitlements = Entitlements::new();
entitlements.insert("bearer".to_string(), vec!["pages:foo:read".to_string(), "admin".to_string()]);
assert!(checker.verify_resource(&entitlements, "pages", "foo", "read", &vec![]));
assert!(!checker.verify_resource(&entitlements, "pages", "bar", "read", &vec![]));
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["admin".to_string()]);
let additional = vec![req_set];
assert!(checker.verify_resource(&entitlements, "pages", "foo", "read", &additional));
let mut req_set = RequirementSet::new();
req_set.insert("bearer".to_string(), vec!["superadmin".to_string()]);
let additional = vec![req_set];
assert!(!checker.verify_resource(&entitlements, "pages", "foo", "read", &additional));
}
#[test]
fn test_anonymous_vs_base() {
let checker = EntitlementsChecker::new(
vec!["anon:read".to_string()],
"bearer".to_string(),
)
.with_base_entitlements(vec!["base:read".to_string()]);
let mut authed = Entitlements::new();
authed.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
let anonymous = Entitlements::new();
let need_anon = vec![{
let mut s = RequirementSet::new();
s.insert("bearer".to_string(), vec!["anon:read".to_string()]);
s
}];
let need_base = vec![{
let mut s = RequirementSet::new();
s.insert("bearer".to_string(), vec!["base:read".to_string()]);
s
}];
assert!(checker.verify(&authed, &need_base));
assert!(!checker.verify(&authed, &need_anon));
assert!(checker.verify(&anonymous, &need_base));
assert!(checker.verify(&anonymous, &need_anon));
let mut empty_list = Entitlements::new();
empty_list.insert("bearer".to_string(), vec![]);
assert!(checker.verify(&empty_list, &need_anon));
let mut other_scheme = Entitlements::new();
other_scheme.insert("oauth2".to_string(), vec!["scope1".to_string()]);
assert!(!checker.verify(&other_scheme, &need_anon));
}
#[test]
fn test_anonymous_vs_base_via_verify_resource() {
let base_checker = EntitlementsChecker::new(vec![], "bearer".to_string())
.with_base_entitlements(vec!["pages:/foo:read".to_string()]);
let mut authed = Entitlements::new();
authed.insert("bearer".to_string(), vec!["other:read".to_string()]);
assert!(base_checker.verify_resource(&authed, "pages", "/foo", "read", &vec![]));
let anonymous = Entitlements::new();
assert!(base_checker.verify_resource(&anonymous, "pages", "/foo", "read", &vec![]));
let anon_checker = EntitlementsChecker::new(
vec!["pages:/foo:read".to_string()],
"bearer".to_string(),
);
assert!(!anon_checker.verify_resource(&authed, "pages", "/foo", "read", &vec![]));
assert!(anon_checker.verify_resource(&anonymous, "pages", "/foo", "read", &vec![]));
}
#[test]
fn test_dominates() {
let cases: Vec<(&str, &str, bool)> = vec![
("vector_stores::write", "vector_stores:X:write", true), ("vector_stores:*:write", "vector_stores:X:write", true), ("vector_stores:X:write", "vector_stores:*:write", false), ("vector_stores:X:write", "vector_stores::write", false), ("vector_stores:X:write", "vector_stores:X:write", true), ("vector_stores:X:write", "vector_stores:Y:write", false), ("functions:/api/v1/files:all", "functions:/api/v1/files:write", true), ("functions:/api/v1/files:write", "functions:/api/v1/files:all", false), ("functions:/x:write", "pages:/x:write", false), ("functions:/api/v1/files:read", "functions:/api/v1/files:write", false), ("admin", "admin", true), ("admin", "billing", false), ("functions:read", "functions:/api/v1/files:read", true), ];
for (held, requested, want) in cases {
let hp = Pattern::parse(held);
let rp = Pattern::parse(requested);
assert_eq!(
hp.dominates(&rp),
want,
"Dominates({held:?},{requested:?}) want {want}"
);
}
}
#[test]
fn test_verify_attenuation() {
let held = vec!["vector_stores:X:read".to_string()];
let requested = vec![
"vector_stores:X:read".to_string(),
"vector_stores:*:read".to_string(),
];
assert_eq!(
Pattern::verify_attenuation(&held, &requested),
Some("vector_stores:*:read".to_string())
);
let held2 = vec![
"functions:/api/v1/files:write".to_string(),
"vector_stores::read".to_string(),
];
let requested2 = vec![
"functions:/api/v1/files:write".to_string(),
"vector_stores:X:read".to_string(),
];
assert_eq!(Pattern::verify_attenuation(&held2, &requested2), None);
}
#[test]
fn test_with_base_entitlements_replaces() {
let checker = EntitlementsChecker::new(vec![], "bearer".to_string())
.with_base_entitlements(vec!["first:read".to_string()])
.with_base_entitlements(vec!["second:read".to_string()]);
let mut authed = Entitlements::new();
authed.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
let need_first = vec![{
let mut s = RequirementSet::new();
s.insert("bearer".to_string(), vec!["first:read".to_string()]);
s
}];
let need_second = vec![{
let mut s = RequirementSet::new();
s.insert("bearer".to_string(), vec!["second:read".to_string()]);
s
}];
assert!(!checker.verify(&authed, &need_first));
assert!(checker.verify(&authed, &need_second));
}
fn compact_real_world_input() -> Vec<String> {
[
"functions:/v1/users:read",
"functions:/v1/users:create",
"functions:/v1/users:update",
"functions:/v1/users:delete",
"users:me:read",
"users:me:create",
"users:me:update",
"users:me:delete",
"apitokens::mint",
"apitokens::revoke",
"vector_stores:system:read",
"functions:/api/v1/vector_stores:read",
"functions:/api/v1/vector_stores:create",
"functions:/api/v1/vector_stores:update",
"functions:/api/v1/vector_stores:delete",
"functions:/api/v1/files:read",
"functions:/api/v1/files:create",
"functions:/api/v1/files:update",
"functions:/api/v1/files:delete",
"functions:/api/v1/search:read",
"functions:/api/v1/search:create",
"functions:/api/v1/search:update",
"functions:/api/v1/search:delete",
"functions:/api/v1/uploads:read",
"functions:/api/v1/uploads:create",
"functions:/api/v1/uploads:update",
"functions:/api/v1/uploads:delete",
"functions:/api/v1/ingest:read",
"functions:/api/v1/ingest:create",
"functions:/api/v1/ingest:update",
"functions:/api/v1/ingest:delete",
"functions:/api/v1/mcp:read",
"functions:/api/v1/mcp:create",
"functions:/api/v1/mcp:update",
"functions:/api/v1/mcp:delete",
"functions:/api/v1/events:read",
"functions:/api/v1/events:create",
"functions:/api/v1/events:update",
"functions:/api/v1/events:delete",
"functions:/tenant/v1:read",
"functions:/tenant/v1:create",
"functions:/tenant/v1:update",
"functions:/tenant/v1:delete",
"functions:/feedback/v1:read",
"functions:/feedback/v1:create",
"pages::read",
"functions::read",
"vector_stores:system:read",
"functions:/v1/chat:read",
]
.iter()
.map(|s| s.to_string())
.collect()
}
fn compact_real_world_expected() -> Vec<String> {
[
"functions:/v1/users:create",
"functions:/v1/users:update",
"functions:/v1/users:delete",
"users:me:read",
"users:me:create",
"users:me:update",
"users:me:delete",
"apitokens::mint",
"apitokens::revoke",
"vector_stores:system:read",
"functions:/api/v1/vector_stores:create",
"functions:/api/v1/vector_stores:update",
"functions:/api/v1/vector_stores:delete",
"functions:/api/v1/files:create",
"functions:/api/v1/files:update",
"functions:/api/v1/files:delete",
"functions:/api/v1/search:create",
"functions:/api/v1/search:update",
"functions:/api/v1/search:delete",
"functions:/api/v1/uploads:create",
"functions:/api/v1/uploads:update",
"functions:/api/v1/uploads:delete",
"functions:/api/v1/ingest:create",
"functions:/api/v1/ingest:update",
"functions:/api/v1/ingest:delete",
"functions:/api/v1/mcp:create",
"functions:/api/v1/mcp:update",
"functions:/api/v1/mcp:delete",
"functions:/api/v1/events:create",
"functions:/api/v1/events:update",
"functions:/api/v1/events:delete",
"functions:/tenant/v1:create",
"functions:/tenant/v1:update",
"functions:/tenant/v1:delete",
"functions:/feedback/v1:create",
"pages::read",
"functions::read",
]
.iter()
.map(|s| s.to_string())
.collect()
}
fn strs(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn test_compact_cases() {
let cases: Vec<(Vec<String>, Vec<String>)> = vec![
(strs(&[]), strs(&[])),
(strs(&["x:/a:read"]), strs(&["x:/a:read"])),
(strs(&["x:*:read", "x:/a:read", "x:/b:read"]), strs(&["x:*:read"])),
(strs(&["x::read", "x:/a:read"]), strs(&["x::read"])),
(strs(&["x:/a:all", "x:/a:read"]), strs(&["x:/a:all"])),
(strs(&["pages:read", "pages::read", "pages:*:read"]), strs(&["pages:read"])),
(strs(&["x:/a:read", "x:/a:read"]), strs(&["x:/a:read"])),
(strs(&["admin", "admin", "email"]), strs(&["admin", "email"])),
(strs(&["functions", "functions::read"]), strs(&["functions", "functions::read"])),
(strs(&["functions::read", "vector_stores:system:read"]), strs(&["functions::read", "vector_stores:system:read"])),
(strs(&["functions::read", "functions:/a:create"]), strs(&["functions::read", "functions:/a:create"])),
(strs(&["x:/a:read", "x:/b:create"]), strs(&["x:/a:read", "x:/b:create"])),
];
for (input, want) in cases {
assert_eq!(Pattern::compact(&input), want, "input: {:?}", input);
}
}
#[test]
fn test_compact_real_world_array() {
let input = compact_real_world_input();
let got = Pattern::compact(&input);
assert_eq!(got.len(), 37);
assert_eq!(got, compact_real_world_expected());
assert_eq!(input.len(), 49); }
#[test]
fn test_compact_idempotent() {
let once = Pattern::compact(&compact_real_world_input());
let twice = Pattern::compact(&once);
assert_eq!(once, twice);
}
#[test]
fn test_compact_preserves_authority() {
let checker = EntitlementsChecker::new(vec![], "bearer".to_string());
let input = compact_real_world_input();
let compacted = Pattern::compact(&input);
let probes: Vec<(&str, bool)> = vec![
("functions:/api/v1/files:read", true),
("functions:/api/v1/files:delete", true),
("billing::read", false),
];
for (req, want) in probes {
let reqs: Requirements =
vec![HashMap::from([("bearer".to_string(), vec![req.to_string()])])];
let ents_orig: Entitlements =
HashMap::from([("bearer".to_string(), input.clone())]);
let ents_comp: Entitlements =
HashMap::from([("bearer".to_string(), compacted.clone())]);
let orig = checker.verify(&ents_orig, &reqs);
let comp = checker.verify(&ents_comp, &reqs);
assert_eq!(orig, want, "original result for {req}");
assert_eq!(orig, comp, "authority equivalence for {req}");
}
}
fn reqs(scheme: &str, list: &[&str]) -> Requirements {
let mut set = RequirementSet::new();
set.insert(scheme.to_string(), list.iter().map(|s| s.to_string()).collect());
vec![set]
}
fn ents(scheme: &str, list: &[&str]) -> Entitlements {
let mut m = Entitlements::new();
m.insert(scheme.to_string(), list.iter().map(|s| s.to_string()).collect());
m
}
#[test]
fn placeholder_recognition() {
assert_eq!(Pattern::parse("vs:{vector_store_id}:read").placeholder(), Some("vector_store_id"));
assert_eq!(Pattern::parse("vs:{a}:read").placeholder(), Some("a"));
assert_eq!(Pattern::parse("vs:{}:read").placeholder(), None); assert_eq!(Pattern::parse("vs:vs_alice:read").placeholder(), None);
assert_eq!(Pattern::parse("vs:*:read").placeholder(), None);
assert_eq!(Pattern::parse("opaque").placeholder(), None);
}
#[test]
fn bind_requirements_substitutes_and_scopes() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let r = reqs("bearer", &["vector_stores:{vector_store_id}:write"]);
let mut b = Binding::new();
b.insert("vector_store_id".to_string(), "vs_alice".to_string());
let bound = ec.bind_requirements(&r, &b).unwrap();
let held = ents("bearer", &["vector_stores:vs_alice:all"]);
assert!(ec.verify(&held, &bound));
let mut b2 = Binding::new();
b2.insert("vector_store_id".to_string(), "vs_bob".to_string());
let bound2 = ec.bind_requirements(&r, &b2).unwrap();
assert!(!ec.verify(&held, &bound2), "vs_alice must not satisfy vs_bob");
assert!(ec.verify(&ents("bearer", &["vector_stores::all"]), &bound2));
}
#[test]
fn bind_requirements_unbound_errors() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let r = reqs("bearer", &["vector_stores:{vector_store_id}:write"]);
assert!(matches!(
ec.bind_requirements(&r, &Binding::new()),
Err(BindError::UnboundPlaceholder(_))
));
}
#[test]
fn bind_requirements_rejects_wildcard_bound_value() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let r = reqs("bearer", &["vector_stores:{vector_store_id}:write"]);
for v in ["", "*", "a:b"] {
let mut b = Binding::new();
b.insert("vector_store_id".to_string(), v.to_string());
assert!(
matches!(ec.bind_requirements(&r, &b), Err(BindError::InvalidBoundValue(_))),
"binding to {v:?} should be rejected"
);
}
let mut ok = Binding::new();
ok.insert("vector_store_id".to_string(), "vs_alice".to_string());
assert!(ec.bind_requirements(&r, &ok).is_ok());
}
#[test]
fn bind_requirements_no_placeholder_is_unchanged() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let r = reqs("bearer", &["functions:/api/v1/files:read"]);
assert_eq!(ec.bind_requirements(&r, &Binding::new()).unwrap(), r);
}
#[test]
fn bind_requirements_multiple_and_superset() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let r = reqs("bearer", &["vector_stores:{vector_store_id}:write", "files:{file_id}:read"]);
let mut b = Binding::new();
b.insert("vector_store_id".to_string(), "vs_alice".to_string());
b.insert("file_id".to_string(), "file_1".to_string());
b.insert("unused".to_string(), "ignored".to_string());
let bound = ec.bind_requirements(&r, &b).unwrap();
let held = ents("bearer", &["vector_stores:vs_alice:all", "files:file_1:read"]);
assert!(ec.verify(&held, &bound));
}
#[test]
fn held_side_placeholder_is_literal() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let held = ents("bearer", &["vector_stores:{vector_store_id}:all"]);
assert!(
!ec.verify(&held, &reqs("bearer", &["vector_stores:vs_alice:write"])),
"a held-side placeholder must be literal text, not a wildcard"
);
}
#[test]
fn strict_rejects_wildcard_requirements() {
let held = ents("bearer", &["vector_stores:vs_alice:all"]);
let lax = EntitlementsChecker::new(vec![], "bearer".to_string());
assert!(lax.verify(&held, &reqs("bearer", &["vector_stores:*:write"])));
let strict = EntitlementsChecker::new(vec![], "bearer".to_string())
.with_strict_requirements(true);
assert!(!strict.verify(&held, &reqs("bearer", &["vector_stores:*:write"])));
assert!(strict.verify(&held, &reqs("bearer", &["vector_stores:vs_alice:write"])));
for s in ["vector_stores:*:write", "vector_stores::write", "vector_stores:write"] {
assert!(
matches!(
strict.bind_requirements(&reqs("bearer", &[s]), &Binding::new()),
Err(BindError::WildcardRequirement(_))
),
"{s} should be rejected"
);
}
assert!(
!strict.verify(
&ents("bearer", &["vector_stores::all"]),
&reqs("bearer", &["vector_stores:*:write"])
),
"a wildcard requirement is illegal regardless of the grant"
);
for s in ["vector_stores_create", "functions:/api/v1/files:read"] {
assert!(
strict.bind_requirements(&reqs("bearer", &[s]), &Binding::new()).is_ok(),
"{s} should bind cleanly under strict"
);
}
}
#[test]
fn strict_unbound_placeholder_fails_closed() {
let strict = EntitlementsChecker::new(vec![], "bearer".to_string())
.with_strict_requirements(true);
let admin = ents("bearer", &["vector_stores::all"]);
assert!(!strict.verify(&admin, &reqs("bearer", &["vector_stores:{vector_store_id}:write"])));
}
#[test]
fn strict_wildcard_error_is_order_independent() {
let strict = EntitlementsChecker::new(vec![], "bearer".to_string())
.with_strict_requirements(true);
for list in [
["vector_stores:{vector_store_id}:write", "vector_stores:*:read"],
["vector_stores:*:read", "vector_stores:{vector_store_id}:write"],
] {
assert!(
matches!(
strict.bind_requirements(&reqs("bearer", &list), &Binding::new()),
Err(BindError::WildcardRequirement(_))
),
"wildcard must win regardless of order: {list:?}"
);
}
}
#[test]
fn wildcard_requirements_inventory() {
let ec = EntitlementsChecker::new(vec![], "bearer".to_string());
let r = reqs("bearer", &[
"functions:/api/v1/ingest:read",
"vector_stores:*:write",
"apitokens:mint",
"vector_stores:*:write",
"vector_stores:{vector_store_id}:write",
"vector_stores_create",
]);
assert_eq!(
ec.wildcard_requirements(&r),
vec!["vector_stores:*:write".to_string(), "apitokens:mint".to_string()]
);
assert!(ec.wildcard_requirements(&reqs("bearer", &["users:me:read"])).is_empty());
}
}