mooncedar

A Cedar policy engine — parser, evaluator, and authorizer in MoonBit.

moon add jaredzhou/mooncedar@0.1.5
Download zip
Author
Version
0.1.5
License
Apache-2.0
Last updated
29 days ago
Downloads
35

Dependencies

README

#MoonCedar

A Cedar policy engine implemented in MoonBit — parser, evaluator, and authorizer.

#Installation

moon add jaredzhou/mooncedar@0.1.2

#Quick Start

// 1. Parse a Cedar policy
let policies = parse_policies(
#|permit (principal == User::"alice", action == Action::"view", resource in Album::"jane_vacation");|
)

// 2. Build an entity store from JSON
let entities_src =
#|[{"uid":{"type":"User","id":"alice"},"attrs":{},"tags":{},"parents":[]},{"uid":{"type":"Photo","id":"VacationPhoto94.jpg"},"attrs":{},"tags":{},"parents":[{"type":"Album","id":"jane_vacation"}]}]
let store : MapEntityStore = @json.from_json(@json.parse(entities_src))

// 3. Create a request
let req = Request::{
principal: @evaluator.concrete_uid("User", "alice"),
action: @evaluator.concrete_uid("Action", "view"),
resource: @evaluator.concrete_uid("Photo", "VacationPhoto94.jpg"),
context: Context::Concrete(Value::Record(Map([]))),
}

// 4. Authorize
let result = is_authorized(req, policies.iter(), store)
match result.decision {
Decision::Allow => println("permitted")
Decision::Deny => println("denied")
}
// => permitted

For a complete, runnable example (multi-user todo app with Cedar policies, pony HTTP routing, and a CLI client), see moon-examples/todo.

#Packages

PackagePurpose
jaredzhou/mooncedarUnified API: parse_policies, stringify, builder fns, type aliases (Expr, Policy, Request, Value, ...), MapEntityStore, is_authorized, evaluate, reauthorize
jaredzhou/mooncedar/astCore AST: Expr, Policy, Entity, EntityUID, Value, PartialValue, Type + builder methods
jaredzhou/mooncedar/evaluatorEntityStore trait, EvalError, Request, Context, EntityUIDEntry, helpers (concrete_uid, unknown_uid)
jaredzhou/mooncedar/parserLexer, recursive descent parser, stringify

Root types.mbt re-exports all commonly-used types as pub type aliases and builder functions, so most users only need import { "jaredzhou/mooncedar" }.

#Features

#Policy Language

Full Cedar policy syntax support:

  • permit / forbid effects with annotations
  • Scope constraints (==, in, in [set], All)
  • when / unless conditions
  • Expression language: &&, ||, !, >, <, >=, <=, !=, like, in, contains, has, ., .tag
  • Records, sets, if-then-else, extension function calls

#Expression Builder

let e = expr_principal()
.eq(expr_str("alice"))
.and_(expr_resource()
.has_tag(expr_str("confidential"))
)

#Policy Builder

let policy = default_policy()
.permit()
.principal_eq("User", "alice")
.action_eq("Action", "view")
.resource_in("Album", "photos")
.when_(expr_resource().has_tag(expr_str("public")))

#Strategy Validation

let errors = @ast.validate_policies(policies)

#Entity Store (Pluggable)

// In-memory store (built-in)
let store = new_map_store()

// From Cedar JSON
let store : MapEntityStore = @json.from_json(@json.parse(entities_src))

// Custom backend via pub(open) trait
struct DbStore { conn : Connection }
pub impl @evaluator.EntityStore for DbStore with get_entity(self, uid) {
db_lookup(self.conn, uid)
}

Entity hierarchy: is_descendant uses BFS for ancestor traversal. Wildcard matching uses two-pointer backtracking.

#Partial Evaluation

Support for 4 sources of unknown during partial eval — policies evaluate to residual expressions that can be re-evaluated when more information is available:

let answer = evaluate(req, policies.iter(), store1) // partial result
let answer = answer.reauthorize(req, store2, mapping) // fill unknowns
let result = answer.concretize() // final decision

The reauthorize method accepts an expanded entity store and a Map[String, Value] mapping to resolve unknowns by name.

#Request JSON

// Strings: "Type::\"id\"" → Concrete, "Type" → Unknown
// Objects: {"type":"...","id":"..."} → Concrete
let src =
#|{"principal":"User::\"alice\"","action":"Action::\"view\"","resource":"Photo::\"x\"","context":{}}|
let dto : RequestJSON = @json.from_json(@json.parse(src))
let req : Request = dto.to_request()
let json = to_request_json(req).to_json()

#Stringify (AST -> Cedar Source)

let src = stringify(policies)

#Status

  • 494 tests passing (parser, evaluator, authorizer, JSON, reauthorize, RequestJSON)
  • Entity stores as pluggable pub(open) traits
  • JSON serialization (Cedar-compatible format) for entities and requests
  • Policy validation
  • Full expression evaluation (18 expression variants, 12 binary + 3 unary operators)
  • Partial evaluation with reauthorize (4-category coverage)

#License

Apache-2.0

#
Condition

A when/unless condition in a policy.

#
Context

Request context: Concrete, Unknown, or Partial.

#
Entity

Application entity with attrs, tags, and parents.

#
EntityType

An entity type name.

#
EntityUID

A unique entity identifier, e.g. User::"alice".

#
EntityUIDEntry

A PARC slot: either a known EntityUID or a typed Unknown.

#
EvalError

Evaluation error.

