Skip to content

Commit

Permalink
feat(gate,sdk)!: new policy spec (#937)
Browse files Browse the repository at this point in the history
#### Migration notes

* Replaced true, false, and null to ALLOW, DENY and PASS.

Composition rules:
1. On traversal order:

* `ALLOW`: allow parent and all its children (ignore inner policies)
* `DENY`: deny parent and all its children  (ignore inner policies)
* `PASS`: pass through parent and evaluate each children (no-op,
equivalent to no policies)

2. On a single type (a.with_policy(X).with_policy(Y)):

`ALLOW` and `DENY` compose the same as true and false with the AND gate,
`PASS` does not participate.
* `ALLOW` & P = P
* `DENY` & P = `DENY` (e.g. DENY & ALLOW = DENY)
* `PASS` & P = P (does not participate)

- [x] The change comes with new or modified tests
- [x] Hard-to-understand functions have explanatory comments
- [x] End-user documentation is updated to reflect the change


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

- **New Features**
- Enhanced documentation for Metatype's mental model, including clearer
policy definitions and a comparison table with classical models.
- Introduction of a comprehensive tutorial on building a Metatype API,
covering setup, CRUD operations, and security practices.

- **Bug Fixes**
- Updated policy logic to return explicit 'ALLOW' or 'DENY' strings
instead of boolean values across various components.

- **Documentation**
- Improved clarity and detail in documentation for policies and core
concepts.
	- Added new sections for policy composition rules and traversal order.

- **Refactor**
- Streamlined policy management and evaluation logic across multiple
files, enhancing clarity and maintainability.

- **Tests**
- Added tests for new policy functionalities and updated existing tests
to reflect changes in policy handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Natoandro <[email protected]>
  • Loading branch information
michael-0acf4 and Natoandro authored Dec 21, 2024
1 parent ea8711f commit 37f51aa
Show file tree
Hide file tree
Showing 78 changed files with 2,374 additions and 1,805 deletions.
6 changes: 3 additions & 3 deletions docs/metatype.dev/docs/concepts/mental-model/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ Policies are a special type of function `t.func(t.struct({...}), t.boolean().opt

The policy decision can be:

- `true`: the access is authorized
- `false`: the access is denied
- `null`: the access in inherited from the parent types
- `ALLOW`: Grants access to the current type and all its descendants.
- `DENY`: Restricts access to the current type and all its descendants.
- `PASS`: Grants access to the current type while requiring individual checks for all its descendants (similar to the absence of policies).

<CodeBlock language="python">
{
Expand Down
18 changes: 16 additions & 2 deletions docs/metatype.dev/docs/reference/policies/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ The Deno runtime enable to understand the last abstraction. Policies are a way t

Metatype comes with some built-in policies, but you can use the Deno runtime to define your own:

- `policies.public()` is an alias for `Policy(PureFunMat("() => true"))` providing everyone open access.
- `policies.ctx("role_value", "role_field")` is a companion policy for the authentication strategy you learned in the previous section. It will verify the context and give adequate access to the user.
- `policies.public()` is an alias for `deno.policy("public", "() => 'PASS'")` providing everyone open access while still allowing field level custom access.
- `Policy.context("role_value", "role_field")` is a companion policy for the authentication strategy you learned in the previous section. It will verify the context and give adequate access to the user.

Policies are hierarchical in the sense that the request starts with a denial, and the root functions must explicitly provide an access or not. Once access granted, any further types can either inherit or override the access. Policies evaluate in order in case multiple ones are defined.

Expand All @@ -28,3 +28,17 @@ Policies are hierarchical in the sense that the request starts with a denial, an
typescript={require("!!code-loader!../../../../../examples/typegraphs/policies.ts")}
query={require("./policies.graphql")}
/>

## Composition rules

### Traversal order

- `ALLOW`: Allows access to the parent and all its descendants, disregarding inner policies.
- `DENY`: Denies access to the parent and all its descendants, disregarding inner policies.
- `PASS`: Allows access to the parent, each descendant will still be evaluated individually (equivalent to having no policies set).

### Chaining policies

If you have `foo.with_policy(A, B).with_policy(C)` for example, it will evaluated in batch as `[A, B, C]`.

If one or more policies fail (`DENY`), the type will be inaccessible.
4 changes: 2 additions & 2 deletions docs/metatype.dev/docs/tutorials/metatype-basics/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ typegraph("roadmap", (g) => {
const admins = deno.policy(
"admins",
`
(_args, { context }) => !!context.username
(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'
`,
);

Expand Down Expand Up @@ -619,7 +619,7 @@ def roadmap(g: Graph):
# the username value is only available if the basic
# extractor was successful
admins = deno.policy("admins", """
(_args, { context }) => !!context.username
(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'
""")

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def roadmap(g: Graph):
admins = deno.policy(
"admins",
"""
(_args, { context }) => !!context.username
(_args, { context }) => !!context.username ? 'PASS' : 'DENY'
""",
)

Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ await typegraph(
const admins = deno.policy(
"admins",
`
(_args, { context }) => !!context.username
(_args, { context }) => !!context.username ? 'PASS' : 'DENY'
`,
);

Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/func.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def roadmap(g: Graph):

admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
)
# skip:end

Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/func.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ await typegraph(

const admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
);
// skip:end

Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def math(g: Graph):
# the policy implementation is based on functions as well
restrict_referer = deno.policy(
"restrict_referer_policy",
'(_, context) => context.headers.referer && ["localhost", "metatype.dev"].includes(new URL(context.headers.referer).hostname)',
'(_, context) => context.headers.referer && ["localhost", "metatype.dev"].includes(new URL(context.headers.referer).hostname) ? "ALLOW" : "DENY"',
)

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ await typegraph(
// the policy implementation is based on functions itself
const restrict_referer = deno.policy(
"restrict_referer_policy",
'(_, context) => context.headers.referer && ["localhost", "metatype.dev"].includes(new URL(context.headers.referer).hostname)',
'(_, context) => context.headers.referer && ["localhost", "metatype.dev"].includes(new URL(context.headers.referer).hostname) ? "ALLOW" : "DENY"',
);

// or we can point to a local file that's accessible to the meta-cli
Expand Down
7 changes: 5 additions & 2 deletions examples/typegraphs/policies-example.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@
def policies_example(g):
# skip:end
deno = DenoRuntime()
public = deno.policy("public", "() => true") # noqa
team_only = deno.policy("team", "(ctx) => ctx.user.role === 'admin'") # noqa
public = deno.policy("public", "() => 'PASS'") # noqa
allow_all = deno.policy("allow_all", "() => 'ALLOW'") # noqa
team_only = deno.policy( # noqa
"team", "(ctx) => ctx.user.role === 'admin' ? 'ALLOW' : 'DENY' "
)
8 changes: 4 additions & 4 deletions examples/typegraphs/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ def policies(g: Graph):
deno = DenoRuntime()
random = RandomRuntime(seed=0, reset=None)

# `public` is sugar for to `() => true`
# `public` is sugar for to `(_args, _ctx) => "PASS"`
public = Policy.public()

admin_only = deno.policy(
"admin_only",
# note: policies either return true | false | null
"(args, { context }) => context.username ? context.username === 'admin' : null",
# note: policies either return "ALLOW" | "DENY" | "PASS"
"(args, { context }) => context?.username === 'admin' ? 'ALLOW' : 'DENY'",
)
user_only = deno.policy(
"user_only",
"(args, { context }) => context.username ? context.username === 'user' : null",
"(args, { context }) => context?.username === 'user' ? 'PASS' : 'DENY'",
)

g.auth(Auth.basic(["admin", "user"]))
Expand Down
8 changes: 4 additions & 4 deletions examples/typegraphs/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ typegraph(
// skip:end
const deno = new DenoRuntime();
const random = new RandomRuntime({ seed: 0 });
// `public` is sugar for `(_args, _ctx) => true`
// `public` is sugar for `(_args, _ctx) => "PASS"`
const pub = Policy.public();

const admin_only = deno.policy(
"admin_only",
// note: policies either return true | false | null
"(args, { context }) => context.username ? context.username === 'admin' : null",
// note: policies either return "ALLOW" | "DENY" | "PASS"
"(args, { context }) => context?.username === 'admin' ? 'ALLOW' : 'DENY'",
);
const user_only = deno.policy(
"user_only",
"(args, { context }) => context.username ? context.username === 'user' : null",
"(args, { context }) => context?.username === 'user' ? 'PASS' : 'DENY'",
);

g.auth(Auth.basic(["admin", "user"]));
Expand Down
4 changes: 3 additions & 1 deletion examples/typegraphs/programmable-api-gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ def programmable_api_gateway(g: Graph):
deno = DenoRuntime()

public = Policy.public()
roulette_access = deno.policy("roulette", "() => Math.random() < 0.5")
roulette_access = deno.policy(
"roulette", "() => Math.random() < 0.5 ? 'PASS' : 'DENY'"
)

my_api_format = """
static_a:
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/programmable-api-gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ typegraph(
const pub = Policy.public();
const roulette_access = deno.policy(
"roulette",
"() => Math.random() < 0.5",
"() => Math.random() < 0.5 ? 'PASS' : 'DENY'",
);

// skip:next-line
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/reduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def roadmap(g: Graph):

admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
)

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/reduce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ typegraph(

const admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
);

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def roadmap(g: Graph):

admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
)

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ typegraph(

const admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
);

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/roadmap-policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def roadmap(g: Graph):
# highlight-start
admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
)
# highlight-end

Expand Down
2 changes: 1 addition & 1 deletion examples/typegraphs/roadmap-policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ typegraph(

const admins = deno.policy(
"admins",
"(_args, { context }) => !!context.username",
"(_args, { context }) => !!context.username ? 'PASS' : 'DENY'",
);

g.expose(
Expand Down
4 changes: 3 additions & 1 deletion src/common/src/typegraph/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ pub enum Injection {
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TypeNodeBase {
pub title: String,
pub policies: Vec<PolicyIndices>,
#[serde(default)]
pub description: Option<String>,
#[serde(default, rename = "enum")]
Expand Down Expand Up @@ -158,6 +157,9 @@ pub struct ObjectTypeData<Id = TypeId> {
pub id: Vec<String>,
#[serde(default)]
pub required: Vec<String>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
#[serde(default)]
pub policies: IndexMap<String, Vec<PolicyIndices>>,
}

#[skip_serializing_none]
Expand Down
1 change: 1 addition & 0 deletions src/metagen/src/fdk_rust/stubs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ mod test {
TypeNode::Object {
data: ObjectTypeData {
properties: Default::default(),
policies: Default::default(),
id: vec![],
required: vec![],
},
Expand Down
8 changes: 8 additions & 0 deletions src/metagen/src/fdk_rust/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ mod test {
]
.into_iter()
.collect(),
policies: Default::default(),
id: vec![],
// FIXME: remove required
required: vec![],
Expand Down Expand Up @@ -615,6 +616,7 @@ pub enum MyUnion {
properties: [("obj_b".to_string(), 1)].into_iter().collect(),
id: vec![],
required: ["obj_b"].into_iter().map(Into::into).collect(),
policies: Default::default(),
},
base: TypeNodeBase {
title: "ObjA".into(),
Expand All @@ -624,6 +626,7 @@ pub enum MyUnion {
TypeNode::Object {
data: ObjectTypeData {
properties: [("obj_c".to_string(), 2)].into_iter().collect(),
policies: Default::default(),
id: vec![],
required: ["obj_c"].into_iter().map(Into::into).collect(),
},
Expand All @@ -635,6 +638,7 @@ pub enum MyUnion {
TypeNode::Object {
data: ObjectTypeData {
properties: [("obj_a".to_string(), 0)].into_iter().collect(),
policies: Default::default(),
id: vec![],
required: ["obj_a"].into_iter().map(Into::into).collect(),
},
Expand Down Expand Up @@ -665,6 +669,7 @@ pub struct ObjC {
TypeNode::Object {
data: ObjectTypeData {
properties: [("obj_b".to_string(), 1)].into_iter().collect(),
policies: Default::default(),
id: vec![],
required: ["obj_b"].into_iter().map(Into::into).collect(),
},
Expand All @@ -676,6 +681,7 @@ pub struct ObjC {
TypeNode::Object {
data: ObjectTypeData {
properties: [("union_c".to_string(), 2)].into_iter().collect(),
policies: Default::default(),
id: vec![],
required: ["union_c"].into_iter().map(Into::into).collect(),
},
Expand Down Expand Up @@ -714,6 +720,7 @@ pub enum CUnion {
TypeNode::Object {
data: ObjectTypeData {
properties: [("obj_b".to_string(), 1)].into_iter().collect(),
policies: Default::default(),
id: vec![],
required: ["obj_b"].into_iter().map(Into::into).collect(),
},
Expand All @@ -725,6 +732,7 @@ pub enum CUnion {
TypeNode::Object {
data: ObjectTypeData {
properties: [("either_c".to_string(), 2)].into_iter().collect(),
policies: Default::default(),
id: vec![],
required: ["either_c"].into_iter().map(Into::into).collect(),
},
Expand Down
2 changes: 1 addition & 1 deletion src/metagen/src/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub fn test_typegraph_2() -> Typegraph {
TypeNode::Object {
data: ObjectTypeData {
properties: Default::default(),
policies: Default::default(),
id: vec![],
required: vec![],
},
Expand Down Expand Up @@ -99,7 +100,6 @@ pub fn test_typegraph_2() -> Typegraph {
pub fn default_type_node_base() -> TypeNodeBase {
TypeNodeBase {
title: String::new(),
policies: vec![],
description: None,
enumeration: None,
}
Expand Down
Loading

0 comments on commit 37f51aa

Please sign in to comment.