// enforcer.mbt — The enforcement core: model + policy + request -> decision.
//
// `Enforcer::new` preprocesses the matcher and effect expressions, parses
// the matcher once, and resolves the token names (`r_sub`, `p_obj`, ...).
// `load_policy_from_text` validates policy lines against the model and adds
// them to the in-memory store. `enforce` follows Casbin's algorithm:
//
// 1. The request must match the request definition token count.
// 2. When the policy has rows and the matcher references the policy (`p_`),
// the matcher is evaluated once per row; each row's `eft` column maps to
// allow / deny / indeterminate, and `merge_effects` decides whether to
// stop early. A row without an `eft` column counts as allow.
// 3. Otherwise the matcher is evaluated once with no policy values; a truthy
// result contributes an allow effect, and the effect expression merges
// that single result.
//
// The final decision is `effect == Allow`. Role definitions (`g`, `g2`) get
// one role manager each; the manager is injected into the function registry
// under the definition key, and role links are rebuilt from the grouping
// policy after every load or grouping-policy change (see `build_role_links`
// and `enable_auto_build_role_links`).
///|
/// An enforcer bound to one model and one in-memory policy store.
pub(all) struct Enforcer {
model : Model
store : PolicyStore
effect : EffectExpression
functions : FunctionRegistry
matcher : Expr
uses_policy : Bool
request_tokens : Array[String]
policy_type : String
policy_tokens : Array[String]
eft_index : Int?
role_managers : Array[(String, RoleManager)]
mut auto_build_role_links : Bool
}
///|
/// Builds an enforcer from a loaded model.
///
/// Returns `Err(ModelValidation)` for an unsupported policy effect or a
/// missing matcher, and `Err(MatcherSyntax)` when the matcher does not
/// parse.
pub fn Enforcer::new(model : Model) -> Result[Enforcer, CasbinError] {
Ok(enforcer_new_raise(model)) catch {
error => Err(error)
}
}
///|
fn enforcer_new_raise(model : Model) -> Enforcer raise CasbinError {
let matcher_assertion = match model.matcher("m") {
Some(assertion) => assertion
None => raise casbin_error(ModelValidation, "missing matcher \"m\"")
}
let matcher_source = preprocess_expression(matcher_assertion.value())
let matcher = match parse_matcher(matcher_source) {
Ok(expression) => expression
Err(error) => raise error
}
let effect_assertion = match model.policy_effect() {
Some(assertion) => assertion
None => raise casbin_error(ModelValidation, "missing policy effect")
}
let effect_text = preprocess_expression(effect_assertion.value())
let effect = match EffectExpression::parse(effect_text) {
Some(effect) => effect
None =>
raise casbin_error(
ModelValidation,
"unsupported policy effect \"" + effect_text + "\"",
)
}
let request_assertion = match model.request_definition("r") {
Some(assertion) => assertion
None =>
raise casbin_error(ModelValidation, "missing request definition \"r\"")
}
let policy_assertion = match model.policy_definition("p") {
Some(assertion) => assertion
None =>
raise casbin_error(ModelValidation, "missing policy definition \"p\"")
}
let request_tokens = qualified_tokens(request_assertion)
let policy_tokens = qualified_tokens(policy_assertion)
let eft_index = find_token(policy_tokens, "p_eft")
let functions = builtin_functions()
let role_managers : Array[(String, RoleManager)] = []
for assertion in model.role {
let manager = RoleManager::new()
functions.add(assertion.key(), make_g_function(manager))
role_managers.push((assertion.key(), manager))
}
{
model,
store: PolicyStore::new(),
effect,
functions,
matcher,
uses_policy: matcher_source.contains("p_"),
request_tokens,
policy_type: "p",
policy_tokens,
eft_index,
role_managers,
auto_build_role_links: true,
}
}
///|
/// The model this enforcer was built from.
pub fn Enforcer::model(self : Enforcer) -> Model {
self.model
}
///|
/// The in-memory policy store.
pub fn Enforcer::policy(self : Enforcer) -> PolicyStore {
self.store
}
///|
/// Registers a custom matcher function, replacing an existing one.
pub fn Enforcer::add_function(
self : Enforcer,
name : String,
function : (Array[Value]) -> Value raise CasbinError,
) -> Unit {
self.functions.add(name, function)
}
///|
/// Rebuilds every role graph from the grouping policy, mirroring Casbin's
/// `BuildRoleLinks`. Called automatically after policy loads and grouping
/// policy changes unless automatic rebuilding is disabled.
pub fn Enforcer::build_role_links(self : Enforcer) -> Result[Unit, CasbinError] {
Ok(enforcer_build_role_links_raise(self)) catch {
error => Err(error)
}
}
///|
fn enforcer_build_role_links_raise(
enforcer : Enforcer,
) -> Unit raise CasbinError {
for entry in enforcer.role_managers {
let key = entry.0
let manager = entry.1
manager.clear()
let token_count = match enforcer.model.role_definition(key) {
Some(assertion) => assertion.tokens().length()
None => 0
}
if token_count < 2 {
raise casbin_error(
ModelValidation,
"role definition \"" + key + "\" must have at least 2 tokens",
)
}
for row in enforcer.store.rows(key) {
if row.length() < token_count {
raise casbin_error(
Enforcement,
"grouping policy elements do not meet role definition \"" + key + "\"",
)
}
if token_count >= 3 {
manager.add_link(row[0], row[1], domain=row[2])
} else {
manager.add_link(row[0], row[1])
}
}
}
}
///|
/// Enables or disables automatic role-link rebuilding. Enabled by default,
/// like Casbin.
pub fn Enforcer::enable_auto_build_role_links(
self : Enforcer,
enabled : Bool,
) -> Unit {
self.auto_build_role_links = enabled
}
///|
/// Whether automatic role-link rebuilding is enabled.
pub fn Enforcer::is_auto_build_role_links(self : Enforcer) -> Bool {
self.auto_build_role_links
}
///|
/// Builds the `g`-style matcher function for one role manager: two
/// arguments test `has_link`, three arguments additionally pass the domain.
/// Like Casbin, a failed lookup is `false`; a wrong argument count or a
/// non-string argument raises `MatcherEval`.
fn make_g_function(
manager : RoleManager,
) -> (Array[Value]) -> Value raise CasbinError {
arguments => {
let count = arguments.length()
if count != 2 && count != 3 {
raise casbin_error(
MatcherEval,
"g: expected 2 or 3 arguments, but got " + count.to_string(),
)
}
let name1 = match arguments[0] {
Value::String(name) => name
other =>
raise casbin_error(
MatcherEval,
"g: argument must be a string, got " + other.type_name(),
)
}
let name2 = match arguments[1] {
Value::String(name) => name
other =>
raise casbin_error(
MatcherEval,
"g: argument must be a string, got " + other.type_name(),
)
}
let linked = if count == 2 {
manager.has_link(name1, name2)
} else {
let domain = match arguments[2] {
Value::String(name) => name
other =>
raise casbin_error(
MatcherEval,
"g: argument must be a string, got " + other.type_name(),
)
}
manager.has_link(name1, name2, domain~)
}
Value::Bool(linked)
}
}
///|
/// Loads policy lines from text, validating each against the model.
///
/// `p` sections require every row to have exactly the token count of the
/// policy definition; `g` sections require at least the token count.
/// Duplicate rows are skipped. Returns `Err(ModelValidation)` for a policy
/// type the model does not define, and `Err(Enforcement)` for a row of the
/// wrong size.
pub fn Enforcer::load_policy_from_text(
self : Enforcer,
text : String,
) -> Result[Unit, CasbinError] {
Ok(enforcer_load_policy_raise(self, text)) catch {
error => Err(error)
}
}
///|
fn enforcer_load_policy_raise(
enforcer : Enforcer,
text : String,
) -> Unit raise CasbinError {
let lines = match parse_policy_text(text) {
Ok(lines) => lines
Err(error) => raise error
}
for line in lines {
let key = line.key()
let row = line.values()
if key.has_prefix("p") {
let assertion = match enforcer.model.policy_definition(key) {
Some(assertion) => assertion
None =>
raise casbin_error(
ModelValidation,
"policy type \"" + key + "\" is not defined in the model",
)
}
if row.length() != assertion.tokens().length() {
raise casbin_error(
Enforcement,
"invalid policy rule size for \"" +
key +
"\": expected " +
assertion.tokens().length().to_string() +
", got " +
row.length().to_string(),
)
}
} else if key.has_prefix("g") {
let assertion = match enforcer.model.role_definition(key) {
Some(assertion) => assertion
None =>
raise casbin_error(
ModelValidation,
"policy type \"" + key + "\" is not defined in the model",
)
}
if row.length() < assertion.tokens().length() {
raise casbin_error(
Enforcement,
"invalid policy rule size for \"" +
key +
"\": expected at least " +
assertion.tokens().length().to_string() +
", got " +
row.length().to_string(),
)
}
} else {
raise casbin_error(PolicySyntax, "invalid policy type \"" + key + "\"")
}
ignore(enforcer.store.add(key, row))
}
if enforcer.auto_build_role_links {
enforcer_build_role_links_raise(enforcer)
}
}
///|
/// Decides whether `request` is allowed.
///
/// The request must provide one value per request definition token.
pub fn Enforcer::enforce(
self : Enforcer,
request : Array[String],
) -> Result[Bool, CasbinError] {
Ok(enforcer_enforce_raise(self, request)) catch {
error => Err(error)
}
}
///|
fn enforcer_enforce_raise(
enforcer : Enforcer,
request : Array[String],
) -> Bool raise CasbinError {
if request.length() != enforcer.request_tokens.length() {
raise casbin_error(
Enforcement,
"invalid request size: expected " +
enforcer.request_tokens.length().to_string() +
", got " +
request.length().to_string(),
)
}
let rows = enforcer.store.rows(enforcer.policy_type)
let mut effect = Indeterminate
if rows.length() != 0 && enforcer.uses_policy {
let effects : Array[Effect] = []
let matches : Array[Bool] = []
for _i in 0.. flag
other =>
raise casbin_error(
Enforcement,
"matcher result should be bool, got " + other.type_name(),
)
}
let effects : Array[Effect] = [if matched { Allow } else { Indeterminate }]
let matches : Array[Bool] = [true]
let (merged, _explain_index) = merge_effects(
enforcer.effect,
effects,
matches,
0,
1,
)
effect = merged
}
effect == Allow
}
///|
/// Evaluates the matcher for one policy row; a numeric result counts as
/// matched when non-zero, like Casbin.
fn Enforcer::row_matches(
self : Enforcer,
request : Array[String],
row : Array[String],
) -> Bool raise CasbinError {
let value = self.eval_matcher(request, row)
match value {
Bool(flag) => flag
Int(number) => number != 0
Double(number) => number != 0.0
other =>
raise casbin_error(
Enforcement,
"matcher result should be bool, int or float, got " + other.type_name(),
)
}
}
///|
/// The effect contributed by one policy row: its `eft` value when the
/// policy definition has an `eft` token, otherwise allow.
fn Enforcer::row_effect(self : Enforcer, row : Array[String]) -> Effect {
match self.eft_index {
None => Allow
Some(index) =>
if index >= row.length() {
Indeterminate
} else {
match row[index] {
"allow" => Allow
"deny" => Deny
_ => Indeterminate
}
}
}
}
///|
/// Evaluates the matcher with the request and one policy row bound to the
/// `r_` / `p_` tokens.
fn Enforcer::eval_matcher(
self : Enforcer,
request : Array[String],
row : Array[String],
) -> Value raise CasbinError {
let lookup = (name : String) => {
if name.has_prefix("r_") {
match find_token(self.request_tokens, name) {
Some(index) => Some(Value::String(request[index]))
None => None
}
} else if name.has_prefix("p_") {
match find_token(self.policy_tokens, name) {
Some(index) =>
if index < row.length() {
Some(Value::String(row[index]))
} else {
Some(Value::String(""))
}
None => None
}
} else {
None
}
}
match self.matcher.eval(lookup, self.functions) {
Ok(value) => value
Err(error) => raise error
}
}
///|
/// The full token names of a definition assertion, for example
/// `["r_sub", "r_obj", "r_act"]`, matching Casbin's `AddDef`.
fn qualified_tokens(assertion : Assertion) -> Array[String] {
let tokens : Array[String] = []
for token in assertion.tokens() {
tokens.push(assertion.key() + "_" + token)
}
tokens
}
///|
fn find_token(tokens : Array[String], name : String) -> Int? {
for i in 0..