#
Expr

A Cedar expression AST node.

#
ParseError

Parser error.

#
PartialValue

Partial evaluation result: concrete Value or residual Expr.

#
Policy

A Cedar policy.

#
PolicyEffect

Policy effect: Permit or Forbid.

#
Request

The PARC authorization request.

#
ScopeConstraint

A scope constraint on principal, action, or resource.

#
Type

The Cedar type system (Bool, Long, String, Set, Record, Entity, Extension).

#
Value

A runtime Cedar value.

#
AuthorizationResult

pub(all) struct AuthorizationResult {
decision : Decision
determining_policies : Array[DiagnosticReason]
errors : Array[DiagnosticError]
} derive(Eq,
Debug
)

Full result of an authorization request.

#
Decision

pub(all) enum Decision {
Allow
Deny
} derive(Eq,
Debug
)

Authorization decision.

#
DecisionRecord

type DecisionRecord derive(Eq,
Debug
)

Record of a single policy that fully satisfied (scope matched + all conditions passed).

#
DiagnosticError

pub(all) struct DiagnosticError {
policy_id : String
position :
Position

message : String
} derive(Eq,
Debug
)

Evaluation error for a specific policy.

#
DiagnosticReason

pub(all) struct DiagnosticReason {
policy_id : String
position :
Position

} derive(Eq,
Debug
)

Diagnostic — identifying which policy caused a reason or error.

#
MapEntityStore

In-memory entity store backed by a Map.

#
PartialAuthorizationAnswer

pub(all) struct PartialAuthorizationAnswer {
satisfied : Array[DecisionRecord]
residuals : Array[ResidualRecord]
errors : Array[DiagnosticError]
} derive(
Debug
)

Rich result of evaluating all policies against a Request. Holds satisfied decisions, residuals, and errors separately so downstream can concretize / reauthorize / sqlize as needed.

#
PartialAuthorizationAnswer::concretize

Concretize a PartialAuthorizationAnswer into a binary AuthorizationResult. Permit + no satisfied Forbid → Allow; otherwise → Deny. Residual policies are treated as non-determining (they don't count toward Allow).

#
PartialAuthorizationAnswer::reauthorize

Re-evaluate residual policies from a previous round, with an expanded entity store and/or a mapping from unknown names to concrete values.

The mapping parameter handles expression-level Unknown("x") nodes (e.g., context values that were unknown in the first pass but are now known). The store parameter provides expanded entity hierarchy (e.g., after entity slicing fetched missing entities).

Satisfied policies from the prior round are carried forward unconditionally.

#
RequestJSON

pub(all) struct RequestJSON {
principal : String?
action : String?
resource : String?
context :
Value
?
} derive(Eq,
Debug
)

JSON-friendly representation of an authorization Request.

{ "principal":"GitApp::User::\"JaneDoe\"", "action":"Action::\"view\"", "resource":"Photo::\"x\"", "context":{"is_admin":true} }

  • PARC strings: "Type::"id"" → Concrete, "Type" → Unknown, null/absent → Unknown("")
  • PARC objects: {"type":"..","id":".."} → normalised to string
  • Context absent/empty-object/null → Unknown, otherwise → Concrete

#
RequestJSON::to_request

Convert a RequestJSON into a full Request.

#
ResidualRecord

type ResidualRecord derive(Eq,
Debug
)

Record of a single policy where some conditions were residual (partial eval).

#
bool

Leaf constructors

#
cedar_entity_from_json

Parse a single Cedar entity from JSON. Handles the Cedar entity JSON format: { "uid": {"type":"...","id":"..."}, "attrs":{...}, "tags":{...}, "parents":[...] }

#
cedar_value_from_json

Parse a Cedar Value from JSON. Handles the Cedar-native entity JSON format:
  • plain numbers -> Long, strings -> String, booleans -> Bool
  • arrays -> Set
  • objects -> Record, EntityUID (__entity), or Extension (__extn)

#
cedar_value_from_object

Parse a Cedar Value from a JSON object: Record, EntityUID (__entity), or Extension (__extn).

#
default_policy

Create a default (permit-all) Policy struct.

#
entity_type

fn entity_type(name : String) ->
EntityType

Create an EntityType in builder context.

#
entity_uid

fn entity_uid(type_ : String, id : String) ->
EntityUID

Create an EntityUID in builder context.

#
euid

fn euid(type_ : String, id : String) ->
Expr

#
eval_expr

Evaluate a Cedar expression with the given request and entity store. Returns Value(concrete) for fully-reducible expressions, or Residual(expr) for expressions that cannot be fully reduced (partial evaluation).

#
evaluate

Evaluate all policies against a Request. Returns a rich PartialAuthorizationAnswer that preserves residuals and errors for downstream processing.

#
is_authorized

Evaluate policies and produce a binary authorization decision. Convenience that composes evaluate() + concretize().

#
long

fn long(l : Int64) ->
Expr

#
new_map_store

fn new_map_store() -> MapEntityStore

Create an empty MapEntityStore.

#
parse_policies

Parse one or more Cedar policies from source text.

#
principal

#
resource

#
str

fn str(s : String) ->
Expr

#
stringify

fn stringify(p :
Policy
) -> String

Convert a policy to Cedar source text.

#
stringify_expr

fn stringify_expr(e :
Expr
) -> String

Convert an expression to Cedar source text.

#
to_request_json

Convert a Request to its JSON-friendly representation.

#
value_to_expr

Convert a concrete Value back into an Expr AST node.