use systemprompt_identifiers::UserId;
use super::types::{Access, AccessRule, Decision, DenyReason, EntityRef, MatchedBy, RuleType};
#[derive(Debug, Clone, Copy)]
pub struct ResolveParent<'a> {
pub entity: &'a EntityRef,
pub rules: &'a [AccessRule],
pub default_included: Option<bool>,
}
#[derive(Debug, Clone, Copy)]
pub struct ResolveInput<'a> {
pub entity: &'a EntityRef,
pub rules: &'a [AccessRule],
pub user_id: &'a UserId,
pub user_roles: &'a [String],
pub default_included: Option<bool>,
pub parents: &'a [ResolveParent<'a>],
}
#[must_use]
pub fn resolve(input: ResolveInput<'_>) -> Decision {
let ResolveInput {
entity,
rules,
user_id,
user_roles,
default_included,
parents,
} = input;
if let Some(decision) = match_ruleset(entity, rules, user_id, user_roles) {
return decision;
}
for parent in parents {
if let Some(decision) = match_ruleset(parent.entity, parent.rules, user_id, user_roles) {
return decision;
}
}
if default_included == Some(true) {
return Decision::Allow {
matched_by: MatchedBy::DefaultIncluded,
};
}
if parents
.iter()
.any(|parent| parent.default_included == Some(true))
{
return Decision::Allow {
matched_by: MatchedBy::DefaultIncluded,
};
}
if default_included.is_none() {
return Decision::Deny {
reason: DenyReason::UnknownEntity {
entity: entity.clone(),
},
};
}
Decision::Deny {
reason: DenyReason::NotAssigned {
entity: entity.clone(),
user_id: user_id.clone(),
roles: user_roles.to_vec(),
},
}
}
fn match_ruleset(
target: &EntityRef,
ruleset: &[AccessRule],
user_id: &UserId,
user_roles: &[String],
) -> Option<Decision> {
let user_match =
|r: &AccessRule| r.rule_type == RuleType::User && r.rule_value == user_id.as_str();
let role_match = |r: &AccessRule| {
r.rule_type == RuleType::Role && user_roles.iter().any(|role| role == &r.rule_value)
};
if let Some(rule) = ruleset
.iter()
.find(|r| user_match(r) && r.access == Access::Deny)
{
return Some(Decision::Deny {
reason: DenyReason::UserDeny {
entity: target.clone(),
user_id: user_id.clone(),
justification: rule.justification.clone(),
},
});
}
if ruleset
.iter()
.any(|r| user_match(r) && r.access == Access::Allow)
{
return Some(Decision::Allow {
matched_by: MatchedBy::UserAllow,
});
}
if let Some(rule) = ruleset
.iter()
.find(|r| role_match(r) && r.access == Access::Deny)
{
return Some(Decision::Deny {
reason: DenyReason::RoleDeny {
entity: target.clone(),
role: rule.rule_value.clone(),
justification: rule.justification.clone(),
},
});
}
if let Some(rule) = ruleset
.iter()
.find(|r| role_match(r) && r.access == Access::Allow)
{
return Some(Decision::Allow {
matched_by: MatchedBy::RoleAllow {
role: rule.rule_value.clone(),
},
});
}
None
}