Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gate,sdk)!: new policy spec #937

Merged
merged 14 commits into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
34 changes: 32 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,33 @@ 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).

### Inline chain

If you have `foo.with_policy(A, B).with_policy(C)` for example, it will be merged into a single chain `[A, B, C]`.

The evaluation is as follows:

- `ALLOW` and `DENY` compose the same as `true` and `false` under the logical `AND` operator.
- `PASS` does not participate.

Or more concretely:

- `ALLOW` & Other -> Other
michael-0acf4 marked this conversation as resolved.
Show resolved Hide resolved
- `DENY` & Other -> `DENY`
- `PASS` & Other -> Other (`PASS` is a no-op)

Examples:

- `[DENY, DENY, PASS, ALLOW]` -> `DENY`
- `[ALLOW, PASS]` -> `ALLOW`
- `[PASS, PASS, PASS]` -> `PASS`
- `[]` -> `PASS` (no policies)
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 ? 'ALLOW' : 'DENY'
michael-0acf4 marked this conversation as resolved.
Show resolved Hide resolved
""",
)

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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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"',
michael-0acf4 marked this conversation as resolved.
Show resolved Hide resolved
)

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' ? 'ALLOW' : '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' ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : '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 ? 'ALLOW' : 'DENY'",
michael-0acf4 marked this conversation as resolved.
Show resolved Hide resolved
);

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
Loading