// model.mbt — Casbin model loading.
//
// A model file declares up to five sections: request_definition,
// policy_definition, role_definition (optional), policy_effect, and
// matchers. Definition entries carry comma-separated tokens naming the
// positional arguments of a request or policy; expression entries keep
// their raw text for evaluation.
//
// Request, policy, effect, and matcher sections are required;
// role_definition is optional. Unknown sections and invalid tokens are
// rejected so that typos surface at load time rather than at
// enforcement time.
///|
/// One assertion in the model: a definition (`r`, `p`, `g`) or an
/// expression (`e`, `m`).
pub(all) struct Assertion {
key : String
value : String
tokens : Array[String]
} derive(Eq, Debug)
///|
/// The assertion key, for example `r`, `p`, `g`, `e`, or `m`.
pub fn Assertion::key(self : Assertion) -> String {
self.key
}
///|
/// The raw value as written in the configuration.
pub fn Assertion::value(self : Assertion) -> String {
self.value
}
///|
/// The comma-separated tokens of a definition assertion, trimmed and in
/// declaration order; empty for expression assertions.
pub fn Assertion::tokens(self : Assertion) -> Array[String] {
self.tokens
}
///|
/// Loaded Casbin model: request, policy, and role definitions, the
/// policy effect, and matcher expressions.
pub(all) struct Model {
request : Array[Assertion]
policy : Array[Assertion]
role : Array[Assertion]
effect : Assertion?
matcher : Array[Assertion]
} derive(Debug)
///|
/// Builds a model from parsed configuration.
///
/// Returns `Err(CasbinErrorKind::ModelValidation)` for a semantically
/// invalid model.
pub fn Model::from_config(config : Config) -> Result[Model, CasbinError] {
Ok(model_from_config_raise(config)) catch {
error => Err(error)
}
}
///|
/// Internal: builds a model, raising on the first violation.
fn model_from_config_raise(config : Config) -> Model raise CasbinError {
for name in config.section_names() {
if !is_known_section(name) {
raise casbin_error(ModelValidation, "unknown section [" + name + "]")
}
}
let request = load_definition(config, "request_definition", "r")
let policy = load_definition(config, "policy_definition", "p")
let role = load_definition(config, "role_definition", "g")
let effect = load_effect(config)
let matcher = load_matcher(config)
if request.is_empty() {
raise casbin_error(
ModelValidation,
"missing or empty [request_definition] section",
)
}
if policy.is_empty() {
raise casbin_error(
ModelValidation,
"missing or empty [policy_definition] section",
)
}
if matcher.is_empty() {
raise casbin_error(ModelValidation, "missing or empty [matchers] section")
}
{ request, policy, role, effect, matcher, }
}
///|
/// The request definition with the given key, for example `r`.
pub fn Model::request_definition(self : Model, key : String) -> Assertion? {
find_assertion(self.request, key)
}
///|
/// The policy definition with the given key, for example `p` or `p2`.
pub fn Model::policy_definition(self : Model, key : String) -> Assertion? {
find_assertion(self.policy, key)
}
///|
/// The role definition with the given key, for example `g` or `g2`.
pub fn Model::role_definition(self : Model, key : String) -> Assertion? {
find_assertion(self.role, key)
}
///|
/// The policy effect assertion.
pub fn Model::policy_effect(self : Model) -> Assertion? {
self.effect
}
///|
/// The matcher with the given key, for example `m` or `m2`.
pub fn Model::matcher(self : Model, key : String) -> Assertion? {
find_assertion(self.matcher, key)
}
///|
fn find_assertion(assertions : Array[Assertion], key : String) -> Assertion? {
for assertion in assertions {
if assertion.key == key {
return Some(assertion)
}
}
None
}
///|
fn is_known_section(name : String) -> Bool {
match name {
"request_definition"
| "policy_definition"
| "role_definition"
| "policy_effect"
| "matchers" => true
_ => false
}
}
///|
fn load_definition(
config : Config,
section : String,
prefix : String,
) -> Array[Assertion] raise CasbinError {
let assertions : Array[Assertion] = []
for entry in config.entries(section) {
if !entry.key.has_prefix(prefix) {
raise casbin_error(
ModelValidation,
"[" +
section +
"] key \"" +
entry.key +
"\" must start with \"" +
prefix +
"\"",
)
}
let tokens = split_tokens(entry.value)
assertions.push({ key: entry.key, value: entry.value, tokens, })
}
assertions
}
///|
fn load_effect(config : Config) -> Assertion? raise CasbinError {
let entries = config.entries("policy_effect")
if entries.is_empty() {
raise casbin_error(
ModelValidation,
"missing or empty [policy_effect] section",
)
}
if entries.length() > 1 {
raise casbin_error(
ModelValidation,
"[policy_effect] must contain exactly one entry",
)
}
let entry = entries[0]
if entry.value.is_empty() {
raise casbin_error(
ModelValidation,
"[policy_effect] entry \"" + entry.key + "\" is empty",
)
}
Some({ key: entry.key, value: entry.value, tokens: [], })
}
///|
fn load_matcher(config : Config) -> Array[Assertion] raise CasbinError {
let assertions : Array[Assertion] = []
for entry in config.entries("matchers") {
if !is_identifier(entry.key) {
raise casbin_error(
ModelValidation,
"[matchers] invalid key \"" + entry.key + "\"",
)
}
if entry.value.is_empty() {
raise casbin_error(
ModelValidation,
"[matchers] entry \"" + entry.key + "\" is empty",
)
}
assertions.push({ key: entry.key, value: entry.value, tokens: [], })
}
assertions
}
///|
fn split_tokens(value : String) -> Array[String] raise CasbinError {
let tokens : Array[String] = []
for raw in value.split(",") {
let token = raw.trim()
if token.is_empty() {
raise casbin_error(ModelValidation, "empty token in \"" + value + "\"")
}
if !is_identifier(token) {
raise casbin_error(
ModelValidation,
"invalid token \"" + token.to_owned() + "\"",
)
}
tokens.push(token.to_owned())
}
if tokens.is_empty() {
raise casbin_error(ModelValidation, "empty definition value")
}
tokens
}
///|
fn is_identifier(text : StringView) -> Bool {
if text.is_empty() {
return false
}
let mut index = 0
for ch in text.iter() {
let valid = if index == 0 {
ch.is_ascii_alphabetic() || ch == '_'
} else {
ch.is_ascii_digit() || ch.is_ascii_alphabetic() || ch == '_'
}
if !valid {
return false
}
index += 1
}
true
}