///|
pub(all) struct ParsedPolicy {
policy : AccessPolicy
diagnostics : Array[Diagnostic]
} derive(Debug)
///|
pub(all) struct ParsedUniverse {
universe : RequestUniverse
diagnostics : Array[Diagnostic]
} derive(Debug)
///|
fn fields(line : String) -> Array[String] {
let result : Array[String] = []
for item in line.split(" ") {
let word = item.to_owned().trim().to_owned()
if word != "" {
result.push(word)
}
}
result
}
///|
fn comma_list(source : String) -> Array[String] {
let result : Array[String] = []
if source == "-" || source == "*" {
return result
}
for item in source.split(",") {
let word = item.to_owned().trim().to_owned()
if word != "" {
result.push(word)
}
}
result
}
///|
fn parse_attributes(source : String) -> Array[(String, String)] {
let result : Array[(String, String)] = []
if source == "-" {
return result
}
for item in source.split(",") {
let parts = item.split("=").collect()
if parts.length() == 2 {
result.push((parts[0].to_owned(), parts[1].to_owned()))
}
}
result
}
///|
fn attributes_well_formed(source : String) -> Bool {
if source == "-" {
return true
}
for item in source.split(",") {
let parts = item.split("=").collect()
if parts.length() != 2 || parts[0] == "" || parts[1] == "" {
return false
}
}
true
}
///|
fn parse_effect(word : String) -> RuleEffect? {
match word {
"permit" => Some(Permit)
"deny" => Some(Deny)
_ => None
}
}
///|
fn parse_tenant_relation(word : String) -> TenantRelation? {
match word {
"any" => Some(AnyTenant)
"same" => Some(SameTenant)
"other" => Some(OtherTenant)
_ => None
}
}
///|
fn parse_conditions(source : String) -> Array[AttributeCondition]? {
let result : Array[AttributeCondition] = []
if source == "-" {
return Some(result)
}
for item in source.split(",") {
let parts = item.split(":").collect()
if parts.length() != 4 {
return None
}
let source_type = match parts[0] {
"principal" => PrincipalAttribute
"resource" => ResourceAttribute
"request" => RequestAttribute
_ => return None
}
let operator = match parts[2] {
"eq" => Equals
"neq" => NotEquals
"exists" => Exists
"missing" => Missing
_ => return None
}
result.push({
source: source_type,
key: parts[1].to_owned(),
operator,
value: parts[3].to_owned(),
})
}
Some(result)
}
///|
fn parse_issue(line : Int, message : String) -> Diagnostic {
{ code: "PARSE", severity: Error, message: "line \{line}: \{message}", }
}
///|
/// Policy DSL: policy NAME; inherit CHILD PARENT;
/// rule ID EFFECT ROLES ACTIONS KINDS IDS TENANT CONDITIONS.
/// Lists are comma separated; '-' means unrestricted or empty.
pub fn parse_policy(source : String) -> ParsedPolicy {
let mut name = ""
let inheritance : Array[RoleInheritance] = []
let rules : Array[AccessRule] = []
let diagnostics : Array[Diagnostic] = []
let lines = source.split("\n").collect()
for index, raw in lines {
let line = raw.to_owned().trim().to_owned()
if line == "" || line.has_prefix("#") {
continue
}
let parts = fields(line)
match parts[0] {
"policy" =>
if parts.length() == 2 && name == "" {
name = parts[1]
} else {
diagnostics.push(parse_issue(index + 1, "expected one policy name"))
}
"inherit" =>
if parts.length() == 3 {
inheritance.push({ child: parts[1], parent: parts[2], })
} else {
diagnostics.push(
parse_issue(index + 1, "expected inherit CHILD PARENT"),
)
}
"rule" =>
if parts.length() == 9 {
match
(
parse_effect(parts[2]),
parse_tenant_relation(parts[7]),
parse_conditions(parts[8]),
) {
(Some(effect), Some(tenant_relation), Some(conditions)) =>
rules.push({
id: parts[1],
effect,
roles: comma_list(parts[3]),
actions: comma_list(parts[4]),
resource_kinds: comma_list(parts[5]),
resource_ids: comma_list(parts[6]),
tenant_relation,
conditions,
})
_ =>
diagnostics.push(
parse_issue(
index + 1,
"invalid rule effect, tenant relation, or condition",
),
)
}
} else {
diagnostics.push(
parse_issue(
index + 1,
"expected rule ID EFFECT ROLES ACTIONS KINDS IDS TENANT CONDITIONS",
),
)
}
_ => diagnostics.push(parse_issue(index + 1, "unknown policy directive"))
}
}
if name == "" {
diagnostics.push(parse_issue(0, "missing policy name"))
}
let policy : AccessPolicy = { name, role_inheritance: inheritance, rules, }
for issue in validate_policy(policy) {
diagnostics.push(issue)
}
{ policy, diagnostics, }
}
///|
/// Universe DSL:
/// principal ID TENANT ROLES ATTRS
/// resource ID KIND TENANT ATTRS
/// request PRINCIPAL ACTION RESOURCE ATTRS
/// matrix ACTION1,ACTION2
pub fn parse_universe(source : String) -> ParsedUniverse {
let principals : Array[Principal] = []
let resources : Array[AccessResource] = []
let requests : Array[AccessRequest] = []
let matrix_actions : Array[String] = []
let diagnostics : Array[Diagnostic] = []
let lines = source.split("\n").collect()
for index, raw in lines {
let line = raw.to_owned().trim().to_owned()
if line == "" || line.has_prefix("#") {
continue
}
let parts = fields(line)
if parts[0] == "matrix" {
if parts.length() == 2 {
for action in comma_list(parts[1]) {
if !matrix_actions.contains(action) {
matrix_actions.push(action)
}
}
} else {
diagnostics.push(parse_issue(index + 1, "expected matrix ACTIONS"))
}
continue
}
if parts.length() != 5 {
diagnostics.push(parse_issue(index + 1, "expected five fields"))
continue
}
if !attributes_well_formed(parts[4]) {
diagnostics.push(parse_issue(index + 1, "malformed attributes"))
continue
}
match parts[0] {
"principal" =>
principals.push({
id: parts[1],
tenant: parts[2],
roles: comma_list(parts[3]),
attributes: parse_attributes(parts[4]),
})
"resource" =>
resources.push({
id: parts[1],
kind: parts[2],
tenant: parts[3],
attributes: parse_attributes(parts[4]),
})
"request" =>
requests.push({
principal_id: parts[1],
action: parts[2],
resource_id: parts[3],
attributes: parse_attributes(parts[4]),
})
_ =>
diagnostics.push(parse_issue(index + 1, "unknown universe directive"))
}
}
let mut universe : RequestUniverse = { principals, resources, requests, }
if matrix_actions.length() > 0 {
let expansion = expand_request_matrix(universe, matrix_actions, 100000)
for issue in expansion.diagnostics {
diagnostics.push(issue)
}
universe = expansion.universe
}
for issue in validate_universe(universe) {
diagnostics.push(issue)
}
{ universe, diagnostics, }
}