///|
/// Errors raised while decoding a JSON policy document.
pub(all) suberror PolicyParseError {
InvalidJson(String)
MissingField(String)
ExpectedType(field~ : String, expected~ : String)
InvalidEffect(String)
InvalidCondition(String)
InvalidPolicy(String)
UnknownField(String)
} derive(Debug, Eq)
///|
fn expect_object(
value : Json,
field~ : String,
) -> Map[String, Json] raise PolicyParseError {
match value {
Object(object) => object
_ => raise ExpectedType(field~, expected="object")
}
}
///|
fn expect_array(
value : Json,
field~ : String,
) -> Array[Json] raise PolicyParseError {
match value {
Array(array) => array
_ => raise ExpectedType(field~, expected="array")
}
}
///|
fn expect_string(
value : Json,
field~ : String,
) -> String raise PolicyParseError {
match value {
String(string) => string
_ => raise ExpectedType(field~, expected="string")
}
}
///|
fn required_field(
object : Map[String, Json],
field : String,
) -> Json raise PolicyParseError {
match object.get(field) {
Some(value) => value
None => raise MissingField(field)
}
}
///|
fn reject_unknown_fields(
object : Map[String, Json],
allowed : Array[String],
location : String,
) -> Unit raise PolicyParseError {
for field, _ in object {
let mut known = false
for candidate in allowed {
if field == candidate {
known = true
break
}
}
if !known {
raise UnknownField(location + "." + field)
}
}
}
///|
fn decode_permissions(
value : Json,
field~ : String,
) -> Array[String] raise PolicyParseError {
let permissions = expect_array(value, field~)
let decoded : Array[String] = []
for permission in permissions {
decoded.push(expect_string(permission, field~))
}
decoded
}
///|
fn decode_roles(
value : Json,
field~ : String,
) -> Map[String, Array[String]] raise PolicyParseError {
let object = expect_object(value, field~)
let roles : Map[String, Array[String]] = {}
for role, permissions in object {
roles[role] = decode_permissions(permissions, field=field + "." + role)
}
roles
}
///|
fn decode_bindings(value : Json) -> Array[RoleBinding] raise PolicyParseError {
let entries = expect_array(value, field="bindings")
let bindings : Array[RoleBinding] = []
for entry in entries {
let object = expect_object(entry, field="bindings[]")
reject_unknown_fields(object, ["subject", "role", "resource"], "bindings[]")
bindings.push(
RoleBinding(
subject=expect_string(
required_field(object, "subject"),
field="bindings[].subject",
),
role=expect_string(
required_field(object, "role"),
field="bindings[].role",
),
resource=decode_optional_matcher(object, "resource", "bindings[]"),
),
)
}
bindings
}
///|
fn decode_effect(value : Json) -> Effect raise PolicyParseError {
match expect_string(value, field="rules[].effect") {
"allow" => Allow
"deny" => Deny
other => raise InvalidEffect(other)
}
}
///|
fn decode_attribute_value(
value : Json,
field~ : String,
) -> AttributeValue raise PolicyParseError {
decode_attribute_value_at_depth(value, field, 0)
}
///|
fn decode_attribute_value_at_depth(
value : Json,
field : String,
depth : Int,
) -> AttributeValue raise PolicyParseError {
if depth > 16 {
raise InvalidPolicy("attribute nesting exceeds 16 levels at " + field)
}
match value {
String(string) => StringValue(string)
Object(fields) => {
let values : Map[String, AttributeValue] = {}
for name, item in fields {
if name == "" {
raise InvalidPolicy(field + " contains an empty attribute name")
}
values[name] = decode_attribute_value_at_depth(
item,
field + "." + name,
depth + 1,
)
}
ObjectValue(values)
}
Array(items) => {
let values : Array[String] = []
for item in items {
values.push(expect_string(item, field=field + "[]"))
}
StringListValue(values)
}
True => BoolValue(true)
False => BoolValue(false)
Number(number, ..) => {
let integer = number.to_int()
if Double::from_int(integer) == number {
IntValue(integer)
} else {
raise ExpectedType(
field~,
expected="string, string array, object, boolean, or integer",
)
}
}
_ =>
raise ExpectedType(
field~,
expected="string, string array, object, boolean, or integer",
)
}
}
///|
fn decode_comparison(value : Json) -> Comparison raise PolicyParseError {
match expect_string(value, field="rules[].condition.compare.op") {
"lt" => LessThan
"lte" => LessOrEqual
"gt" => GreaterThan
"gte" => GreaterOrEqual
_ => raise InvalidCondition("compare op must be lt, lte, gt, or gte")
}
}
///|
fn decode_compare(value : Json) -> Condition raise PolicyParseError {
let object = expect_object(value, field="rules[].condition.compare")
if object.length() != 3 {
raise InvalidCondition("compare requires only path, op, and value")
}
let threshold = decode_attribute_value(
required_field(object, "value"),
field="rules[].condition.compare.value",
)
match threshold {
IntValue(value) =>
Compare(
path=expect_string(
required_field(object, "path"),
field="rules[].condition.compare.path",
),
op=decode_comparison(required_field(object, "op")),
value~,
)
_ =>
raise ExpectedType(
field="rules[].condition.compare.value",
expected="integer",
)
}
}
///|
fn decode_same(value : Json) -> Condition raise PolicyParseError {
let object = expect_object(value, field="rules[].condition.same")
if object.length() != 2 {
raise InvalidCondition("same requires only left and right")
}
Same(
left=expect_string(
required_field(object, "left"),
field="rules[].condition.same.left",
),
right=expect_string(
required_field(object, "right"),
field="rules[].condition.same.right",
),
)
}
///|
fn decode_one_of(value : Json) -> Condition raise PolicyParseError {
let object = expect_object(value, field="rules[].condition.one_of")
if object.length() != 2 {
raise InvalidCondition("one_of requires only path and values")
}
let source = expect_array(
required_field(object, "values"),
field="rules[].condition.one_of.values",
)
let values : Array[AttributeValue] = []
for item in source {
values.push(
decode_attribute_value(item, field="rules[].condition.one_of.values[]"),
)
}
OneOf(
path=expect_string(
required_field(object, "path"),
field="rules[].condition.one_of.path",
),
values~,
)
}
///|
fn decode_contains(value : Json) -> Condition raise PolicyParseError {
let object = expect_object(value, field="rules[].condition.contains")
if object.length() != 2 {
raise InvalidCondition("contains requires only path and value")
}
Contains(
path=expect_string(
required_field(object, "path"),
field="rules[].condition.contains.path",
),
value=expect_string(
required_field(object, "value"),
field="rules[].condition.contains.value",
),
)
}
///|
fn decode_string_values(
value : Json,
field : String,
) -> Array[String] raise PolicyParseError {
let source = expect_array(value, field~)
let values : Array[String] = []
for item in source {
values.push(expect_string(item, field=field + "[]"))
}
values
}
///|
fn decode_matcher_value(
value : Json,
field : String,
) -> Matcher raise PolicyParseError {
match value {
String(exact) => Exact(exact)
Object(spec) => {
if spec.length() != 1 {
raise ExpectedType(field~, expected="string or one-key glob object")
}
match spec.get("glob") {
Some(pattern) => Glob(expect_string(pattern, field=field + ".glob"))
None =>
raise ExpectedType(field~, expected="string or one-key glob object")
}
}
_ => raise ExpectedType(field~, expected="string or one-key glob object")
}
}
///|
fn decode_string_match(value : Json) -> Condition raise PolicyParseError {
let object = expect_object(value, field="rules[].condition.string_matches")
if object.length() != 2 {
raise InvalidCondition("string_matches requires only path and matcher")
}
StringMatches(
path=expect_string(
required_field(object, "path"),
field="rules[].condition.string_matches.path",
),
matcher=decode_matcher_value(
required_field(object, "matcher"),
"rules[].condition.string_matches.matcher",
),
)
}
///|
fn decode_list_membership(
value : Json,
operator : String,
) -> Condition raise PolicyParseError {
let field = "rules[].condition." + operator
let object = expect_object(value, field~)
if object.length() != 2 {
raise InvalidCondition(operator + " requires only path and values")
}
let path = expect_string(
required_field(object, "path"),
field=field + ".path",
)
let values = decode_string_values(
required_field(object, "values"),
field + ".values",
)
if operator == "contains_any" {
ContainsAny(path~, values~)
} else {
ContainsAll(path~, values~)
}
}
///|
fn decode_condition(value : Json) -> Condition raise PolicyParseError {
let object = expect_object(value, field="rules[].condition")
if object.length() != 1 {
raise InvalidCondition("a condition must have exactly one operator")
}
match object.get("exists") {
Some(path) =>
return Exists(expect_string(path, field="rules[].condition.exists"))
None => ()
}
match object.get("equals") {
Some(equals) => {
let fields = expect_object(equals, field="rules[].condition.equals")
if fields.length() != 2 {
raise InvalidCondition("equals requires only path and value")
}
return Equals(
path=expect_string(
required_field(fields, "path"),
field="rules[].condition.equals.path",
),
value=decode_attribute_value(
required_field(fields, "value"),
field="rules[].condition.equals.value",
),
)
}
None => ()
}
match object.get("same") {
Some(same) => return decode_same(same)
None => ()
}
match object.get("one_of") {
Some(one_of) => return decode_one_of(one_of)
None => ()
}
match object.get("contains") {
Some(contains) => return decode_contains(contains)
None => ()
}
match object.get("string_matches") {
Some(spec) => return decode_string_match(spec)
None => ()
}
match object.get("contains_any") {
Some(spec) => return decode_list_membership(spec, "contains_any")
None => ()
}
match object.get("contains_all") {
Some(spec) => return decode_list_membership(spec, "contains_all")
None => ()
}
match object.get("compare") {
Some(compare) => return decode_compare(compare)
None => ()
}
match object.get("all") {
Some(conditions) =>
return AllOf(decode_conditions(conditions, field="rules[].condition.all"))
None => ()
}
match object.get("any") {
Some(conditions) =>
return AnyOf(decode_conditions(conditions, field="rules[].condition.any"))
None => ()
}
match object.get("not") {
Some(condition) => return Not(decode_condition(condition))
None => ()
}
raise InvalidCondition(
"expected exists, equals, same, one_of, contains, string_matches, contains_any, contains_all, compare, all, any, or not",
)
}
///|
fn decode_conditions(
value : Json,
field~ : String,
) -> Array[Condition] raise PolicyParseError {
let values = expect_array(value, field~)
let conditions : Array[Condition] = []
for value in values {
conditions.push(decode_condition(value))
}
conditions
}
///|
fn decode_optional_matcher(
object : Map[String, Json],
field : String,
location : String,
) -> Matcher raise PolicyParseError {
match object.get(field) {
None => Any
Some(value) => decode_matcher_value(value, location + "." + field)
}
}
///|
fn decode_rules(value : Json) -> Array[Rule] raise PolicyParseError {
let entries = expect_array(value, field="rules")
let rules : Array[Rule] = []
for entry in entries {
let object = expect_object(entry, field="rules[]")
reject_unknown_fields(
object,
["id", "effect", "subject", "action", "resource", "condition"],
"rules[]",
)
rules.push(
Rule(
id=expect_string(required_field(object, "id"), field="rules[].id"),
effect=decode_effect(required_field(object, "effect")),
subject=decode_optional_matcher(object, "subject", "rules[]"),
action=decode_optional_matcher(object, "action", "rules[]"),
resource=decode_optional_matcher(object, "resource", "rules[]"),
condition=match object.get("condition") {
Some(condition) => decode_condition(condition)
None => Always
},
),
)
}
rules
}
///|
/// Decode the documented JSON representation of a policy.
///
/// `roles`, `role_parents`, `bindings`, and `rules` are optional. Unknown fields
/// are rejected to prevent misspelled restrictions from widening access.
pub fn policy_from_json(source : String) -> Policy raise PolicyParseError {
let root = @json.parse(source) catch {
error => raise InvalidJson(error.to_string())
}
let object = expect_object(root, field="policy")
reject_unknown_fields(
object,
["roles", "role_parents", "bindings", "rules"],
"policy",
)
let role_permissions = match object.get("roles") {
Some(value) => decode_roles(value, field="roles")
None => {}
}
let role_parents = match object.get("role_parents") {
Some(value) => decode_roles(value, field="role_parents")
None => {}
}
let bindings = match object.get("bindings") {
Some(value) => decode_bindings(value)
None => []
}
let rules = match object.get("rules") {
Some(value) => decode_rules(value)
None => []
}
let policy = Policy(role_permissions~, role_parents~, bindings~, rules~)
let issues = policy.validate()
if issues.length() > 0 {
raise InvalidPolicy(issues[0].code + " at " + issues[0].location)
}
policy
}