Skip to content

Commit

Permalink
fix: outdated tests/snapshots + doc updates
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-0acf4 committed Dec 19, 2024
1 parent 60149e2 commit a0e0f60
Show file tree
Hide file tree
Showing 38 changed files with 810 additions and 649 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
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`
- `PASS` does not participate.

Or more concretely:
- `ALLOW` & Other -> Other
- `DENY` & Other -> `DENY`
- `PASS` & Other -> Other (`PASS` is a no-op)


Examples:
- `[DENY, DENY, ALLOW]` -> `DENY`
- `[ALLOW, PASS]` -> `ALLOW`
- `[PASS, PASS, PASS]` -> `PASS`
- `[]` -> `PASS`
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'
""",
)

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"',
)

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'",
);

g.expose(
Expand Down
2 changes: 1 addition & 1 deletion src/typegate/src/engine/planner/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class Planner {
for (const stage of stages) {
stage.varTypes = varTypes;
const stageId = stage.id();
if (stageId.startsWith("__schema")) {
if (stageId.startsWith("__")) {
// TODO: allow and reuse previous stage policy?
continue;
}
Expand Down
31 changes: 14 additions & 17 deletions src/typegate/src/engine/planner/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ export interface StageMetadata {
}

interface ComposePolicyOperand {
/** Field name according to the underlying struct on the typegraph */
canonFieldName: string;
/** The actual policy index selected against a given effect */
index: PolicyIdx;
}

Expand All @@ -56,6 +58,7 @@ export type OperationPoliciesConfig = {
};

interface PolicyForStage {
/** Field name according to the underlying struct on the typegraph */
canonFieldName: string;
/** Each item is either a PolicyIndicesByEffect or a number */
indices: Array<PolicyIndices>;
Expand Down Expand Up @@ -178,29 +181,25 @@ export class OperationPolicies {

let activeEffect = this.#getEffectOrNull(fakeStageMeta.typeIdx) ?? "read"; // root

outerIter: for (const { stageId, typeIdx } of stageMetaList) {
traversal: for (const { stageId, typeIdx } of stageMetaList) {
const newEffect = this.#getEffectOrNull(typeIdx);
if (newEffect != null) {
activeEffect = newEffect;
}
console.log(
` > stage ${stageId} :: ${activeEffect}`,
resolvedPolicyCachePerStage.get(stageId) ?? "<not yet>",
);

for (
const [priorStageId, verdict] of resolvedPolicyCachePerStage.entries()
) {
const globalAllows = priorStageId == EXPOSE_STAGE_ID &&
verdict == "ALLOW";
if (globalAllows) {
break outerIter;
break traversal;
}

const parentAllows = stageId.startsWith(priorStageId) &&
verdict == "ALLOW";
if (parentAllows) {
continue outerIter;
continue traversal;
} // elif deny => already thrown
}

Expand All @@ -227,14 +226,14 @@ export class OperationPolicies {
}));

throw new BadContext(
this.getRejectionReason(stageId, activeEffect, policyNames),
this.#getRejectionReason(stageId, activeEffect, policyNames),
);
}
}
}
}

getRejectionReason(
#getRejectionReason(
stageId: StageId,
effect: EffectType,
policiesData: Array<{ name: string; concernedField: string }>,
Expand Down Expand Up @@ -346,12 +345,6 @@ export class OperationPolicies {
}
}

// console.info(
// "Composing",
// effect,
// policies.map((p) => [p.canonFieldName, p.index]),
// );

if (operands.length == 0) {
return { authorized: "PASS" };
} else {
Expand All @@ -368,7 +361,6 @@ export class OperationPolicies {
let nestedSchema = this.tg.type(nestedTypeIdx);

if (isFunction(nestedSchema)) {
// TODO: collect the policies on the function as part of the oeprands
nestedTypeIdx = nestedSchema.output;
nestedSchema = this.tg.type(nestedTypeIdx);
}
Expand Down Expand Up @@ -431,7 +423,7 @@ export class OperationPolicies {
const actualIndex = index[effect] ?? null;
if (actualIndex == null) {
throw new BadContext(
this.getRejectionReason(stageId, effect, [{
this.#getRejectionReason(stageId, effect, [{
name: "__deny",
concernedField: canonFieldName,
}]),
Expand Down Expand Up @@ -461,4 +453,9 @@ export class OperationPolicies {
return targetStageId == parent ? node : null;
}).filter((name) => name != null);
}

// for testing
get stageToPolicies() {
return this.#stageToPolicies;
}
}
2 changes: 1 addition & 1 deletion src/typegate/src/typegraphs/prisma_migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@
"idempotent": true
},
"data": {
"script": "var _my_lambda = (_args, { context }) => context.username === 'admin'",
"script": "var _my_lambda = (_args, { context }) => context.username === 'admin' ? 'ALLOW' : 'DENY'",
"secrets": []
}
},
Expand Down
3 changes: 2 additions & 1 deletion src/typegate/src/typegraphs/prisma_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
def prisma_migration(g: Graph):
deno = DenoRuntime()
admin_only = deno.policy(
"admin_only", code="(_args, { context }) => context.username === 'admin'"
"admin_only",
code="(_args, { context }) => context.username === 'admin' ? 'ALLOW' : 'DENY'",
)

g.auth(Auth.basic(["admin"]))
Expand Down
Loading

0 comments on commit a0e0f60

Please sign in to comment.