Cedar policies
Amazon's Cedar policies: requests authorized against policies and entities from JSON, policies validated against a schema, why a decision was reached by policy name, and syntax mistakes placed by line and column.
Version 1.0.0 · 4 functions · 3 crates
Functions
What use cedar.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use cedar_authorize, cedar_check from cedar (Quillo 0.6).
cedar_authorize(policies: str, entities: any, request: any, schema_value: any) → anyor an errorDecides a request. request is { principal, action, resource, context }, entities written User::"alice" or { type, id }. entities is Cedar's JSON, [{ uid, attrs, parents }], or text. schema is null, Cedar schema text or schema JSON: given, the request, the context and the entities are checked against it. Gives { allowed, decision, reasons, errors, why }: reasons are the policies that decided, by their @id annotation if they have one, errors the policies that failed to evaluate and so were skipped, why a sentence. A policy that fails to parse is an error with its line.
cedar_check(policies: str) → anyParses policies without deciding anything: { ok, errors, policies }, each error with line and column, each policy with its id, effect and annotations.
cedar_validate(policies: str, schema_value: any) → anyor an errorChecks policies against a schema, Cedar schema text or JSON: a misspelled attribute, a type that cannot fit, an action applied to what it does not take. { ok, errors, warnings }, each with message and line, in strict mode.
cedar_matrix(policies: str, entities: any, principals: list, actions: list, resources: list) → listor an errorWho may do what, one line per pair of principals, actions and resources: a small access matrix for an admin page. Each cell is { principal, action, resource, allowed, why }.
Get it
$ quillo install cedar # cedar.qwer into this folder$ quillo install cedar --example # and its example, cedar.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/cedar.qwer$ curl -O https://quillolang.org/plugins/files/cedar.qweThen, in your program, all of it or only what you call:
use cedar.qweruse cedar_authorize, cedar_check from cedar // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }cedar-policy = "=4.14.0"miette = "7"Example
// Who may see what in a photo-sharing app, decided by Cedar policies: an allow, a forbid that wins,// a request no policy permits, each with its reason, and policies checked against a schema.//// quillo run cedar.qwe then open http://localhost:8080/allowed, /denied, /matrix, /validate and /brokenuse "cedar.qwer"let POLICIES = `@id("viewers-see-albums")permit (principal in Group::"viewers", action == Action::"view", resource in Album::"holiday");@id("owners-do-anything")permit (principal, action, resource)when { resource has owner && resource.owner == principal };@id("no-private-for-contractors")forbid (principal, action == Action::"view", resource)when { resource.private && principal.contractor };@id("office-hours")permit (principal == User::"tom", action == Action::"edit", resource)when { context.hour >= 9 && context.hour < 17 };`let ENTITIES = [ { uid: { type: "User", id: "alice" }, attrs: { contractor: false }, parents: [{ type: "Group", id: "viewers" }] }, { uid: { type: "User", id: "carl" }, attrs: { contractor: true }, parents: [{ type: "Group", id: "viewers" }] }, { uid: { type: "User", id: "tom" }, attrs: { contractor: false }, parents: [] }, { uid: { type: "Group", id: "viewers" }, attrs: {}, parents: [] }, { uid: { type: "Album", id: "holiday" }, attrs: {}, parents: [] }, { uid: { type: "Photo", id: "beach.jpg" }, attrs: { private: false, owner: { __entity: { type: "User", id: "tom" } } }, parents: [{ type: "Album", id: "holiday" }] }, { uid: { type: "Photo", id: "tax.pdf" }, attrs: { private: true, owner: { __entity: { type: "User", id: "tom" } } }, parents: [{ type: "Album", id: "holiday" }] },]fn ask(who, what, res, ctx) = cedar_authorize(POLICIES, ENTITIES, { principal: who, action: what, resource: res, context: ctx }, null)route GET /allowed = { viewer: ask("User::\"alice\"", "Action::\"view\"", "Photo::\"beach.jpg\"", {})?, owner: ask({ type: "User", id: "tom" }, "Action::\"delete\"", "Photo::\"tax.pdf\"", {})?.why, in_hours: ask("User::\"tom\"", "Action::\"edit\"", "Photo::\"nothing\"", { hour: 10 })?.why,}route GET /denied = { forbidden: ask("User::\"carl\"", "Action::\"view\"", "Photo::\"tax.pdf\"", {})?, nothing: ask("User::\"alice\"", "Action::\"delete\"", "Photo::\"beach.jpg\"", {})?.why, late: ask("User::\"tom\"", "Action::\"edit\"", "Photo::\"nothing\"", { hour: 22 })?.why, // office-hours cannot see `hour`: that policy is skipped and named among the errors. errors: ask("User::\"tom\"", "Action::\"edit\"", "Photo::\"nothing\"", {})?.errors,}route GET /matrix = cedar_matrix(POLICIES, ENTITIES, ["User::\"alice\"", "User::\"carl\""], ["Action::\"view\""], ["Photo::\"beach.jpg\"", "Photo::\"tax.pdf\""])? | map((c) => [c.principal, c.resource, c.allowed])let SCHEMA = `entity Group;entity Album;entity User in [Group] { contractor: Bool };entity Photo in [Album] { private: Bool, owner: User };action view, edit, delete appliesTo { principal: User, resource: [Photo, Album], context: { hour?: Long } };`route GET /validate = { good: cedar_validate("permit (principal, action == Action::\"view\", resource) when { resource.private == false };", "entity User; entity Photo { private: Bool }; action view appliesTo { principal: User, resource: Photo };")?, typo: cedar_validate("permit (principal, action == Action::\"view\", resource)\nwhen { resource.privat };", "entity User; entity Photo { private: Bool }; action view appliesTo { principal: User, resource: Photo };")?, with_schema: cedar_authorize("permit (principal, action, resource);", ENTITIES, { principal: "User::\"alice\"", action: "Action::\"view\"", resource: "Photo::\"beach.jpg\"" }, SCHEMA)?.allowed, wrong_entity: cedar_authorize("permit (principal, action, resource);", [{ uid: { type: "User", id: "x" }, attrs: { contractor: "no" }, parents: [] }], { principal: "User::\"x\"", action: "Action::\"view\"", resource: "Album::\"a\"" }, SCHEMA).error, wrong_action: cedar_authorize("permit (principal, action, resource);", [], { principal: "User::\"x\"", action: "Action::\"fly\"", resource: "Album::\"a\"" }, SCHEMA).error,}route GET /broken = { check: cedar_check("permit (principal, action, resource);\n\nforbid (principal action, resource);"), list: cedar_check(POLICIES).policies | map((p) => `${p.effect} ${p.id}`), refused: cedar_authorize("permit (", [], { principal: "User::\"a\"", action: "Action::\"b\"", resource: "C::\"d\"" }, null).error, bad_uid: cedar_authorize("permit (principal, action, resource);", [], { principal: "alice", action: "Action::\"b\"", resource: "C::\"d\"" }, null).error,}$ quillo run cedar.qwe$ curl 'localhost:8080/allowed'$ curl 'localhost:8080/denied'$ curl 'localhost:8080/matrix'$ curl 'localhost:8080/validate'$ curl 'localhost:8080/broken'Source
//! Amazon's Cedar policies: requests authorized against policies and entities from JSON, policies validated against a schema, and why a decision was reached, each problem placed by line.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }//! cedar-policy = "=4.14.0"//! miette = "7"use cedar_policy::{Authorizer, Context, Decision, Entities, EntityUid, PolicyId, PolicySet, Request, Schema, ValidationMode, Validator};use miette::Diagnostic;use serde_json::{json, Value};use std::str::FromStr;/// Line and column, from 1, of a byte in a text.fn place(text: &str, at: usize) -> (usize, usize) { let before = &text[..text.floor_char_boundary(at.min(text.len()))]; (before.matches('\n').count() + 1, before.rsplit('\n').next().unwrap_or("").chars().count() + 1)}/// A problem with where it is: `{ message, line, column, help }`.fn described(d: &dyn Diagnostic, text: &str) -> Value { let mut message = d.to_string(); let mut src = std::error::Error::source(d); while let Some(e) = src { let s = e.to_string(); if !message.contains(&s) { message = format!("{}: {}", message, s); } src = e.source(); } // Only a label in a text we hold says anything. let at = if text.is_empty() { None } else { d.labels().and_then(|mut l| l.next()).map(|l| l.offset()) }; let (line, column) = match at { Some(a) => { let (l, c) = place(text, a); (json!(l), json!(c)) } None => (Value::Null, Value::Null), }; json!({ "message": message, "line": line, "column": column, "help": d.help().map(|h| h.to_string()) })}fn first_line(v: &Value) -> String { match v["line"].as_u64() { Some(l) => format!("line {}, column {}: {}", l, v["column"], v["message"].as_str().unwrap_or("")), None => v["message"].as_str().unwrap_or("").to_string(), }}fn parse(policies: &str) -> Result<PolicySet, String> { PolicySet::from_str(policies).map_err(|e| first_line(&described(&e, policies)))}fn schema(s: &Value) -> Result<Option<Schema>, String> { match s { Value::Null => Ok(None), Value::String(t) => Schema::from_cedarschema_str(t).map(|x| Some(x.0)).map_err(|e| format!("schema: {}", first_line(&described(&e, t)))), o => Schema::from_json_value(o.clone()).map(Some).map_err(|e| format!("schema: {}", first_line(&described(&e, "")))), }}fn uid(v: &Value, what: &str) -> Result<EntityUid, String> { match v { Value::String(s) => EntityUid::from_str(s).map_err(|e| format!("{} `{}` is not an entity like User::\"alice\": {}", what, s, e)), Value::Object(_) => { let t = v["type"].as_str().ok_or(format!("{}: no type", what))?; let id = v["id"].as_str().ok_or(format!("{}: no id", what))?; Ok(EntityUid::from_type_name_and_id(t.parse().map_err(|e: cedar_policy::ParseErrors| format!("{}: {}", what, e))?, id.parse().map_err(|e| format!("{}: {:?}", what, e))?)) } _ => Err(format!("{} is needed", what)), }}fn label(set: &PolicySet, id: &PolicyId) -> Value { let Some(p) = set.policy(id) else { return json!({ "id": id.to_string() }) }; let notes: serde_json::Map<String, Value> = p.annotations().map(|(k, v)| (k.to_string(), json!(v))).collect(); let name = notes.get("id").cloned().unwrap_or_else(|| json!(id.to_string())); let effect = format!("{:?}", p.effect()).to_lowercase(); json!({ "id": name, "effect": effect, "annotations": notes, "text": p.to_string().trim() })}/// Decides a request. `request` is `{ principal, action, resource, context }`, entities written `User::"alice"` or `{ type, id }`. `entities` is Cedar's JSON, `[{ uid, attrs, parents }]`, or text. `schema` is null, Cedar schema text or schema JSON: given, the request, the context and the entities are checked against it. Gives `{ allowed, decision, reasons, errors, why }`: `reasons` are the policies that decided, by their `@id` annotation if they have one, `errors` the policies that failed to evaluate and so were skipped, `why` a sentence. A policy that fails to parse is an error with its line.pub fn cedar_authorize(policies: &str, entities: serde_json::Value, request: serde_json::Value, schema_value: serde_json::Value) -> Result<Value, String> { let set = parse(policies)?; let sch = schema(&schema_value)?; let principal = uid(&request["principal"], "principal")?; let action = uid(&request["action"], "action")?; let resource = uid(&request["resource"], "resource")?; let context = match &request["context"] { Value::Null => Context::empty(), c => Context::from_json_value(c.clone(), sch.as_ref().map(|s| (s, &action))).map_err(|e| format!("context: {}", first_line(&described(&e, ""))))?, }; let list = match entities { Value::String(t) => serde_json::from_str(&t).map_err(|e| format!("entities: not JSON: {}", e))?, Value::Null => json!([]), o => o, }; let ents = Entities::from_json_value(list, sch.as_ref()).map_err(|e| format!("entities: {}", first_line(&described(&e, ""))))?; let q = Request::new(principal.clone(), action.clone(), resource.clone(), context, sch.as_ref()).map_err(|e| format!("the request: {}", first_line(&described(&e, ""))))?; let r = Authorizer::new().is_authorized(&q, &set, &ents); let allowed = r.decision() == Decision::Allow; let reasons: Vec<Value> = r.diagnostics().reason().map(|id| label(&set, id)).collect(); let errors: Vec<String> = r.diagnostics().errors().map(|e| { let v = described(e, ""); v["message"].as_str().unwrap_or("").to_string() }).collect(); let names: Vec<String> = reasons.iter().map(|x| x["id"].as_str().unwrap_or("").to_string()).collect(); let why = if allowed { format!("allowed by {}", names.join(", ")) } else if !names.is_empty() { format!("denied: forbidden by {}", names.join(", ")) } else if !errors.is_empty() { format!("denied: no policy permits it, and {} failed to evaluate", errors.len()) } else { "denied: no policy permits it".to_string() }; Ok(json!({ "allowed": allowed, "decision": if allowed { "Allow" } else { "Deny" }, "reasons": reasons, "errors": errors, "why": why }))}/// Parses policies without deciding anything: `{ ok, errors, policies }`, each error with `line` and `column`, each policy with its `id`, `effect` and annotations.pub fn cedar_check(policies: &str) -> Value { match PolicySet::from_str(policies) { Err(e) => { let errors: Vec<Value> = e.iter().map(|d| described(d, policies)).collect(); json!({ "ok": false, "errors": if errors.is_empty() { vec![described(&e, policies)] } else { errors }, "policies": [] }) } Ok(set) => { let list: Vec<Value> = set.policies().map(|p| { let mut v = label(&set, p.id()); v.as_object_mut().unwrap().remove("text"); v }).collect(); json!({ "ok": true, "errors": [], "policies": list }) } }}/// Checks policies against a schema, Cedar schema text or JSON: a misspelled attribute, a type that cannot fit, an action applied to what it does not take. `{ ok, errors, warnings }`, each with `message` and `line`, in strict mode.pub fn cedar_validate(policies: &str, schema_value: serde_json::Value) -> Result<Value, String> { let set = parse(policies)?; let sch = schema(&schema_value)?.ok_or("no schema given")?; let r = Validator::new(sch).validate(&set, ValidationMode::Strict); let errors: Vec<Value> = r.validation_errors().map(|e| described(e, policies)).collect(); let warnings: Vec<Value> = r.validation_warnings().map(|e| described(e, policies)).collect(); Ok(json!({ "ok": errors.is_empty(), "errors": errors, "warnings": warnings }))}/// Who may do what, one line per pair of `principals`, `actions` and `resources`: a small access matrix for an admin page. Each cell is `{ principal, action, resource, allowed, why }`.pub fn cedar_matrix(policies: &str, entities: serde_json::Value, principals: Vec<String>, actions: Vec<String>, resources: Vec<String>) -> Result<Vec<Value>, String> { let mut out = vec![]; for p in &principals { for a in &actions { for r in &resources { let d = cedar_authorize(policies, entities.clone(), json!({ "principal": p, "action": a, "resource": r }), Value::Null)?; out.push(json!({ "principal": p, "action": a, "resource": r, "allowed": d["allowed"], "why": d["why"] })); } } } Ok(out)}Related
Thread 0 posts
On Cedar policies: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.