use systemprompt_identifiers::UserId;
use super::subject::{ROLE_PRECEDENCE, SubjectAttributes, SubjectDimension, USER_PRECEDENCE};
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>],
pub attributes: &'a SubjectAttributes,
pub dimensions: &'a [SubjectDimension],
}
#[must_use]
pub fn resolve(input: ResolveInput<'_>) -> Decision {
let ResolveInput {
entity,
rules,
user_id,
user_roles,
default_included,
parents,
attributes,
dimensions,
} = input;
let ladder = ladder(dimensions);
let subject = Subject {
user_id,
user_roles,
attributes,
ladder: &ladder,
};
if let Some(decision) = match_ruleset(entity, rules, &subject) {
return decision;
}
let parents = if rules.is_empty() { parents } else { &[] };
for parent in parents {
if let Some(decision) = match_ruleset(parent.entity, parent.rules, &subject) {
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 ladder(dimensions: &[SubjectDimension]) -> Vec<(RuleType, u16)> {
let mut bands = vec![
(RuleType::USER, USER_PRECEDENCE),
(RuleType::ROLE, ROLE_PRECEDENCE),
];
bands.extend(
dimensions
.iter()
.filter(|d| d.rule_type != RuleType::USER && d.rule_type != RuleType::ROLE)
.map(|d| (d.rule_type.clone(), d.precedence)),
);
bands.sort_by_key(|&(_, precedence)| precedence);
bands
}
struct Subject<'a> {
user_id: &'a UserId,
user_roles: &'a [String],
attributes: &'a SubjectAttributes,
ladder: &'a [(RuleType, u16)],
}
impl Subject<'_> {
fn matches(&self, rule: &AccessRule) -> bool {
if rule.rule_type == RuleType::USER {
return rule.rule_value == self.user_id.as_str();
}
let held = if rule.rule_type == RuleType::ROLE {
self.user_roles
} else {
self.attributes.values(&rule.rule_type)
};
held.iter().any(|value| value == &rule.rule_value)
}
}
fn match_ruleset(
target: &EntityRef,
ruleset: &[AccessRule],
subject: &Subject<'_>,
) -> Option<Decision> {
for (rule_type, _) in subject.ladder {
let in_band = |r: &&AccessRule| r.rule_type == *rule_type && subject.matches(r);
if let Some(rule) = ruleset
.iter()
.find(|r| in_band(r) && r.access == Access::Deny)
{
return Some(deny_for(target, subject, rule));
}
if let Some(rule) = ruleset
.iter()
.find(|r| in_band(r) && r.access == Access::Allow)
{
return Some(allow_for(rule));
}
}
None
}
fn deny_for(target: &EntityRef, subject: &Subject<'_>, rule: &AccessRule) -> Decision {
let reason = if rule.rule_type == RuleType::USER {
DenyReason::UserDeny {
entity: target.clone(),
user_id: subject.user_id.clone(),
justification: rule.justification.clone(),
}
} else if rule.rule_type == RuleType::ROLE {
DenyReason::RoleDeny {
entity: target.clone(),
role: rule.rule_value.clone(),
justification: rule.justification.clone(),
}
} else {
DenyReason::AttributeDeny {
entity: target.clone(),
rule_type: rule.rule_type.clone(),
value: rule.rule_value.clone(),
justification: rule.justification.clone(),
}
};
Decision::Deny { reason }
}
fn allow_for(rule: &AccessRule) -> Decision {
let matched_by = if rule.rule_type == RuleType::USER {
MatchedBy::UserAllow
} else if rule.rule_type == RuleType::ROLE {
MatchedBy::RoleAllow {
role: rule.rule_value.clone(),
}
} else {
MatchedBy::AttributeAllow {
rule_type: rule.rule_type.clone(),
value: rule.rule_value.clone(),
}
};
Decision::Allow { matched_by }
}