use std::collections::{HashMap, HashSet};
use super::super::error::{AuthzError, AuthzResult};
use super::super::types::EntityKind;
#[derive(Debug, Clone, Default)]
pub struct RegisteredEntities {
by_kind: HashMap<EntityKind, HashSet<String>>,
}
impl RegisteredEntities {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_kind<I, S>(mut self, kind: EntityKind, ids: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.by_kind
.insert(kind, ids.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn permits(&self, kind: EntityKind, id: &str) -> bool {
self.by_kind
.get(&kind)
.is_none_or(|known| known.contains(id))
}
pub fn require(&self, kind: EntityKind, id: &str) -> AuthzResult<()> {
if self.permits(kind, id) {
Ok(())
} else {
Err(AuthzError::Validation(unregistered_id_message(
kind, id, self,
)))
}
}
#[must_use]
pub fn known_ids(&self, kind: EntityKind) -> Vec<&str> {
let mut ids: Vec<&str> = self
.by_kind
.get(&kind)
.map(|known| known.iter().map(String::as_str).collect())
.unwrap_or_default();
ids.sort_unstable();
ids
}
}
fn unregistered_id_message(kind: EntityKind, id: &str, registered: &RegisteredEntities) -> String {
let known = registered.known_ids(kind);
let catalog = if known.is_empty() {
format!("no {} is registered on this deployment", kind.as_str())
} else {
format!("registered {}s: {}", kind.as_str(), known.join(", "))
};
let hint = match kind {
EntityKind::GatewayRoute => {
" — gateway route ids are generated by synthesize_route_id(model_pattern, provider), \
not authored, so a hand-written one can never match; grant routes with `entity_match` \
instead of a literal `entity_id`"
},
_ => "",
};
format!(
"access-control rule references {kind} '{id}', which no catalog row registers ({catalog}){hint}",
kind = kind.as_str(),
)
}