///|
pub(all) suberror ReleaseInputError {
InvalidReleaseInput(String)
} derive(Debug, Eq)
///|
pub(all) struct ReleasePlan {
config : ReleaseConfig
domains : Array[ReleaseAttributeDomain]
max_requests : Int
} derive(Debug)
///|
pub fn default_release_plan() -> ReleasePlan {
{ config: strict_release_config(), domains: [], max_requests: 10000, }
}
///|
fn release_object(
value : Json,
allowed : Array[String],
) -> Map[String, Json] raise ReleaseInputError {
match value {
Object(fields) => {
for key, _ in fields {
if !allowed.contains(key) {
raise InvalidReleaseInput("Unknown release field: " + key)
}
}
fields
}
_ => raise InvalidReleaseInput("Expected release JSON object")
}
}
///|
fn release_string(value : Json) -> String raise ReleaseInputError {
match value {
String(text) => text
_ => raise InvalidReleaseInput("Expected string")
}
}
///|
fn release_integer(value : Json) -> Int raise ReleaseInputError {
match value {
Number(number, ..) =>
if number < 0 || number > 100000 || number != number.to_int().to_double() {
raise InvalidReleaseInput("Expected integer between 0 and 100000")
} else {
number.to_int()
}
_ => raise InvalidReleaseInput("Expected integer")
}
}
///|
fn release_array(value : Json) -> Array[Json] raise ReleaseInputError {
match value {
Array(items) => items
_ => raise InvalidReleaseInput("Expected array")
}
}
///|
fn release_required(
fields : Map[String, Json],
key : String,
) -> Json raise ReleaseInputError {
match fields.get(key) {
Some(value) => value
None => raise InvalidReleaseInput("Missing field: " + key)
}
}
///|
fn release_optional_int(
fields : Map[String, Json],
key : String,
fallback : Int,
) -> Int raise ReleaseInputError {
match fields.get(key) {
Some(value) => release_integer(value)
None => fallback
}
}
///|
fn release_domain_value(
value : Json,
) -> @moon_policy.AttributeValue? raise ReleaseInputError {
if value is Null {
return None
}
// Reuse the upstream decoder for all typed attributes and its nesting
// checks instead of defining a second interpretation of these values.
let request = @moon_policy.request_from_json(
Json::object({
"subject": Json::string("sample"),
"action": Json::string("sample"),
"resource": Json::string("sample"),
"context": Json::object({ "value": value }),
}).stringify(),
) catch {
_ =>
raise InvalidReleaseInput(
"Domain value rejected by MoonPolicy attribute decoder",
)
}
request.context.get("value")
}
///|
/// Unknown configuration fields are rejected, including misspelled
/// restrictions. JSON null in a domain means an absent attribute.
pub fn parse_release_plan(
source : String,
) -> ReleasePlan raise ReleaseInputError {
let root = @json.parse(source) catch {
error => raise InvalidReleaseInput("Invalid release JSON: \{error}")
}
let fields = release_object(root, [
"max_requests", "action_budgets", "forbid_cross_tenant", "tenant_attribute",
"forbidden_actions", "domains",
])
let budgets : Array[ActionBudget] = []
if fields.get("action_budgets") is Some(value) {
for item in release_array(value) {
let budget = release_object(item, [
"action", "max_new_grants", "max_revocations",
])
budgets.push({
action: release_string(release_required(budget, "action")),
max_new_grants: release_optional_int(budget, "max_new_grants", 0),
max_revocations: release_optional_int(budget, "max_revocations", 0),
})
}
}
let forbidden_actions : Array[String] = []
if fields.get("forbidden_actions") is Some(value) {
for item in release_array(value) {
forbidden_actions.push(release_string(item))
}
}
let forbid_cross_tenant = match fields.get("forbid_cross_tenant") {
Some(True) => true
Some(False) => false
None => true
_ => raise InvalidReleaseInput("forbid_cross_tenant must be boolean")
}
let tenant_attribute = match fields.get("tenant_attribute") {
Some(value) => release_string(value)
None => "tenant"
}
let domains : Array[ReleaseAttributeDomain] = []
if fields.get("domains") is Some(value) {
for item in release_array(value) {
let domain = release_object(item, ["source", "key", "values"])
let domain_source = release_string(release_required(domain, "source"))
let source = match domain_source {
"subject" => PrincipalAttribute
"resource" => ResourceAttribute
"context" => RequestAttribute
_ =>
raise InvalidReleaseInput(
"Domain source must be subject, resource, or context",
)
}
let values : Array[@moon_policy.AttributeValue?] = []
for value in release_array(release_required(domain, "values")) {
values.push(release_domain_value(value))
}
domains.push({
source,
key: release_string(release_required(domain, "key")),
values,
})
}
}
{
config: {
action_budgets: budgets,
forbid_cross_tenant,
tenant_attribute,
forbidden_actions,
},
domains,
max_requests: release_optional_int(fields, "max_requests", 10000),
}
}