From 56829e944e2b4a5eba5aad87ae5bb8ab3e43ba61 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Wed, 11 Dec 2024 01:11:54 +0300 Subject: [PATCH 01/11] wip: working impl --- src/typegate/src/engine/planner/mod.ts | 46 +-- src/typegate/src/engine/planner/policies.ts | 321 +++++++++++------- src/typegate/src/runtimes/deno/deno.ts | 6 +- .../src/typegraphs/introspection.json | 4 +- src/typegate/src/typegraphs/typegate.json | 4 +- src/typegate/src/typegraphs/typegate.py | 3 +- src/typegraph/core/src/global_store.rs | 2 +- .../core/src/validation/materializers.rs | 12 + 8 files changed, 247 insertions(+), 151 deletions(-) diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index ed4709ef9..a10cddd1c 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -507,30 +507,32 @@ export class Planner { stages.push(stage); this.policiesBuilder.push(stage.id(), node.typeIdx, collected.policies); - const types = this.policiesBuilder.setReferencedTypes( - stage.id(), - node.typeIdx, - outputIdx, - inputIdx, - ); - - // nested quantifiers - let wrappedTypeIdx = outputIdx; - let wrappedType = this.tg.type(wrappedTypeIdx); - while (isQuantifier(wrappedType)) { - wrappedTypeIdx = getWrappedType(wrappedType); - wrappedType = this.tg.type(wrappedTypeIdx); - types.push(wrappedTypeIdx); + { + const types = this.policiesBuilder.setReferencedTypes( + stage.id(), + node.typeIdx, + outputIdx, + inputIdx, + ); + + // nested quantifiers + let wrappedTypeIdx = outputIdx; + let wrappedType = this.tg.type(wrappedTypeIdx); + while (isQuantifier(wrappedType)) { + wrappedTypeIdx = getWrappedType(wrappedType); + wrappedType = this.tg.type(wrappedTypeIdx); + types.push(wrappedTypeIdx); + } + + stages.push( + ...this.traverse( + { ...node, typeIdx: wrappedTypeIdx, parentStage: stage }, + stage, + ), + ); } - - stages.push( - ...this.traverse( - { ...node, typeIdx: wrappedTypeIdx, parentStage: stage }, - stage, - ), - ); - this.policiesBuilder.pop(stage.id()); + return stages; } diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index d82cbcd8c..109922ba3 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -19,6 +19,9 @@ import { Type } from "../../typegraph/type_node.ts"; import type { ArgPolicies } from "./args.ts"; import { BadContext } from "../../errors.ts"; +export type PolicyResolverOutput = "DENY" | "ALLOW" | "PASS" | (unknown & {}); +type GetResolverResult = (polIdx: PolicyIdx, effect: EffectType) => Promise; + export interface FunctionSubtreeData { typeIdx: TypeIdx; isTopLevel: boolean; @@ -26,15 +29,13 @@ export interface FunctionSubtreeData { referencedTypes: Map>; } -interface GetResolverResult { - (polIdx: PolicyIdx, effect: EffectType): Promise; -} type CheckResult = - | { authorized: true } + | { authorized: "ALLOW" } + | { authorized: "PASS" } | { - authorized: false; - policyIdx: PolicyIdx | null; + authorized: "DENY"; + policiesFailedIdx: Array; }; export type OperationPoliciesConfig = { @@ -44,7 +45,7 @@ export type OperationPoliciesConfig = { export class OperationPolicies { // should be private -- but would not be testable functions: Map; - private policyLists: Map; + private policiesForType: Map; private resolvers: Map; constructor( @@ -53,30 +54,33 @@ export class OperationPolicies { config: OperationPoliciesConfig, ) { this.functions = builder.subtrees; + console.log("functions", this.functions); - this.policyLists = new Map(); - for (const [stageId, subtree] of this.functions.entries()) { - const { funcTypeIdx, topLevel, referencedTypes } = subtree; + this.policiesForType = new Map(); + for (const [stageId, subtreeData] of this.functions.entries()) { + // Note: referencedTypes are the types that appear on the query + const { funcTypeIdx, topLevel, referencedTypes } = subtreeData; ensure( referencedTypes.has(stageId) && referencedTypes.get(stageId)!.includes(funcTypeIdx), "unexpected", ); + + // Set policies for each referenced type if any (on **selection** output) for (const types of referencedTypes.values()) { - // set policyLists for (const typeIdx of types) { - if (this.policyLists.has(typeIdx)) { + if (this.policiesForType.has(typeIdx)) { continue; } const policies = this.tg.type(typeIdx).policies; if (policies.length > 0) { - this.policyLists.set(typeIdx, policies); + this.policiesForType.set(typeIdx, policies); } } } // top-level functions must have policies - if (topLevel && !this.policyLists.has(funcTypeIdx)) { + if (topLevel && !this.policiesForType.has(funcTypeIdx)) { const details = [ `top-level function '${this.tg.type(funcTypeIdx).title}'`, `at '${stageId}'`, @@ -88,7 +92,7 @@ export class OperationPolicies { } this.resolvers = new Map(); - const policies = new Set([...this.policyLists.values()].flat()); + const policies = new Set([...this.policiesForType.values()].flat()); for (const idx of policies) { for (const polIdx of iterIndices(idx)) { const mat = this.tg.policyMaterializer(this.tg.policy(polIdx)); @@ -97,6 +101,7 @@ export class OperationPolicies { runtime.constructor === DenoRuntime, "Policies must run on a Deno Runtime", ); + if (!this.resolvers.has(polIdx)) { this.resolvers.set( polIdx, @@ -116,27 +121,27 @@ export class OperationPolicies { delete: new Set(), }; - const cache = new Map(); + const cache = new Map(); const getResolverResult = async ( - idx: PolicyIdx, + polIdx: PolicyIdx, effect: EffectType, - ): Promise => { + ): Promise => { verbose && logger.info( `checking policy '${ - this.tg.policy(idx).name - }'[${idx}] with effect '${effect}'...`, + this.tg.policy(polIdx).name + }'[${polIdx}] with effect '${effect}'...`, ); - if (cache.has(idx)) { - return cache.get(idx) as boolean | null; + if (cache.has(polIdx)) { + return cache.get(polIdx); } - const resolver = this.resolvers.get(idx); + const resolver = this.resolvers.get(polIdx); ensure( resolver != null, `Could not find resolver for the policy '${ - this.tg.policy(idx).name + this.tg.policy(polIdx).name }'; effect=${effect}`, ); @@ -148,62 +153,43 @@ export class OperationPolicies { variables: {}, effect: effect === "read" ? null : effect, }, - })) as boolean | null; - cache.set(idx, res); + })) as PolicyResolverOutput; + cache.set(polIdx, res); verbose && logger.info(`> authorize: ${res}`); return res; }; - // TODO refactor: too much indentation for (const [_stageId, subtree] of this.functions) { const effect = this.tg.materializer( this.tg.type(subtree.funcTypeIdx, Type.FUNCTION).materializer, ).effect.effect ?? "read"; - this.authorizeArgs( - subtree.argPolicies, - effect, - getResolverResult, - authorizedTypes[effect], - ); - for (const [stageId, types] of subtree.referencedTypes) { - for (const typeIdx of types) { - if (authorizedTypes[effect].has(typeIdx)) { - continue; - } - const policies = (this.policyLists.get(typeIdx) ?? []).map((p) => - typeof p === "number" ? p : p[effect] ?? null - ); - - if (policies.some((idx) => idx == null)) { - throw new BadContext( - this.getRejectionReason(stageId, typeIdx, effect, "__deny"), - ); - } - - const res = await this.checkTypePolicies( - policies as number[], - effect, - getResolverResult, - ); - - if (res.authorized) { - continue; - } + // await this.authorizeArgs( + // subtree.argPolicies, + // effect, + // getResolverResult, + // authorizedTypes[effect], + // ); - if (res.policyIdx == null) { - const typ = this.tg.type(typeIdx); - throw new Error( - `No policy took decision on type '${typ.title}' ('${typ.type}') at '.${stageId}'`, - ); + const req = { + authorizedTypes, + effect, + getResolverResult + }; + + // TODO: maybe collect errors for each stage then fail? + outerIter: for (const [stageId, refTypes] of subtree.referencedTypes) { + for (const [stageIdParent, verdict] of this.#resolvedPolicyCachePerStage) { + // Note: assumes parent id is a path string such as "subject.tags.name" + // Also assumes that the parent is traversed first as the list is built + // (e.g. "subject.tags" evaluated before "subject.tags.name") + if (stageId.startsWith(stageIdParent) && verdict == "ALLOW") { + continue outerIter; } - - const policyName = this.tg.policy(res.policyIdx).name; - throw new BadContext( - this.getRejectionReason(stageId, typeIdx, effect, policyName), - ); } + + await this.#checkPolicyForStage(stageId, refTypes, req); } } } @@ -212,64 +198,155 @@ export class OperationPolicies { stageId: StageId, typeIdx: TypeIdx, effect: EffectType, - policyName: string, + policyNames: Array, ): string { const typ = this.tg.type(typeIdx); - const details = [ - `policy '${policyName}'`, - `with effect '${effect}'`, - `on type '${typ.title}' ('${typ.type}')`, - `at '.${stageId}'`, - ].join(" "); - return `Authorization failed for ${details}`; + const details = policyNames + .map((policyName) => [ + `policy '${policyName}'`, + `with effect '${effect}'`, + `on type '${typ.title}' ('${typ.type}')`, + `at '.${stageId}'`, + ].join(" ")); + return `Authorization failed for ${details.join("; ")}`; } - private async authorizeArgs( - argPolicies: ArgPolicies, + // TODO: rm??? checkTypePolicies assumes the policy appear on the **selected** output either way + // private async authorizeArgs( + // argPolicies: ArgPolicies, + // effect: EffectType, + // getResolverResult: GetResolverResult, + // authorizedTypes: Set, + // ) { + // for (const [typeIdx, { argDetails, policyIndices }] of argPolicies) { + // if (authorizedTypes.has(typeIdx)) { + // continue; + // } + // authorizedTypes.add(typeIdx); + + // const res = await this.#checkTypePolicies( + // policyIndices, + // effect, + // getResolverResult, + // ); + // if (!res.authorized) { + // // unauthorized or no decision + // throw new Error(`Unexpected argument ${argDetails}`); + // } + // } + // } + + /** + * A single type may hold multiple policies + * + * * `ALLOW`: ALLOW & P = P + * * `DENY`: DENY & P = DENY + * + * DENY and ALLOW combine just like booleans with the AND gate + * + * PASS does not participate. + **/ + async #composePolicies( + policies: PolicyIdx[], effect: EffectType, getResolverResult: GetResolverResult, - authorizedTypes: Set, - ) { - for (const [typeIdx, { argDetails, policyIndices }] of argPolicies) { - if (authorizedTypes.has(typeIdx)) { - continue; + ): Promise { + const operands = []; + const deniersIdx = []; + for (const policyIdx of policies) { + const res = await getResolverResult(policyIdx, effect); + switch(res) { + case "ALLOW": { + operands.push(true); + break; + } + case "DENY": { + operands.push(false); + deniersIdx.push(policyIdx); + break; + } + case "PASS": { + continue; + } + default: { + // TODO: + throw new Error(`Could not take decision on value: ${JSON.stringify(res)}`) + } } - authorizedTypes.add(typeIdx); + } - const res = await this.checkTypePolicies( - policyIndices, - effect, - getResolverResult, - ); - if (!res.authorized) { - // unauthorized or no decision - throw new Error(`Unexpected argument ${argDetails}`); + if (operands.length == 0) { + return { authorized: "PASS" }; + } else { + if (operands.every((_bool) => _bool)) { + return { authorized: "ALLOW" }; + } else { + return { authorized: "DENY", policiesFailedIdx: deniersIdx } } } } - private async checkTypePolicies( - policies: PolicyIdx[], - effect: EffectType, - getResolverResult: GetResolverResult, - ): Promise { - if (policies.length === 0) { - return { authorized: true }; - } + #resolvedPolicyCachePerStage: Map = new Map(); - for (const polIdx of policies) { - const res = await getResolverResult(polIdx, effect); - if (res == null) { + async #checkPolicyForStage( + stageId: string, + referencedTypes: Array, + req: { + effect: EffectType, + authorizedTypes: Record>, + getResolverResult: GetResolverResult + } + ) { + for (const typeIdx of referencedTypes) { + if (req.authorizedTypes[req.effect].has(typeIdx)) { continue; } - if (res) { - return { authorized: true }; + + const allPolicies = this.policiesForType.get(typeIdx) ?? []; + const policies = allPolicies.map((p) => + typeof p === "number" ? p : p[req.effect] ?? null + ); + + if (policies.some((idx) => idx == null)) { + throw new BadContext( + this.getRejectionReason(stageId, typeIdx, req.effect, ["__deny"]), + ); } - return { authorized: false, policyIdx: polIdx }; - } + const res = await this.#composePolicies( + policies as number[], + req.effect, + req.getResolverResult, + ); + + console.log("verdict for", this.tg.type(typeIdx).title, res.authorized); - return { authorized: false, policyIdx: null }; + switch(res.authorized) { + case "ALLOW": { + this.#resolvedPolicyCachePerStage.set(stageId, "ALLOW"); + return; + } + case "PASS": { + this.#resolvedPolicyCachePerStage.set(stageId, "PASS"); + continue; + } + default: { + this.#resolvedPolicyCachePerStage.set(stageId, res.authorized); + + if (res.policiesFailedIdx.length == 0) { + const typ = this.tg.type(typeIdx); + throw new Error( + `No policy took decision on type '${typ.title}' ('${typ.type}') at '.${stageId}'`, + ); + } + + const policyNames = res.policiesFailedIdx.map((idx) => this.tg.policy(idx).name); + throw new BadContext( + this.getRejectionReason(stageId, typeIdx, req.effect, policyNames), + ); + } + } + } } } @@ -282,39 +359,39 @@ interface SubtreeData { } export class OperationPoliciesBuilder { - // stack of function stages - stack: SubtreeData[] = []; + // subtreeStack of function stages + subtreeStack: SubtreeData[] = []; subtrees: Map = new Map(); - current: SubtreeData | null = null; + currentSubtree: SubtreeData | null = null; constructor( private tg: TypeGraph, private config: OperationPoliciesConfig, ) {} - // set current function stage + /** set currentSubtree function stage */ push(stageId: StageId, funcTypeIdx: TypeIdx, argPolicies: ArgPolicies) { const subtreeData = { stageId, funcTypeIdx, argPolicies, - topLevel: this.stack.length === 0, + topLevel: this.subtreeStack.length === 0, referencedTypes: new Map(), }; - this.current = subtreeData; - this.stack.push(subtreeData); + this.currentSubtree = subtreeData; + this.subtreeStack.push(subtreeData); this.subtrees.set(stageId, subtreeData); } - // set current function stage to parent function stage + /** set currentSubtree function stage to parent function stage */ pop(stageId: StageId) { - ensure(this.stack.pop()!.stageId === stageId, "unexpected: invalid state"); - const top = this.stack.pop(); + ensure(this.subtreeStack.pop()!.stageId === stageId, "unexpected: invalid state"); + const top = this.subtreeStack.pop(); if (top == null) { - this.current == null; + this.currentSubtree == null; } else { - this.stack.push(top); - this.current = top; + this.subtreeStack.push(top); + this.currentSubtree = top; } } @@ -323,13 +400,13 @@ export class OperationPoliciesBuilder { } setReferencedTypes(stageId: StageId, ...types: TypeIdx[]): TypeIdx[] { - if (this.current == null) { + if (this.currentSubtree == null) { if (this.tg.tg.meta.namespaces!.includes(types[0])) { return types; } throw new Error("unexpected state"); } - this.current.referencedTypes.set(stageId, types); + this.currentSubtree.referencedTypes.set(stageId, types); return types; } diff --git a/src/typegate/src/runtimes/deno/deno.ts b/src/typegate/src/runtimes/deno/deno.ts index 38a9a7195..507e36d2c 100644 --- a/src/typegate/src/runtimes/deno/deno.ts +++ b/src/typegate/src/runtimes/deno/deno.ts @@ -14,12 +14,16 @@ import type { Task } from "./shared_types.ts"; import { path } from "compress/deps.ts"; import { globalConfig as config } from "../../config.ts"; import { createArtifactMeta } from "../utils/deno.ts"; +import { PolicyResolverOutput } from "../../engine/planner/policies.ts"; const predefinedFuncs: Record>> = { identity: ({ _, ...args }) => args, true: () => true, false: () => false, - internal_policy: ({ _: { context } }) => context.provider === "internal", + allow: () => "ALLOW" as PolicyResolverOutput, + deny: () => "DENY" as PolicyResolverOutput, + pass: () => "PASS" as PolicyResolverOutput, + internal_policy: ({ _: { context } }) => context.provider === "internal" ? "ALLOW" : "DENY" as PolicyResolverOutput, }; export class DenoRuntime extends Runtime { diff --git a/src/typegate/src/typegraphs/introspection.json b/src/typegate/src/typegraphs/introspection.json index eeabf338f..888e9e6a0 100644 --- a/src/typegate/src/typegraphs/introspection.json +++ b/src/typegate/src/typegraphs/introspection.json @@ -582,7 +582,7 @@ "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { @@ -635,4 +635,4 @@ "randomSeed": null, "artifacts": {} } -} +} \ No newline at end of file diff --git a/src/typegate/src/typegraphs/typegate.json b/src/typegate/src/typegraphs/typegate.json index 391ec404f..30d0ba8ca 100644 --- a/src/typegate/src/typegraphs/typegate.json +++ b/src/typegate/src/typegraphs/typegate.json @@ -785,7 +785,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": [] } }, @@ -956,4 +956,4 @@ "randomSeed": null, "artifacts": {} } -} +} \ No newline at end of file diff --git a/src/typegate/src/typegraphs/typegate.py b/src/typegate/src/typegraphs/typegate.py index 2b6552783..3bfbcfd2d 100644 --- a/src/typegate/src/typegraphs/typegate.py +++ b/src/typegate/src/typegraphs/typegate.py @@ -97,7 +97,8 @@ def typegate(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"])) diff --git a/src/typegraph/core/src/global_store.rs b/src/typegraph/core/src/global_store.rs index 42545ac44..c06046ea0 100644 --- a/src/typegraph/core/src/global_store.rs +++ b/src/typegraph/core/src/global_store.rs @@ -88,7 +88,7 @@ impl Store { effect: Effect::Read, data: MaterializerData::Deno(Rc::new(DenoMaterializer::Predefined( crate::wit::runtimes::MaterializerDenoPredefined { - name: "true".to_string(), + name: "pass".to_string(), }, ))), }], diff --git a/src/typegraph/core/src/validation/materializers.rs b/src/typegraph/core/src/validation/materializers.rs index 23c3023b4..c05459aeb 100644 --- a/src/typegraph/core/src/validation/materializers.rs +++ b/src/typegraph/core/src/validation/materializers.rs @@ -48,6 +48,18 @@ impl Materializer { }; } } + + "allow" | "deny" | "pass" => { + if let Ok(xdef) = TypeId(func.out).as_xdef() { + let TypeDef::String(_) = xdef.type_def else { + return Err(errors::invalid_output_type_predefined( + &predef.name, + "string", + &TypeId(func.out).repr()?, + )); + }; + } + } _ => { return Err(errors::unknown_predefined_function(&predef.name)); } From 68c26a9a7a072789ba99ec7be5fa99fb49025bc4 Mon Sep 17 00:00:00 2001 From: Natoandro Date: Thu, 12 Dec 2024 21:05:06 +0300 Subject: [PATCH 02/11] hoist policy info to the nearest parent struct type --- src/common/src/typegraph/types.rs | 4 +- src/metagen/src/fdk_rust/stubs.rs | 1 + src/metagen/src/fdk_rust/types.rs | 8 + src/metagen/src/tests/fixtures.rs | 2 +- src/typegate/src/engine/planner/args.ts | 42 ++- src/typegate/src/engine/planner/mod.ts | 35 +- src/typegate/src/engine/planner/policies.ts | 115 +++---- src/typegate/src/engine/typecheck/result.ts | 1 - src/typegate/src/runtimes/typegate.ts | 28 +- src/typegate/src/runtimes/typegraph.ts | 107 +++--- .../src/transports/graphql/typegraph.ts | 77 +++-- src/typegate/src/typegraph/mod.ts | 1 - src/typegate/src/typegraph/types.ts | 13 +- .../src/typegraphs/introspection.json | 179 +++++----- .../src/typegraphs/prisma_migration.json | 135 ++++---- src/typegate/src/typegraphs/typegate.json | 322 ++++++++++-------- src/typegraph/core/src/conversion/types.rs | 15 +- src/typegraph/core/src/typedef/boolean.rs | 6 +- src/typegraph/core/src/typedef/either.rs | 4 +- src/typegraph/core/src/typedef/file.rs | 6 +- src/typegraph/core/src/typedef/float.rs | 6 +- src/typegraph/core/src/typedef/func.rs | 2 - src/typegraph/core/src/typedef/integer.rs | 6 +- src/typegraph/core/src/typedef/list.rs | 4 +- src/typegraph/core/src/typedef/optional.rs | 4 +- src/typegraph/core/src/typedef/string.rs | 6 +- src/typegraph/core/src/typedef/struct_.rs | 64 +++- src/typegraph/core/src/typedef/union.rs | 4 +- src/typegraph/core/src/typegraph.rs | 10 +- 29 files changed, 666 insertions(+), 541 deletions(-) diff --git a/src/common/src/typegraph/types.rs b/src/common/src/typegraph/types.rs index 22d2b3da7..1cd144886 100644 --- a/src/common/src/typegraph/types.rs +++ b/src/common/src/typegraph/types.rs @@ -63,7 +63,6 @@ pub enum Injection { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct TypeNodeBase { pub title: String, - pub policies: Vec, #[serde(default)] pub description: Option, #[serde(default, rename = "enum")] @@ -158,6 +157,9 @@ pub struct ObjectTypeData { pub id: Vec, #[serde(default)] pub required: Vec, + #[serde(skip_serializing_if = "IndexMap::is_empty")] + #[serde(default)] + pub policies: IndexMap>, } #[skip_serializing_none] diff --git a/src/metagen/src/fdk_rust/stubs.rs b/src/metagen/src/fdk_rust/stubs.rs index ad636ef45..ed91fea2e 100644 --- a/src/metagen/src/fdk_rust/stubs.rs +++ b/src/metagen/src/fdk_rust/stubs.rs @@ -113,6 +113,7 @@ mod test { TypeNode::Object { data: ObjectTypeData { properties: Default::default(), + policies: Default::default(), id: vec![], required: vec![], }, diff --git a/src/metagen/src/fdk_rust/types.rs b/src/metagen/src/fdk_rust/types.rs index 9d7d2db91..417b6aedd 100644 --- a/src/metagen/src/fdk_rust/types.rs +++ b/src/metagen/src/fdk_rust/types.rs @@ -438,6 +438,7 @@ mod test { ] .into_iter() .collect(), + policies: Default::default(), id: vec![], // FIXME: remove required required: vec![], @@ -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(), @@ -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(), }, @@ -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(), }, @@ -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(), }, @@ -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(), }, @@ -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(), }, @@ -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(), }, diff --git a/src/metagen/src/tests/fixtures.rs b/src/metagen/src/tests/fixtures.rs index 7dcb060b2..2e70a5670 100644 --- a/src/metagen/src/tests/fixtures.rs +++ b/src/metagen/src/tests/fixtures.rs @@ -55,6 +55,7 @@ pub fn test_typegraph_2() -> Typegraph { TypeNode::Object { data: ObjectTypeData { properties: Default::default(), + policies: Default::default(), id: vec![], required: vec![], }, @@ -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, } diff --git a/src/typegate/src/engine/planner/args.ts b/src/typegate/src/engine/planner/args.ts index baa9cc1e7..f770930dc 100644 --- a/src/typegate/src/engine/planner/args.ts +++ b/src/typegate/src/engine/planner/args.ts @@ -29,6 +29,7 @@ import type { FunctionNode, Injection, InjectionNode, + PolicyIndices, } from "../../typegraph/types.ts"; import { getChildTypes, visitTypes } from "../../typegraph/visitor.ts"; @@ -82,6 +83,7 @@ interface CollectNode { path: string[]; astNode: ast.ArgumentNode | ast.ObjectFieldNode | undefined; typeIdx: number; + policies: PolicyIndices[]; } interface CollectedArgs { @@ -120,6 +122,7 @@ export function collectArgs( path: [argName], astNode: astNodes[argName], typeIdx: argTypeIdx, + policies: argTypeNode.policies?.[argName] ?? [], }); } @@ -298,7 +301,7 @@ class ArgumentCollector { const typ: TypeNode = this.tg.type(typeIdx); - this.addPoliciesFrom(typeIdx); + this.addPoliciesFrom(typeIdx, node.policies); const injection = this.#getInjection(node.path); if (injection != null) { @@ -319,7 +322,6 @@ class ArgumentCollector { // try to get a default value for it, else throw an error if (astNode == null) { if (typ.type === Type.OPTIONAL) { - this.addPoliciesFrom(typ.item); const itemType = this.tg.type(typ.item); const { default_value: defaultValue } = typ; if (defaultValue != null) { @@ -332,6 +334,7 @@ class ArgumentCollector { this.collectDefaults( itemType.properties, node.path, + itemType.policies ?? {}, ), true, ); @@ -349,6 +352,7 @@ class ArgumentCollector { this.collectDefaults( variantType.properties, node.path, + variantType.policies ?? {}, ), true, ); @@ -372,6 +376,7 @@ class ArgumentCollector { compute = this.collectDefaults( variantType.properties, node.path, + variantType.policies ?? {}, ); break; } catch (_e) { @@ -390,7 +395,7 @@ class ArgumentCollector { if (typ.type === Type.OBJECT) { const props = typ.properties; - return this.collectDefaults(props, node.path); + return this.collectDefaults(props, node.path, typ.policies ?? {}); } throw new MandatoryArgumentError(this.currentNodeDetails); @@ -689,12 +694,14 @@ class ArgumentCollector { private collectDefaults( props: Record, path: string[], + policies: Record, ): ComputeArg> { const computes: Record = {}; for (const [name, idx] of Object.entries(props)) { path.push(name); computes[name] = this.collectDefault(idx, path); + this.addPoliciesFrom(idx, policies[name] ?? []); path.pop(); } @@ -723,7 +730,6 @@ class ArgumentCollector { path: string[], ): ComputeArg { const typ = this.tg.type(typeIdx); - this.addPoliciesFrom(typeIdx); const injection = this.#getInjection(path); if (injection != null) { @@ -737,13 +743,12 @@ class ArgumentCollector { switch (typ.type) { case Type.OPTIONAL: { - this.addPoliciesFrom(typ.item); const { default_value: defaultValue = null } = typ; return () => defaultValue; } case Type.OBJECT: { - return this.collectDefaults(typ.properties, path); + return this.collectDefaults(typ.properties, path, typ.policies ?? {}); } default: @@ -756,8 +761,12 @@ class ArgumentCollector { typ: TypeNode, injection: Injection, ): ComputeArg | null { - visitTypes(this.tg.tg, getChildTypes(typ), (node) => { - this.addPoliciesFrom(node.idx); + visitTypes(this.tg.tg, getChildTypes(typ), ({ type: typeNode }) => { + if (typeNode.type === Type.OBJECT) { + for (const [name, idx] of Object.entries(typeNode.properties)) { + this.addPoliciesFrom(idx, typeNode.policies?.[name] ?? []); + } + } return true; }); @@ -840,8 +849,12 @@ class ArgumentCollector { } this.deps.parent.add(key); - visitTypes(this.tg.tg, getChildTypes(typ), (node) => { - this.addPoliciesFrom(node.idx); + visitTypes(this.tg.tg, getChildTypes(typ), ({ type: typeNode }) => { + if (typeNode.type === Type.OBJECT) { + for (const [name, idx] of Object.entries(typeNode.properties)) { + this.addPoliciesFrom(idx, typeNode.policies?.[name] ?? []); + } + } return true; }); @@ -863,11 +876,14 @@ class ArgumentCollector { }; } - private addPoliciesFrom(typeIdx: TypeIdx) { - const typ = this.tg.type(typeIdx); + private addPoliciesFrom(typeIdx: TypeIdx, policies: PolicyIndices[]) { + if (policies.length === 0) { + return; + } + // TODO we might have to check for duplicate indices this.policies.set(typeIdx, { argDetails: this.currentNodeDetails, - policyIndices: typ.policies.map((p) => { + policyIndices: policies.map((p) => { if (typeof p === "number") { return p; } diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index aadabf533..ca48fdb06 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -28,7 +28,7 @@ import { mapValues } from "@std/collections/map-values"; import { DependencyResolver } from "./dependency_resolver.ts"; import { Runtime } from "../../runtimes/Runtime.ts"; import { getInjection } from "../../typegraph/utils.ts"; -import { Injection } from "../../typegraph/types.ts"; +import { Injection, PolicyIndices } from "../../typegraph/types.ts"; import { getInjectionValues } from "./injection_utils.ts"; const logger = getLogger(import.meta); @@ -329,7 +329,10 @@ export class Planner { * @param field {FieldNode} The selection field for node * @param node */ - private traverseField(node: Node, field: ast.FieldNode): ComputeStage[] { + private traverseField( + node: Node, + field: ast.FieldNode, + ): ComputeStage[] { const { parent, path, name } = node; if (parent == null) { @@ -395,12 +398,18 @@ export class Planner { } const fieldType = this.tg.type(node.typeIdx); + // TODO CHECK does this work with aliases + // TODO array or null?? + const policies = + (this.tg.type(parent.typeIdx, Type.OBJECT).policies ?? {})[node.name] ?? + []; const stages = fieldType.type !== Type.FUNCTION - ? this.traverseValueField(node) + ? this.traverseValueField(node, policies) : this.traverseFuncField( node, this.tg.type(parent.typeIdx, Type.OBJECT).properties, + policies, ); return stages; @@ -429,9 +438,11 @@ export class Planner { * Create `ComputeStage`s for `node` and its child nodes, * where `node` corresponds to a selection field for a value (non-function type). * @param node - * @param policies */ - private traverseValueField(node: Node): ComputeStage[] { + private traverseValueField( + node: Node, + policies: PolicyIndices[], + ): ComputeStage[] { const outjection = node.scope && this.#getOutjection(node.scope!); if (outjection) { return [ @@ -461,10 +472,6 @@ export class Planner { rateCalls: true, rateWeight: 0, }); - const types = this.policiesBuilder.setReferencedTypes( - stage.id(), - node.typeIdx, - ); stages.push(stage); @@ -474,7 +481,6 @@ export class Planner { while (isQuantifier(nestedSchema)) { nestedTypeIdx = getWrappedType(nestedSchema); nestedSchema = this.tg.type(nestedTypeIdx); - types.push(nestedTypeIdx); } stages.push(...this.traverse({ ...node, typeIdx: nestedTypeIdx }, stage)); @@ -489,6 +495,7 @@ export class Planner { private traverseFuncField( node: Node, parentProps: Record, + policies: PolicyIndices[], ): ComputeStage[] { const stages: ComputeStage[] = []; const deps = []; @@ -560,12 +567,11 @@ export class Planner { }); stages.push(stage); - this.policiesBuilder.push(stage.id(), node.typeIdx, collected.policies); - const types = this.policiesBuilder.setReferencedTypes( + this.policiesBuilder.push( stage.id(), node.typeIdx, - outputIdx, - inputIdx, + collected.policies, + policies, ); // nested quantifiers @@ -574,7 +580,6 @@ export class Planner { while (isQuantifier(wrappedType)) { wrappedTypeIdx = getWrappedType(wrappedType); wrappedType = this.tg.type(wrappedTypeIdx); - types.push(wrappedTypeIdx); } stages.push( diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index d82cbcd8c..6666ad340 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -44,7 +44,7 @@ export type OperationPoliciesConfig = { export class OperationPolicies { // should be private -- but would not be testable functions: Map; - private policyLists: Map; + private policyLists: Map; private resolvers: Map; constructor( @@ -56,27 +56,11 @@ export class OperationPolicies { this.policyLists = new Map(); for (const [stageId, subtree] of this.functions.entries()) { - const { funcTypeIdx, topLevel, referencedTypes } = subtree; - ensure( - referencedTypes.has(stageId) && - referencedTypes.get(stageId)!.includes(funcTypeIdx), - "unexpected", - ); - for (const types of referencedTypes.values()) { - // set policyLists - for (const typeIdx of types) { - if (this.policyLists.has(typeIdx)) { - continue; - } - const policies = this.tg.type(typeIdx).policies; - if (policies.length > 0) { - this.policyLists.set(typeIdx, policies); - } - } - } + const { funcTypeIdx, topLevel, policies } = subtree; + this.policyLists.set(stageId, policies); // top-level functions must have policies - if (topLevel && !this.policyLists.has(funcTypeIdx)) { + if (topLevel && policies.length === 0) { const details = [ `top-level function '${this.tg.type(funcTypeIdx).title}'`, `at '${stageId}'`, @@ -155,7 +139,7 @@ export class OperationPolicies { }; // TODO refactor: too much indentation - for (const [_stageId, subtree] of this.functions) { + for (const [stageId, subtree] of this.functions) { const effect = this.tg.materializer( this.tg.type(subtree.funcTypeIdx, Type.FUNCTION).materializer, ).effect.effect ?? "read"; @@ -167,58 +151,48 @@ export class OperationPolicies { authorizedTypes[effect], ); - for (const [stageId, types] of subtree.referencedTypes) { - for (const typeIdx of types) { - if (authorizedTypes[effect].has(typeIdx)) { - continue; - } - const policies = (this.policyLists.get(typeIdx) ?? []).map((p) => - typeof p === "number" ? p : p[effect] ?? null - ); - - if (policies.some((idx) => idx == null)) { - throw new BadContext( - this.getRejectionReason(stageId, typeIdx, effect, "__deny"), - ); - } + const policies = subtree.policies.map((p) => { + if (typeof p === "number") { + return p; + } + return p[effect] ?? null; + }); - const res = await this.checkTypePolicies( - policies as number[], - effect, - getResolverResult, - ); + if (policies.some((idx) => idx == null)) { + throw new BadContext( + this.getRejectionReason(stageId, effect, "__deny"), + ); + } - if (res.authorized) { - continue; - } + const res = await this.checkTypePolicies( + policies as number[], + effect, + getResolverResult, + ); - if (res.policyIdx == null) { - const typ = this.tg.type(typeIdx); - throw new Error( - `No policy took decision on type '${typ.title}' ('${typ.type}') at '.${stageId}'`, - ); - } + if (res.authorized) { + continue; + } - const policyName = this.tg.policy(res.policyIdx).name; - throw new BadContext( - this.getRejectionReason(stageId, typeIdx, effect, policyName), - ); - } + if (res.policyIdx == null) { + throw new Error(`No policy took decision at '.${stageId}'`); } + + const policyName = this.tg.policy(res.policyIdx).name; + throw new BadContext( + this.getRejectionReason(stageId, effect, policyName), + ); } } getRejectionReason( stageId: StageId, - typeIdx: TypeIdx, effect: EffectType, policyName: string, ): string { - const typ = this.tg.type(typeIdx); const details = [ `policy '${policyName}'`, `with effect '${effect}'`, - `on type '${typ.title}' ('${typ.type}')`, `at '.${stageId}'`, ].join(" "); return `Authorization failed for ${details}`; @@ -276,9 +250,10 @@ export class OperationPolicies { interface SubtreeData { stageId: StageId; funcTypeIdx: TypeIdx; + // TODO inputPolicies: Map argPolicies: ArgPolicies; + policies: PolicyIndices[]; topLevel: boolean; - referencedTypes: Map; } export class OperationPoliciesBuilder { @@ -293,13 +268,18 @@ export class OperationPoliciesBuilder { ) {} // set current function stage - push(stageId: StageId, funcTypeIdx: TypeIdx, argPolicies: ArgPolicies) { + push( + stageId: StageId, + funcTypeIdx: TypeIdx, + argPolicies: ArgPolicies, + policies: PolicyIndices[], + ) { const subtreeData = { stageId, funcTypeIdx, argPolicies, + policies, topLevel: this.stack.length === 0, - referencedTypes: new Map(), }; this.current = subtreeData; this.stack.push(subtreeData); @@ -318,21 +298,6 @@ export class OperationPoliciesBuilder { } } - #isNamespace(typeIdx: TypeIdx): boolean { - return this.tg.tg.meta.namespaces!.includes(typeIdx); - } - - setReferencedTypes(stageId: StageId, ...types: TypeIdx[]): TypeIdx[] { - if (this.current == null) { - if (this.tg.tg.meta.namespaces!.includes(types[0])) { - return types; - } - throw new Error("unexpected state"); - } - this.current.referencedTypes.set(stageId, types); - return types; - } - build(): OperationPolicies { return new OperationPolicies(this.tg, this, this.config); } diff --git a/src/typegate/src/engine/typecheck/result.ts b/src/typegate/src/engine/typecheck/result.ts index 3cae05862..b76f75324 100644 --- a/src/typegate/src/engine/typecheck/result.ts +++ b/src/typegate/src/engine/typecheck/result.ts @@ -108,7 +108,6 @@ export class ResultValidationCompiler { cg.generateStringValidator({ type: "string", title: "__TypeName", - policies: [], }); } else if (isScalar(typeNode)) { if (entry.selectionSet != null) { diff --git a/src/typegate/src/runtimes/typegate.ts b/src/typegate/src/runtimes/typegate.ts index bec4fea41..56084f3b1 100644 --- a/src/typegate/src/runtimes/typegate.ts +++ b/src/typegate/src/runtimes/typegate.ts @@ -563,18 +563,20 @@ function walkPath( format: format ?? null, fields: node.type == "object" ? collectObjectFields(tg, parent) : null, // TODO enum type on typegraph typegate.py - policies: node.policies.map((policy) => { - if (typeof policy === "number") { - return JSON.stringify(tg.policy(policy).name); - } - return JSON.stringify( - mapValues(policy as Record, (value: number) => { - if (value === null) { - return null; - } - return tg.policy(value).name; - }), - ); - }), + // FIXME + policies: [], + // policies: node.policies.map((policy) => { + // if (typeof policy === "number") { + // return JSON.stringify(tg.policy(policy).name); + // } + // return JSON.stringify( + // mapValues(policy as Record, (value: number) => { + // if (value === null) { + // return null; + // } + // return tg.policy(value).name; + // }), + // ); + // }), }; } diff --git a/src/typegate/src/runtimes/typegraph.ts b/src/typegate/src/runtimes/typegraph.ts index aa4d8cf83..b9b14c985 100644 --- a/src/typegate/src/runtimes/typegraph.ts +++ b/src/typegate/src/runtimes/typegraph.ts @@ -58,6 +58,12 @@ function generateCustomScalar(type: TypeNode, idx: number) { throw `"${type.title}" of type "${type.type}" is not a scalar`; } +type FieldInfo = { + name: string; + typeIdx: number; + policies: PolicyIndices[]; +}; + export class TypeGraphRuntime extends Runtime { tg: TypeGraphDS; private scalarIndex = new Map(); @@ -400,7 +406,13 @@ export class TypeGraphRuntime extends Runtime { name: () => `${type.title}Inp`, description: () => `${type.title} input type`, inputFields: () => { - return Object.entries(type.properties).map(this.formatField(true)); + return Object.entries(type.properties).map(([name, typeIdx]) => + this.formatField(true)({ + name, + typeIdx, + policies: type.policies?.[name] ?? [], + }) + ); }, interfaces: () => [], }; @@ -413,7 +425,13 @@ export class TypeGraphRuntime extends Runtime { fields: () => { let entries = Object.entries(type.properties); entries = entries.sort((a, b) => b[1] - a[1]); - return entries.map(this.formatField(false)); + return entries.map(([name, typeIdx]) => + this.formatField(false)({ + name, + typeIdx, + policies: type.policies?.[name] ?? [], + }) + ); }, interfaces: () => [], }; @@ -477,7 +495,7 @@ export class TypeGraphRuntime extends Runtime { // enum: enumValues }; - policyDescription(type: TypeNode): string { + policyDescription(policies: PolicyIndices[]): string { const describeOne = (p: number) => this.tg.policies[p].name; const describe = (p: PolicyIndices) => { if (typeof p === "number") { @@ -487,12 +505,12 @@ export class TypeGraphRuntime extends Runtime { .map(([eff, polIdx]) => `${eff}:${describeOne(polIdx)}`) .join("; "); }; - const policies = type.policies.map(describe); + const policyNames = policies.map(describe); let ret = "\n\nPolicies:\n"; - if (policies.length > 0) { - ret += policies.map((p: string) => `- ${p}`).join("\n"); + if (policyNames.length > 0) { + ret += policyNames.map((p: string) => `- ${p}`).join("\n"); } else { ret += "- inherit"; } @@ -500,51 +518,52 @@ export class TypeGraphRuntime extends Runtime { return ret; } - formatField = (asInput: boolean) => ([name, typeIdx]: [string, number]) => { - const type = this.tg.types[typeIdx]; - const common = { - // https://github.com/graphql/graphql-js/blob/main/src/type/introspection.ts#L329 - name: () => name, - description: () => `${name} field${this.policyDescription(type)}`, - isDeprecated: () => false, - deprecationReason: () => null, - }; + formatField = + (asInput: boolean) => ({ name, typeIdx, policies }: FieldInfo) => { + const type = this.tg.types[typeIdx]; + const common = { + // https://github.com/graphql/graphql-js/blob/main/src/type/introspection.ts#L329 + name: () => name, + description: () => `${name} field${this.policyDescription(policies)}`, + isDeprecated: () => false, + deprecationReason: () => null, + }; + + if (isFunction(type)) { + return { + ...common, + args: (_: DeprecatedArg = {}) => { + const inp = this.tg.types[type.input as number]; + ensure( + isObject(inp), + `${type} cannot be an input field, require struct`, + ); + let entries = Object.entries((inp as ObjectNode).properties); + entries = entries.sort((a, b) => b[1] - a[1]); + return entries + .map((entry) => + this.formatInputFields( + entry, + (type.injections ?? {})[entry[0]] ?? null, + ) + ) + .filter((f) => f !== null); + }, + type: () => { + const output = this.tg.types[type.output as number]; + return this.formatType(output, true, false); + }, + }; + } - if (isFunction(type)) { return { ...common, - args: (_: DeprecatedArg = {}) => { - const inp = this.tg.types[type.input as number]; - ensure( - isObject(inp), - `${type} cannot be an input field, require struct`, - ); - let entries = Object.entries((inp as ObjectNode).properties); - entries = entries.sort((a, b) => b[1] - a[1]); - return entries - .map((entry) => - this.formatInputFields( - entry, - (type.injections ?? {})[entry[0]] ?? null, - ) - ) - .filter((f) => f !== null); - }, + args: () => [], type: () => { - const output = this.tg.types[type.output as number]; - return this.formatType(output, true, false); + return this.formatType(type, true, asInput); }, }; - } - - return { - ...common, - args: () => [], - type: () => { - return this.formatType(type, true, asInput); - }, }; - }; } function emptyObjectScalar() { diff --git a/src/typegate/src/transports/graphql/typegraph.ts b/src/typegate/src/transports/graphql/typegraph.ts index 9c2f1ba89..b504a6cb9 100644 --- a/src/typegate/src/transports/graphql/typegraph.ts +++ b/src/typegate/src/transports/graphql/typegraph.ts @@ -3,19 +3,33 @@ import type { TypeGraphDS } from "../../typegraph/mod.ts"; import type { ObjectNode } from "../../typegraph/type_node.ts"; +import { PolicyIndices } from "../../typegraph/types.ts"; import { addNode } from "./utils.ts"; type PropertiesTable = Record; +type SplitResult = { + queries: { + properties: PropertiesTable; + policies: Record; + }; + mutations: { + properties: PropertiesTable; + policies: Record; + }; +}; + /** * Splits a TypeGraph into GraphQL `queries` and `mutations` */ function splitGraphQLOperations( typegraph: TypeGraphDS, node: ObjectNode, -): [PropertiesTable, PropertiesTable] { - const queryProperties: PropertiesTable = {}; - const mutationProperties: PropertiesTable = {}; +): SplitResult { + const res: SplitResult = { + queries: { properties: {}, policies: {} }, + mutations: { properties: {}, policies: {} }, + }; if (typegraph.meta.namespaces == null) { typegraph.meta.namespaces = []; @@ -25,37 +39,38 @@ function splitGraphQLOperations( for (const [propertyName, typeIndex] of Object.entries(node.properties)) { const childNode = typegraph.types[typeIndex]; - // if the leaf node of a path its a function - // with a materializer that has an effect, + // if the leaf node of a path is a function + // with a materializer that has an effect other than `read`, // classify the root node of this path as a `mutation` // otherwise as a `query` switch (childNode.type) { case "object": { - const [childQueryProperties, childMutationProperties] = - splitGraphQLOperations( - typegraph, - childNode, - ); - - if (Object.keys(childQueryProperties).length === 0) { - mutationProperties[propertyName] = typeIndex; + const child = splitGraphQLOperations( + typegraph, + childNode, + ); + + if (Object.keys(child.queries.properties).length === 0) { + res.mutations.properties[propertyName] = typeIndex; namespaces.push(typeIndex); - } else if (Object.keys(childMutationProperties).length === 0) { - queryProperties[propertyName] = typeIndex; + } else if (Object.keys(child.mutations.properties).length === 0) { + res.queries.properties[propertyName] = typeIndex; namespaces.push(typeIndex); } else { - queryProperties[propertyName] = addNode(typegraph, { + res.queries.properties[propertyName] = addNode(typegraph, { ...node, title: `${node.title}_q`, - properties: childQueryProperties, + properties: child.queries.properties, + policies: child.queries.policies, }); - namespaces.push(queryProperties[propertyName]); - mutationProperties[propertyName] = addNode(typegraph, { + namespaces.push(res.queries.properties[propertyName]); + res.mutations.properties[propertyName] = addNode(typegraph, { ...node, title: `${node.title}_m`, - properties: childMutationProperties, + properties: child.mutations.properties, + policies: child.mutations.policies, }); - namespaces.push(mutationProperties[propertyName]); + namespaces.push(res.mutations.properties[propertyName]); } break; } @@ -69,10 +84,16 @@ function splitGraphQLOperations( childMaterializer.effect.effect === null || childMaterializer.effect.effect === "read" ) { - queryProperties[propertyName] = typeIndex; + res.queries.properties[propertyName] = typeIndex; + if (propertyName in (node.policies ?? {})) { + res.queries.policies[propertyName] = node.policies![propertyName]; + } // TODO additional checks } else { - mutationProperties[propertyName] = typeIndex; + res.mutations.properties[propertyName] = typeIndex; + if (propertyName in (node.policies ?? {})) { + res.mutations.policies[propertyName] = node.policies![propertyName]; + } // TODO additional checks } @@ -81,7 +102,7 @@ function splitGraphQLOperations( } } - return [queryProperties, mutationProperties]; + return res; } export function parseGraphQLTypeGraph(tgOrig: TypeGraphDS): TypeGraphDS { @@ -94,7 +115,7 @@ export function parseGraphQLTypeGraph(tgOrig: TypeGraphDS): TypeGraphDS { types: [...tgOrig.types], }; - const [queryProperties, mutationProperties] = splitGraphQLOperations( + const { queries, mutations } = splitGraphQLOperations( typegraph, rootNode, ); @@ -106,16 +127,16 @@ export function parseGraphQLTypeGraph(tgOrig: TypeGraphDS): TypeGraphDS { const queryIndex = addNode(typegraph, { ...rootNode, title: "Query", - properties: queryProperties, + ...queries, }); typegraph.meta.namespaces!.push(queryIndex); rootNode.properties.query = queryIndex; - if (Object.keys(mutationProperties).length > 0) { + if (Object.keys(mutations.properties).length > 0) { const mutationIndex = addNode(typegraph, { ...rootNode, title: "Mutation", - properties: mutationProperties, + ...mutations, }); typegraph.meta.namespaces!.push(mutationIndex); rootNode.properties.mutation = mutationIndex; diff --git a/src/typegate/src/typegraph/mod.ts b/src/typegate/src/typegraph/mod.ts index 1a0156dcc..dc093036a 100644 --- a/src/typegate/src/typegraph/mod.ts +++ b/src/typegate/src/typegraph/mod.ts @@ -97,7 +97,6 @@ export class TypeGraph implements AsyncDisposable { static typenameType: TypeNode = { title: "string", type: "string", - policies: [], }; root: TypeNode; diff --git a/src/typegate/src/typegraph/types.ts b/src/typegate/src/typegraph/types.ts index 84cd5570a..ce2d855bc 100644 --- a/src/typegate/src/typegraph/types.ts +++ b/src/typegate/src/typegraph/types.ts @@ -6,7 +6,6 @@ export type OptionalNode = { type: "optional"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; item: number; @@ -15,14 +14,12 @@ export type OptionalNode = { export type BooleanNode = { type: "boolean"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; }; export type FloatNode = { type: "float"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; minimum?: number | null; @@ -34,7 +31,6 @@ export type FloatNode = { export type IntegerNode = { type: "integer"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; minimum?: number | null; @@ -46,7 +42,6 @@ export type IntegerNode = { export type StringNode = { type: "string"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; minLength?: number | null; @@ -57,7 +52,6 @@ export type StringNode = { export type FileNode = { type: "file"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; minSize?: number | null; @@ -67,7 +61,7 @@ export type FileNode = { export type ObjectNode = { type: "object"; title: string; - policies: PolicyIndices[]; + policies?: Record; description?: string | null; enum?: string[] | null; properties: { @@ -79,7 +73,6 @@ export type ObjectNode = { export type ListNode = { type: "list"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; items: number; @@ -93,7 +86,6 @@ export type InjectionNode = export type FunctionNode = { type: "function"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; input: number; @@ -109,7 +101,6 @@ export type FunctionNode = { export type UnionNode = { type: "union"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; anyOf: number[]; @@ -117,7 +108,6 @@ export type UnionNode = { export type EitherNode = { type: "either"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; oneOf: number[]; @@ -125,7 +115,6 @@ export type EitherNode = { export type AnyNode = { type: "any"; title: string; - policies: PolicyIndices[]; description?: string | null; enum?: string[] | null; }; diff --git a/src/typegate/src/typegraphs/introspection.json b/src/typegate/src/typegraphs/introspection.json index eeabf338f..fb6a620b9 100644 --- a/src/typegate/src/typegraphs/introspection.json +++ b/src/typegate/src/typegraphs/introspection.json @@ -3,7 +3,6 @@ { "type": "object", "title": "introspection", - "policies": [], "properties": { "__type": 1, "__schema": 45 @@ -12,17 +11,21 @@ "required": [ "__type", "__schema" - ] + ], + "policies": { + "__type": [ + 0 + ], + "__schema": [ + 0 + ] + } }, { "type": "function", "title": "root___type_fn", - "policies": [ - 0 - ], "input": 2, "output": 4, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -31,29 +34,28 @@ { "type": "object", "title": "root___type_fn_input", - "policies": [], "properties": { "name": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [] + } }, { "type": "string", - "title": "root___type_fn_input_name_string", - "policies": [] + "title": "root___type_fn_input_name_string" }, { "type": "optional", "title": "root___type_fn_output", - "policies": [], "item": 5, "default_value": null }, { "type": "object", "title": "type", - "policies": [], "properties": { "kind": 6, "name": 7, @@ -67,12 +69,23 @@ "ofType": 44 }, "id": [], - "required": [] + "required": [], + "policies": { + "kind": [], + "name": [], + "description": [], + "specifiedByURL": [], + "fields": [], + "interfaces": [], + "possibleTypes": [], + "enumValues": [], + "inputFields": [], + "ofType": [] + } }, { "type": "string", "title": "type_kind", - "policies": [], "enum": [ "\"SCALAR\"", "\"OBJECT\"", @@ -87,31 +100,26 @@ { "type": "optional", "title": "type_name_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "type_description_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "type_specifiedByURL_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "type_fields_fn", - "policies": [], "input": 11, "output": 14, - "injections": {}, "runtimeConfig": null, "materializer": 1, "rate_weight": null, @@ -120,42 +128,39 @@ { "type": "object", "title": "type_fields_fn_input", - "policies": [], "properties": { "includeDeprecated": 12 }, "id": [], - "required": [] + "required": [], + "policies": { + "includeDeprecated": [] + } }, { "type": "optional", "title": "type_fields_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "policies": [], "item": 13, "default_value": null }, { "type": "boolean", - "title": "type_fields_fn_input_includeDeprecated_boolean", - "policies": [] + "title": "type_fields_fn_input_includeDeprecated_boolean" }, { "type": "optional", "title": "type_fields_fn_output", - "policies": [], "item": 15, "default_value": null }, { "type": "list", "title": "type_fields_fn_output", - "policies": [], "items": 16 }, { "type": "object", "title": "field", - "policies": [], "properties": { "name": 3, "description": 17, @@ -165,22 +170,27 @@ "deprecationReason": 26 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "description": [], + "args": [], + "type": [], + "isDeprecated": [], + "deprecationReason": [] + } }, { "type": "optional", "title": "field_description_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "field_args_fn", - "policies": [], "input": 19, "output": 21, - "injections": {}, "runtimeConfig": null, "materializer": 1, "rate_weight": null, @@ -189,30 +199,29 @@ { "type": "object", "title": "field_args_fn_input", - "policies": [], "properties": { "includeDeprecated": 20 }, "id": [], - "required": [] + "required": [], + "policies": { + "includeDeprecated": [] + } }, { "type": "optional", "title": "field_args_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "policies": [], "item": 13, "default_value": null }, { "type": "list", "title": "field_args_fn_output", - "policies": [], "items": 22 }, { "type": "object", "title": "input_value", - "policies": [], "properties": { "name": 3, "description": 23, @@ -222,69 +231,67 @@ "deprecationReason": 25 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "description": [], + "type": [], + "defaultValue": [], + "isDeprecated": [], + "deprecationReason": [] + } }, { "type": "optional", "title": "input_value_description_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "input_value_defaultValue_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "input_value_deprecationReason_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "field_deprecationReason_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "type_interfaces_type_interfaces_type_list_optional", - "policies": [], "item": 28, "default_value": null }, { "type": "list", "title": "type_interfaces_type_list", - "policies": [], "items": 5 }, { "type": "optional", "title": "type_possibleTypes_type_possibleTypes_type_list_optional", - "policies": [], "item": 30, "default_value": null }, { "type": "list", "title": "type_possibleTypes_type_list", - "policies": [], "items": 5 }, { "type": "function", "title": "type_enumValues_fn", - "policies": [], "input": 32, "output": 34, - "injections": {}, "runtimeConfig": null, "materializer": 1, "rate_weight": null, @@ -293,37 +300,35 @@ { "type": "object", "title": "type_enumValues_fn_input", - "policies": [], "properties": { "includeDeprecated": 33 }, "id": [], - "required": [] + "required": [], + "policies": { + "includeDeprecated": [] + } }, { "type": "optional", "title": "type_enumValues_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "policies": [], "item": 13, "default_value": null }, { "type": "optional", "title": "type_enumValues_fn_output", - "policies": [], "item": 35, "default_value": null }, { "type": "list", "title": "type_enumValues_fn_output", - "policies": [], "items": 36 }, { "type": "object", "title": "enum_value", - "policies": [], "properties": { "name": 3, "description": 37, @@ -331,29 +336,31 @@ "deprecationReason": 38 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "description": [], + "isDeprecated": [], + "deprecationReason": [] + } }, { "type": "optional", "title": "enum_value_description_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "enum_value_deprecationReason_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "type_inputFields_fn", - "policies": [], "input": 40, "output": 42, - "injections": {}, "runtimeConfig": null, "materializer": 1, "rate_weight": null, @@ -362,49 +369,43 @@ { "type": "object", "title": "type_inputFields_fn_input", - "policies": [], "properties": { "includeDeprecated": 41 }, "id": [], - "required": [] + "required": [], + "policies": { + "includeDeprecated": [] + } }, { "type": "optional", "title": "type_inputFields_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "policies": [], "item": 13, "default_value": null }, { "type": "optional", "title": "type_inputFields_fn_output", - "policies": [], "item": 43, "default_value": null }, { "type": "list", "title": "type_inputFields_fn_output", - "policies": [], "items": 22 }, { "type": "optional", "title": "type_ofType_type_optional", - "policies": [], "item": 5, "default_value": null }, { "type": "function", "title": "root___schema_fn", - "policies": [ - 0 - ], "input": 46, "output": 47, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -413,7 +414,6 @@ { "type": "object", "title": "root___schema_fn_input", - "policies": [], "properties": {}, "id": [], "required": [] @@ -421,7 +421,6 @@ { "type": "object", "title": "schema", - "policies": [], "properties": { "description": 48, "types": 49, @@ -431,45 +430,47 @@ "directives": 52 }, "id": [], - "required": [] + "required": [], + "policies": { + "description": [], + "types": [], + "queryType": [], + "mutationType": [], + "subscriptionType": [], + "directives": [] + } }, { "type": "optional", "title": "schema_description_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "list", "title": "schema_types_type_list", - "policies": [], "items": 5 }, { "type": "optional", "title": "schema_mutationType_type_optional", - "policies": [], "item": 5, "default_value": null }, { "type": "optional", "title": "schema_subscriptionType_type_optional", - "policies": [], "item": 5, "default_value": null }, { "type": "list", "title": "schema_directives_directive_list", - "policies": [], "items": 53 }, { "type": "object", "title": "directive", - "policies": [], "properties": { "name": 3, "description": 54, @@ -478,25 +479,29 @@ "args": 57 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "description": [], + "isRepeatable": [], + "locations": [], + "args": [] + } }, { "type": "optional", "title": "directive_description_root___type_fn_input_name_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "list", "title": "directive_locations_directive_location_list", - "policies": [], "items": 56 }, { "type": "string", "title": "directive_location", - "policies": [], "enum": [ "\"QUERY\"", "\"MUTATION\"", @@ -522,10 +527,8 @@ { "type": "function", "title": "directive_args_fn", - "policies": [], "input": 58, "output": 60, - "injections": {}, "runtimeConfig": null, "materializer": 1, "rate_weight": null, @@ -534,24 +537,24 @@ { "type": "object", "title": "directive_args_fn_input", - "policies": [], "properties": { "includeDeprecated": 59 }, "id": [], - "required": [] + "required": [], + "policies": { + "includeDeprecated": [] + } }, { "type": "optional", "title": "directive_args_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "policies": [], "item": 13, "default_value": null }, { "type": "list", "title": "directive_args_fn_output", - "policies": [], "items": 22 } ], @@ -635,4 +638,4 @@ "randomSeed": null, "artifacts": {} } -} +} \ No newline at end of file diff --git a/src/typegate/src/typegraphs/prisma_migration.json b/src/typegate/src/typegraphs/prisma_migration.json index 6d5d98040..d93c6d7f9 100644 --- a/src/typegate/src/typegraphs/prisma_migration.json +++ b/src/typegate/src/typegraphs/prisma_migration.json @@ -3,7 +3,6 @@ { "type": "object", "title": "typegate/prisma_migration", - "policies": [], "properties": { "diff": 1, "apply": 8, @@ -18,17 +17,30 @@ "create", "deploy", "reset" - ] + ], + "policies": { + "diff": [ + 0 + ], + "apply": [ + 0 + ], + "create": [ + 0 + ], + "deploy": [ + 0 + ], + "reset": [ + 0 + ] + } }, { "type": "function", "title": "root_diff_fn", - "policies": [ - 0 - ], "input": 2, "output": 6, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -37,59 +49,58 @@ { "type": "object", "title": "root_diff_fn_input", - "policies": [], "properties": { "typegraph": 3, "runtime": 4, "script": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [], + "script": [] + } }, { "type": "string", - "title": "root_diff_fn_input_typegraph_string", - "policies": [] + "title": "root_diff_fn_input_typegraph_string" }, { "type": "optional", "title": "root_diff_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "boolean", - "title": "root_diff_fn_input_script_boolean", - "policies": [] + "title": "root_diff_fn_input_script_boolean" }, { "type": "object", "title": "root_diff_fn_output", - "policies": [], "properties": { "diff": 7, "runtimeName": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "diff": [], + "runtimeName": [] + } }, { "type": "optional", "title": "root_diff_fn_output_diff_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "root_apply_fn", - "policies": [ - 0 - ], "input": 9, "output": 12, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -98,7 +109,6 @@ { "type": "object", "title": "root_apply_fn_input", - "policies": [], "properties": { "typegraph": 3, "runtime": 10, @@ -106,48 +116,50 @@ "resetDatabase": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [], + "migrations": [], + "resetDatabase": [] + } }, { "type": "optional", "title": "root_apply_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "root_apply_fn_input_migrations_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "object", "title": "root_apply_fn_output", - "policies": [], "properties": { "databaseReset": 5, "appliedMigrations": 13 }, "id": [], - "required": [] + "required": [], + "policies": { + "databaseReset": [], + "appliedMigrations": [] + } }, { "type": "list", "title": "root_apply_fn_output_appliedMigrations_root_diff_fn_input_typegraph_string_list", - "policies": [], "items": 3 }, { "type": "function", "title": "root_create_fn", - "policies": [ - 0 - ], "input": 15, "output": 18, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -156,7 +168,6 @@ { "type": "object", "title": "root_create_fn_input", - "policies": [], "properties": { "typegraph": 3, "runtime": 16, @@ -165,26 +176,30 @@ "migrations": 17 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [], + "name": [], + "apply": [], + "migrations": [] + } }, { "type": "optional", "title": "root_create_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "root_create_fn_input_migrations_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "object", "title": "root_create_fn_output", - "policies": [], "properties": { "createdMigrationName": 3, "applyError": 19, @@ -192,31 +207,31 @@ "runtimeName": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "createdMigrationName": [], + "applyError": [], + "migrations": [], + "runtimeName": [] + } }, { "type": "optional", "title": "root_create_fn_output_applyError_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "optional", "title": "root_create_fn_output_migrations_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "root_deploy_fn", - "policies": [ - 0 - ], "input": 22, "output": 24, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -225,53 +240,53 @@ { "type": "object", "title": "root_deploy_fn_input", - "policies": [], "properties": { "typegraph": 3, "runtime": 23, "migrations": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [], + "migrations": [] + } }, { "type": "optional", "title": "root_deploy_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "object", "title": "root_deploy_fn_output", - "policies": [], "properties": { "migrationCount": 25, "appliedMigrations": 26 }, "id": [], - "required": [] + "required": [], + "policies": { + "migrationCount": [], + "appliedMigrations": [] + } }, { "type": "integer", - "title": "root_deploy_fn_output_migrationCount_integer", - "policies": [] + "title": "root_deploy_fn_output_migrationCount_integer" }, { "type": "list", "title": "root_deploy_fn_output_appliedMigrations_root_diff_fn_input_typegraph_string_list", - "policies": [], "items": 3 }, { "type": "function", "title": "root_reset_fn", - "policies": [ - 0 - ], "input": 28, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 5, "rate_weight": null, @@ -280,18 +295,20 @@ { "type": "object", "title": "root_reset_fn_input", - "policies": [], "properties": { "typegraph": 3, "runtime": 29 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [] + } }, { "type": "optional", "title": "root_reset_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "policies": [], "item": 3, "default_value": null } @@ -411,4 +428,4 @@ "randomSeed": null, "artifacts": {} } -} +} \ No newline at end of file diff --git a/src/typegate/src/typegraphs/typegate.json b/src/typegate/src/typegraphs/typegate.json index 391ec404f..7a4f2d323 100644 --- a/src/typegate/src/typegraphs/typegate.json +++ b/src/typegate/src/typegraphs/typegate.json @@ -3,7 +3,6 @@ { "type": "object", "title": "typegate", - "policies": [], "properties": { "typegraphs": 1, "typegraph": 7, @@ -32,17 +31,51 @@ "execRawPrismaUpdate", "execRawPrismaDelete", "queryPrismaModel" - ] + ], + "policies": { + "typegraphs": [ + 0 + ], + "typegraph": [ + 0 + ], + "addTypegraph": [ + 0 + ], + "removeTypegraphs": [ + 0 + ], + "argInfoByPath": [ + 0 + ], + "findAvailableOperations": [ + 0 + ], + "findPrismaModels": [ + 0 + ], + "execRawPrismaRead": [ + 0 + ], + "execRawPrismaCreate": [ + 0 + ], + "execRawPrismaUpdate": [ + 0 + ], + "execRawPrismaDelete": [ + 0 + ], + "queryPrismaModel": [ + 0 + ] + } }, { "type": "function", "title": "root_typegraphs_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, - "injections": {}, "runtimeConfig": null, "materializer": 0, "rate_weight": null, @@ -51,7 +84,6 @@ { "type": "object", "title": "root_typegraphs_fn_input", - "policies": [], "properties": {}, "id": [], "required": [] @@ -59,40 +91,36 @@ { "type": "list", "title": "root_typegraphs_fn_output", - "policies": [], "items": 4 }, { "type": "object", "title": "Typegraph", - "policies": [], "properties": { "name": 5, "url": 6 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "url": [] + } }, { "type": "string", - "title": "Typegraph_name_string", - "policies": [] + "title": "Typegraph_name_string" }, { "type": "string", "title": "Typegraph_url_string_uri", - "policies": [], "format": "uri" }, { "type": "function", "title": "root_typegraph_fn", - "policies": [ - 0 - ], "input": 8, "output": 9, - "injections": {}, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -101,39 +129,42 @@ { "type": "object", "title": "root_typegraph_fn_input", - "policies": [], "properties": { "name": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [] + } }, { "type": "optional", "title": "root_typegraph_fn_output", - "policies": [], "item": 10, "default_value": null }, { "type": "object", "title": "root_typegraph_fn_output", - "policies": [], "properties": { "name": 5, "url": 6, "serialized": 11 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "url": [], + "serialized": [] + } }, { "type": "function", "title": "root_typegraph_fn_output_serialized_fn", - "policies": [], "input": 2, "output": 5, - "injections": {}, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -142,12 +173,8 @@ { "type": "function", "title": "root_addTypegraph_fn", - "policies": [ - 0 - ], "input": 13, "output": 15, - "injections": {}, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -156,25 +183,27 @@ { "type": "object", "title": "root_addTypegraph_fn_input", - "policies": [], "properties": { "fromString": 14, "secrets": 14, "targetVersion": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "fromString": [], + "secrets": [], + "targetVersion": [] + } }, { "type": "string", "title": "root_addTypegraph_fn_input_fromString_string_json", - "policies": [], "format": "json" }, { "type": "object", "title": "root_addTypegraph_fn_output", - "policies": [], "properties": { "name": 5, "messages": 16, @@ -182,29 +211,36 @@ "failure": 21 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "messages": [], + "migrations": [], + "failure": [] + } }, { "type": "list", "title": "root_addTypegraph_fn_output_messages_root_addTypegraph_fn_output_messages_struct_list", - "policies": [], "items": 17 }, { "type": "object", "title": "root_addTypegraph_fn_output_messages_struct", - "policies": [], "properties": { "type": 18, "text": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "type": [], + "text": [] + } }, { "type": "string", "title": "root_addTypegraph_fn_output_messages_struct_type_string_enum", - "policies": [], "enum": [ "\"info\"", "\"warning\"", @@ -214,36 +250,33 @@ { "type": "list", "title": "root_addTypegraph_fn_output_migrations_root_addTypegraph_fn_output_migrations_struct_list", - "policies": [], "items": 20 }, { "type": "object", "title": "root_addTypegraph_fn_output_migrations_struct", - "policies": [], "properties": { "runtime": 5, "migrations": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "runtime": [], + "migrations": [] + } }, { "type": "optional", "title": "root_addTypegraph_fn_output_failure_root_addTypegraph_fn_input_fromString_string_json_optional", - "policies": [], "item": 14, "default_value": null }, { "type": "function", "title": "root_removeTypegraphs_fn", - "policies": [ - 0 - ], "input": 23, "output": 25, - "injections": {}, "runtimeConfig": null, "materializer": 5, "rate_weight": null, @@ -252,33 +285,29 @@ { "type": "object", "title": "root_removeTypegraphs_fn_input", - "policies": [], "properties": { "names": 24 }, "id": [], - "required": [] + "required": [], + "policies": { + "names": [] + } }, { "type": "list", "title": "root_removeTypegraphs_fn_input_names_Typegraph_name_string_list", - "policies": [], "items": 5 }, { "type": "boolean", - "title": "root_removeTypegraphs_fn_output", - "policies": [] + "title": "root_removeTypegraphs_fn_output" }, { "type": "function", "title": "root_argInfoByPath_fn", - "policies": [ - 0 - ], "input": 27, "output": 30, - "injections": {}, "runtimeConfig": null, "materializer": 6, "rate_weight": null, @@ -287,7 +316,6 @@ { "type": "object", "title": "root_argInfoByPath_fn_input", - "policies": [], "properties": { "typegraph": 5, "queryType": 5, @@ -295,30 +323,32 @@ "argPaths": 28 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "queryType": [], + "fn": [], + "argPaths": [] + } }, { "type": "list", "title": "root_argInfoByPath_fn_input_argPaths_root_argInfoByPath_fn_input_argPaths_Typegraph_name_string_list_list", - "policies": [], "items": 29 }, { "type": "list", "title": "root_argInfoByPath_fn_input_argPaths_Typegraph_name_string_list", - "policies": [], "items": 5 }, { "type": "list", "title": "root_argInfoByPath_fn_output", - "policies": [], "items": 31 }, { "type": "object", "title": "TypeInfo", - "policies": [], "properties": { "optional": 25, "title": 5, @@ -330,74 +360,76 @@ "fields": 37 }, "id": [], - "required": [] + "required": [], + "policies": { + "optional": [], + "title": [], + "type": [], + "enum": [], + "default": [], + "format": [], + "policies": [], + "fields": [] + } }, { "type": "optional", "title": "TypeInfo_enum_TypeInfo_enum_root_addTypegraph_fn_input_fromString_string_json_list_optional", - "policies": [], "item": 33, "default_value": null }, { "type": "list", "title": "TypeInfo_enum_root_addTypegraph_fn_input_fromString_string_json_list", - "policies": [], "items": 14 }, { "type": "optional", "title": "TypeInfo_default_root_addTypegraph_fn_input_fromString_string_json_optional", - "policies": [], "item": 14, "default_value": null }, { "type": "optional", "title": "TypeInfo_format_Typegraph_name_string_optional", - "policies": [], "item": 5, "default_value": null }, { "type": "list", "title": "TypeInfo_policies_Typegraph_name_string_list", - "policies": [], "items": 5 }, { "type": "optional", "title": "TypeInfo_fields_TypeInfo_fields_TypeInfo_fields_struct_list_optional", - "policies": [], "item": 38, "default_value": null }, { "type": "list", "title": "TypeInfo_fields_TypeInfo_fields_struct_list", - "policies": [], "items": 39 }, { "type": "object", "title": "TypeInfo_fields_struct", - "policies": [], "properties": { "subPath": 29, "termNode": 31 }, "id": [], - "required": [] + "required": [], + "policies": { + "subPath": [], + "termNode": [] + } }, { "type": "function", "title": "root_findAvailableOperations_fn", - "policies": [ - 0 - ], "input": 41, "output": 42, - "injections": {}, "runtimeConfig": null, "materializer": 7, "rate_weight": null, @@ -406,23 +438,23 @@ { "type": "object", "title": "root_findAvailableOperations_fn_input", - "policies": [], "properties": { "typegraph": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [] + } }, { "type": "list", "title": "root_findAvailableOperations_fn_output", - "policies": [], "items": 43 }, { "type": "object", "title": "OperationInfo", - "policies": [], "properties": { "name": 5, "type": 44, @@ -431,12 +463,18 @@ "outputItem": 47 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "type": [], + "inputs": [], + "output": [], + "outputItem": [] + } }, { "type": "string", "title": "OperationInfo_type_string_enum", - "policies": [], "enum": [ "\"query\"", "\"mutation\"" @@ -445,36 +483,33 @@ { "type": "list", "title": "OperationInfo_inputs_OperationInfo_inputs_struct_list", - "policies": [], "items": 46 }, { "type": "object", "title": "OperationInfo_inputs_struct", - "policies": [], "properties": { "name": 5, "type": 31 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "type": [] + } }, { "type": "optional", "title": "OperationInfo_outputItem_TypeInfo_optional", - "policies": [], "item": 31, "default_value": null }, { "type": "function", "title": "root_findPrismaModels_fn", - "policies": [ - 0 - ], "input": 41, "output": 49, - "injections": {}, "runtimeConfig": null, "materializer": 8, "rate_weight": null, @@ -483,43 +518,48 @@ { "type": "list", "title": "root_findPrismaModels_fn_output", - "policies": [], "items": 50 }, { "type": "object", "title": "PrismaModelInfo", - "policies": [], "properties": { "name": 5, "runtime": 5, "fields": 51 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "runtime": [], + "fields": [] + } }, { "type": "list", "title": "PrismaModelInfo_fields_PrismaModelInfo_fields_struct_list", - "policies": [], "items": 52 }, { "type": "object", "title": "PrismaModelInfo_fields_struct", - "policies": [], "properties": { "name": 5, "as_id": 25, "type": 53 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "as_id": [], + "type": [] + } }, { "type": "object", "title": "ShallowTypeInfo", - "policies": [], "properties": { "optional": 25, "title": 5, @@ -530,17 +570,22 @@ "policies": 36 }, "id": [], - "required": [] + "required": [], + "policies": { + "optional": [], + "title": [], + "type": [], + "enum": [], + "default": [], + "format": [], + "policies": [] + } }, { "type": "function", "title": "root_execRawPrismaRead_fn", - "policies": [ - 0 - ], "input": 55, "output": 14, - "injections": {}, "runtimeConfig": null, "materializer": 9, "rate_weight": null, @@ -549,19 +594,22 @@ { "type": "object", "title": "root_execRawPrismaRead_fn_input", - "policies": [], "properties": { "typegraph": 5, "runtime": 5, "query": 56 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [], + "query": [] + } }, { "type": "either", "title": "PrismaQuery", - "policies": [], "oneOf": [ 57, 60 @@ -570,26 +618,28 @@ { "type": "object", "title": "PrismaSingleQuery", - "policies": [], "properties": { "modelName": 58, "action": 59, "query": 14 }, "id": [], - "required": [] + "required": [], + "policies": { + "modelName": [], + "action": [], + "query": [] + } }, { "type": "optional", "title": "PrismaSingleQuery_modelName_Typegraph_name_string_optional", - "policies": [], "item": 5, "default_value": null }, { "type": "string", "title": "PrismaQueryTag", - "policies": [], "enum": [ "\"findUnique\"", "\"findFirst\"", @@ -614,48 +664,49 @@ { "type": "object", "title": "PrismaBatchQuery", - "policies": [], "properties": { "batch": 61, "transaction": 62 }, "id": [], - "required": [] + "required": [], + "policies": { + "batch": [], + "transaction": [] + } }, { "type": "list", "title": "PrismaBatchQuery_batch_PrismaSingleQuery_list", - "policies": [], "items": 57 }, { "type": "optional", "title": "PrismaBatchQuery_transaction_PrismaBatchQuery_transaction_struct_optional", - "policies": [], "item": 63, "default_value": null }, { "type": "object", "title": "PrismaBatchQuery_transaction_struct", - "policies": [], "properties": { "isolationLevel": 64 }, "id": [], - "required": [] + "required": [], + "policies": { + "isolationLevel": [] + } }, { "type": "optional", "title": "PrismaBatchQuery_transaction_struct_isolationLevel_PrismaBatchQuery_transaction_struct_isolationLevel_string_enum_optional", - "policies": [], "item": 65, "default_value": null }, { "type": "string", "title": "PrismaBatchQuery_transaction_struct_isolationLevel_string_enum", - "policies": [], "enum": [ "\"read uncommitted\"", "\"readuncommitted\"", @@ -670,12 +721,8 @@ { "type": "function", "title": "root_execRawPrismaCreate_fn", - "policies": [ - 0 - ], "input": 55, "output": 14, - "injections": {}, "runtimeConfig": null, "materializer": 10, "rate_weight": null, @@ -684,12 +731,8 @@ { "type": "function", "title": "root_execRawPrismaUpdate_fn", - "policies": [ - 0 - ], "input": 55, "output": 14, - "injections": {}, "runtimeConfig": null, "materializer": 11, "rate_weight": null, @@ -698,12 +741,8 @@ { "type": "function", "title": "root_execRawPrismaDelete_fn", - "policies": [ - 0 - ], "input": 55, "output": 14, - "injections": {}, "runtimeConfig": null, "materializer": 12, "rate_weight": null, @@ -712,12 +751,8 @@ { "type": "function", "title": "root_queryPrismaModel_fn", - "policies": [ - 0 - ], "input": 70, "output": 72, - "injections": {}, "runtimeConfig": null, "materializer": 13, "rate_weight": null, @@ -726,7 +761,6 @@ { "type": "object", "title": "root_queryPrismaModel_fn_input", - "policies": [], "properties": { "typegraph": 5, "runtime": 5, @@ -735,35 +769,43 @@ "limit": 71 }, "id": [], - "required": [] + "required": [], + "policies": { + "typegraph": [], + "runtime": [], + "model": [], + "offset": [], + "limit": [] + } }, { "type": "integer", - "title": "root_queryPrismaModel_fn_input_offset_integer", - "policies": [] + "title": "root_queryPrismaModel_fn_input_offset_integer" }, { "type": "object", "title": "root_queryPrismaModel_fn_output", - "policies": [], "properties": { "fields": 73, "rowCount": 71, "data": 74 }, "id": [], - "required": [] + "required": [], + "policies": { + "fields": [], + "rowCount": [], + "data": [] + } }, { "type": "list", "title": "root_queryPrismaModel_fn_output_fields_PrismaModelInfo_fields_struct_list", - "policies": [], "items": 52 }, { "type": "list", "title": "root_queryPrismaModel_fn_output_data_root_addTypegraph_fn_input_fromString_string_json_list", - "policies": [], "items": 14 } ], @@ -956,4 +998,4 @@ "randomSeed": null, "artifacts": {} } -} +} \ No newline at end of file diff --git a/src/typegraph/core/src/conversion/types.rs b/src/typegraph/core/src/conversion/types.rs index c90f3728b..e7bed25d7 100644 --- a/src/typegraph/core/src/conversion/types.rs +++ b/src/typegraph/core/src/conversion/types.rs @@ -3,8 +3,8 @@ use crate::errors::Result; use crate::typegraph::TypegraphContext; -use crate::types::{ExtendedTypeDef, PolicySpec, TypeId}; -use common::typegraph::{PolicyIndices, TypeNode, TypeNodeBase}; +use crate::types::{ExtendedTypeDef, TypeId}; +use common::typegraph::{TypeNode, TypeNodeBase}; use enum_dispatch::enum_dispatch; use std::rc::Rc; @@ -20,25 +20,20 @@ impl TypeConversion for Rc { } } -pub struct BaseBuilderInit<'a, 'b> { - pub ctx: &'a mut TypegraphContext, +pub struct BaseBuilderInit { pub base_name: &'static str, pub type_id: TypeId, pub name: Option, - pub policies: &'b [PolicySpec], } pub struct BaseBuilder { name: String, - policies: Vec, // optional features enumeration: Option>, } -impl<'a, 'b> BaseBuilderInit<'a, 'b> { +impl BaseBuilderInit { pub fn init_builder(self) -> Result { - let policies = self.ctx.register_policy_chain(self.policies)?; - let name = match self.name { Some(name) => name, None => format!("{}_{}_placeholder", self.base_name, self.type_id.0), @@ -46,7 +41,6 @@ impl<'a, 'b> BaseBuilderInit<'a, 'b> { Ok(BaseBuilder { name, - policies, enumeration: None, }) } @@ -57,7 +51,6 @@ impl BaseBuilder { Ok(TypeNodeBase { description: None, enumeration: self.enumeration, - policies: self.policies, title: self.name, }) } diff --git a/src/typegraph/core/src/typedef/boolean.rs b/src/typegraph/core/src/typedef/boolean.rs index 652e761b7..786e9e097 100644 --- a/src/typegraph/core/src/typedef/boolean.rs +++ b/src/typegraph/core/src/typedef/boolean.rs @@ -11,19 +11,17 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{Boolean, ExtendedTypeDef, FindAttribute as _, TypeBoolean, TypeDefData}, + types::{Boolean, ExtendedTypeDef, TypeBoolean, TypeDefData}, }; use std::hash::Hash; impl TypeConversion for Boolean { - fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { + fn convert(&self, _ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::Boolean { base: BaseBuilderInit { - ctx, base_name: "boolean", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typedef/either.rs b/src/typegraph/core/src/typedef/either.rs index afae92ca0..a90b9c2a4 100644 --- a/src/typegraph/core/src/typedef/either.rs +++ b/src/typegraph/core/src/typedef/either.rs @@ -13,7 +13,7 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{Either, ExtendedTypeDef, FindAttribute as _, TypeDefData, TypeId}, + types::{Either, ExtendedTypeDef, TypeDefData, TypeId}, wit::core::TypeEither, }; @@ -21,11 +21,9 @@ impl TypeConversion for Either { fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::Either { base: BaseBuilderInit { - ctx, base_name: "either", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typedef/file.rs b/src/typegraph/core/src/typedef/file.rs index f1550a63a..0cb466083 100644 --- a/src/typegraph/core/src/typedef/file.rs +++ b/src/typegraph/core/src/typedef/file.rs @@ -9,7 +9,7 @@ use crate::conversion::hash::Hashable; use crate::conversion::types::{BaseBuilderInit, TypeConversion}; use crate::errors::Result; use crate::typegraph::TypegraphContext; -use crate::types::{ExtendedTypeDef, File, FindAttribute as _, TypeDefData}; +use crate::types::{ExtendedTypeDef, File, TypeDefData}; use crate::wit::core::TypeFile; impl TypeDefData for TypeFile { @@ -49,15 +49,13 @@ impl Hashable for TypeFile { } impl TypeConversion for File { - fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { + fn convert(&self, _ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::File { // TODO should `as_id` be supported? base: BaseBuilderInit { - ctx, base_name: "file", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typedef/float.rs b/src/typegraph/core/src/typedef/float.rs index d7e5f2489..6b08a41e9 100644 --- a/src/typegraph/core/src/typedef/float.rs +++ b/src/typegraph/core/src/typedef/float.rs @@ -14,19 +14,17 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{ExtendedTypeDef, FindAttribute as _, Float, TypeDefData}, + types::{ExtendedTypeDef, Float, TypeDefData}, wit::core::TypeFloat, }; impl TypeConversion for Float { - fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { + fn convert(&self, _ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::Float { base: BaseBuilderInit { - ctx, base_name: "float", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .enum_(self.data.enumeration.as_deref()) diff --git a/src/typegraph/core/src/typedef/func.rs b/src/typegraph/core/src/typedef/func.rs index 377d27e8c..c6a23630d 100644 --- a/src/typegraph/core/src/typedef/func.rs +++ b/src/typegraph/core/src/typedef/func.rs @@ -75,11 +75,9 @@ impl TypeConversion for Func { Ok(TypeNode::Function { base: BaseBuilderInit { - ctx, base_name: "func", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typedef/integer.rs b/src/typegraph/core/src/typedef/integer.rs index 5108b8573..37e1b2666 100644 --- a/src/typegraph/core/src/typedef/integer.rs +++ b/src/typegraph/core/src/typedef/integer.rs @@ -13,19 +13,17 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{ExtendedTypeDef, FindAttribute as _, Integer, TypeDefData}, + types::{ExtendedTypeDef, Integer, TypeDefData}, wit::core::TypeInteger, }; impl TypeConversion for Integer { - fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { + fn convert(&self, _ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::Integer { base: BaseBuilderInit { - ctx, base_name: "integer", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .enum_(self.data.enumeration.as_deref()) diff --git a/src/typegraph/core/src/typedef/list.rs b/src/typegraph/core/src/typedef/list.rs index 069cb79fa..3ea9e405a 100644 --- a/src/typegraph/core/src/typedef/list.rs +++ b/src/typegraph/core/src/typedef/list.rs @@ -12,7 +12,7 @@ use crate::{ }, errors::Result, typegraph::TypegraphContext, - types::{ExtendedTypeDef, FindAttribute as _, List, TypeDefData, TypeId}, + types::{ExtendedTypeDef, List, TypeDefData, TypeId}, wit::core::TypeList, }; @@ -20,11 +20,9 @@ impl TypeConversion for List { fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::List { base: BaseBuilderInit { - ctx, base_name: "list", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typedef/optional.rs b/src/typegraph/core/src/typedef/optional.rs index 77f650838..cb89e3b68 100644 --- a/src/typegraph/core/src/typedef/optional.rs +++ b/src/typegraph/core/src/typedef/optional.rs @@ -13,7 +13,7 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{ExtendedTypeDef, FindAttribute as _, Optional, TypeDefData, TypeId}, + types::{ExtendedTypeDef, Optional, TypeDefData, TypeId}, wit::core::TypeOptional, }; @@ -29,11 +29,9 @@ impl TypeConversion for Optional { Ok(TypeNode::Optional { base: BaseBuilderInit { - ctx, base_name: "optional", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typedef/string.rs b/src/typegraph/core/src/typedef/string.rs index 49afb6f13..57d3e0c9d 100644 --- a/src/typegraph/core/src/typedef/string.rs +++ b/src/typegraph/core/src/typedef/string.rs @@ -13,12 +13,12 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{ExtendedTypeDef, FindAttribute as _, StringT, TypeDefData}, + types::{ExtendedTypeDef, StringT, TypeDefData}, wit::core::TypeString, }; impl TypeConversion for StringT { - fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { + fn convert(&self, _ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { let format: Option = match self.data.format.clone() { Some(format) => { let ret = @@ -30,11 +30,9 @@ impl TypeConversion for StringT { Ok(TypeNode::String { base: BaseBuilderInit { - ctx, base_name: "string", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .enum_(self.data.enumeration.as_deref()) diff --git a/src/typegraph/core/src/typedef/struct_.rs b/src/typegraph/core/src/typedef/struct_.rs index c1481bcba..36993ad1b 100644 --- a/src/typegraph/core/src/typedef/struct_.rs +++ b/src/typegraph/core/src/typedef/struct_.rs @@ -4,10 +4,11 @@ use crate::conversion::hash::Hashable; use crate::conversion::types::{BaseBuilderInit, TypeConversion}; use crate::types::{ - AsTypeDefEx as _, ExtendedTypeDef, FindAttribute as _, IdKind, Struct, TypeDefData, TypeId, + AsTypeDefEx as _, ExtendedTypeDef, FindAttribute as _, IdKind, PolicySpec, Struct, TypeDef, + TypeDefData, TypeId, }; use crate::{errors, typegraph::TypegraphContext, wit::core::TypeStruct}; -use common::typegraph::{ObjectTypeData, TypeNode}; +use common::typegraph::{ObjectTypeData, PolicyIndices, TypeNode}; use errors::Result; use indexmap::IndexMap; use std::hash::Hash as _; @@ -63,11 +64,9 @@ impl TypeConversion for Struct { fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::Object { base: BaseBuilderInit { - ctx, base_name: "object", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .enum_(self.data.enumeration.as_deref()) @@ -81,6 +80,7 @@ impl TypeConversion for Struct { .collect::>>()?, id: self.data.find_id_fields()?, required: Vec::new(), + policies: self.data.collect_policies(ctx)?, }, }) } @@ -126,3 +126,59 @@ impl TypeStruct { .find_map(|(n, t)| if n == name { Some(t.into()) } else { None }) } } + +impl TypeStruct { + fn collect_policies( + &self, + ctx: &mut TypegraphContext, + ) -> Result>> { + let mut res = IndexMap::new(); + for (name, tpe_id) in &self.props { + let mut chain = vec![]; + extend_policy_chain(&mut chain, TypeId(*tpe_id))?; + + res.insert(name.clone(), ctx.register_policy_chain(&chain)?); + } + Ok(res) + } +} + +pub fn extend_policy_chain(chain: &mut Vec, type_id: TypeId) -> Result<()> { + let xdef = type_id.as_xdef()?; + let policies = xdef.attributes.find_policy().unwrap_or(&[]); + chain.extend(policies.iter().cloned()); + + match xdef.type_def { + TypeDef::Optional(inner) => { + extend_policy_chain(chain, TypeId(inner.data.of))?; + } + TypeDef::List(inner) => { + extend_policy_chain(chain, TypeId(inner.data.of))?; + } + TypeDef::Union(inner) => { + for tpe_id in inner.data.variants.iter() { + extend_policy_chain(chain, TypeId(*tpe_id))?; + } + } + TypeDef::Either(inner) => { + for tpe_id in inner.data.variants.iter() { + extend_policy_chain(chain, TypeId(*tpe_id))?; + } + } + TypeDef::Func(inner) => { + extend_policy_chain(chain, TypeId(inner.data.out))?; + } + TypeDef::Struct(_) => { + // noop + // struct is the boundary + } + // scalar types + TypeDef::Integer(_) + | TypeDef::Float(_) + | TypeDef::Boolean(_) + | TypeDef::String(_) + | TypeDef::File(_) => {} + } + + Ok(()) +} diff --git a/src/typegraph/core/src/typedef/union.rs b/src/typegraph/core/src/typedef/union.rs index c037202fc..1eeb0b924 100644 --- a/src/typegraph/core/src/typedef/union.rs +++ b/src/typegraph/core/src/typedef/union.rs @@ -13,7 +13,7 @@ use crate::{ }, errors, typegraph::TypegraphContext, - types::{ExtendedTypeDef, FindAttribute as _, TypeDefData, TypeId, Union}, + types::{ExtendedTypeDef, TypeDefData, TypeId, Union}, wit::core::TypeUnion, }; @@ -21,11 +21,9 @@ impl TypeConversion for Union { fn convert(&self, ctx: &mut TypegraphContext, xdef: ExtendedTypeDef) -> Result { Ok(TypeNode::Union { base: BaseBuilderInit { - ctx, base_name: "union", type_id: self.id, name: xdef.get_owned_name(), - policies: xdef.attributes.find_policy().unwrap_or(&[]), } .init_builder()? .build()?, diff --git a/src/typegraph/core/src/typegraph.rs b/src/typegraph/core/src/typegraph.rs index 71b11b31f..85548bb7f 100644 --- a/src/typegraph/core/src/typegraph.rs +++ b/src/typegraph/core/src/typegraph.rs @@ -5,6 +5,7 @@ use crate::conversion::hash::Hasher; use crate::conversion::runtimes::{convert_materializer, convert_runtime, ConvertedRuntime}; use crate::conversion::types::TypeConversion as _; use crate::global_store::SavedState; +use crate::typedef::struct_::extend_policy_chain; use crate::types::{ AsTypeDefEx as _, FindAttribute as _, PolicySpec, TypeDef, TypeDefExt, TypeId, WithPolicy, }; @@ -129,11 +130,11 @@ pub fn init(params: TypegraphInitParams) -> Result<()> { base: TypeNodeBase { description: None, enumeration: None, - policies: Default::default(), title: params.name, }, data: ObjectTypeData { properties: IndexMap::new(), + policies: Default::default(), id: vec![], required: vec![], }, @@ -320,9 +321,16 @@ pub fn expose( return Err(errors::duplicate_export_name(&key)); } ensure_valid_export(key.clone(), type_id)?; + let mut policy_chain = vec![]; + extend_policy_chain(&mut policy_chain, type_id)?; let type_idx = ctx.register_type(type_id)?; root_data.properties.insert(key.clone(), type_idx.into()); + if !policy_chain.is_empty() { + root_data + .policies + .insert(key.clone(), ctx.register_policy_chain(&policy_chain)?); + } root_data.required.push(key); Ok(()) }) From 40e3ab260c54b2ce1fe7484e23d4d52fa187523f Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Thu, 12 Dec 2024 21:33:26 +0300 Subject: [PATCH 03/11] feat: fixed policy chain merge + small improv + basic spec test --- src/typegate/src/engine/planner/policies.ts | 50 ++------ src/typegraph/core/src/lib.rs | 9 +- src/typegraph/deno/src/types.ts | 9 +- src/typegraph/python/typegraph/t.py | 5 +- tests/policies/effects_py.py | 5 +- tests/policies/policies.py | 17 +-- tests/policies/policies_composition.py | 76 +++++++++++ tests/policies/policies_composition_test.ts | 134 ++++++++++++++++++++ tests/policies/policies_test.ts | 6 +- 9 files changed, 243 insertions(+), 68 deletions(-) create mode 100644 tests/policies/policies_composition.py create mode 100644 tests/policies/policies_composition_test.ts diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index 109922ba3..faa5d8145 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -54,7 +54,6 @@ export class OperationPolicies { config: OperationPoliciesConfig, ) { this.functions = builder.subtrees; - console.log("functions", this.functions); this.policiesForType = new Map(); for (const [stageId, subtreeData] of this.functions.entries()) { @@ -92,6 +91,7 @@ export class OperationPolicies { } this.resolvers = new Map(); + const policies = new Set([...this.policiesForType.values()].flat()); for (const idx of policies) { for (const polIdx of iterIndices(idx)) { @@ -163,18 +163,9 @@ export class OperationPolicies { const effect = this.tg.materializer( this.tg.type(subtree.funcTypeIdx, Type.FUNCTION).materializer, ).effect.effect ?? "read"; - - - // await this.authorizeArgs( - // subtree.argPolicies, - // effect, - // getResolverResult, - // authorizedTypes[effect], - // ); - const req = { - authorizedTypes, effect, + authorizedTypesOnEffect: authorizedTypes[effect], getResolverResult }; @@ -189,7 +180,7 @@ export class OperationPolicies { } } - await this.#checkPolicyForStage(stageId, refTypes, req); + await this.#checkPolicyForStage(stageId, refTypes, req); } } } @@ -211,31 +202,6 @@ export class OperationPolicies { return `Authorization failed for ${details.join("; ")}`; } - // TODO: rm??? checkTypePolicies assumes the policy appear on the **selected** output either way - // private async authorizeArgs( - // argPolicies: ArgPolicies, - // effect: EffectType, - // getResolverResult: GetResolverResult, - // authorizedTypes: Set, - // ) { - // for (const [typeIdx, { argDetails, policyIndices }] of argPolicies) { - // if (authorizedTypes.has(typeIdx)) { - // continue; - // } - // authorizedTypes.add(typeIdx); - - // const res = await this.#checkTypePolicies( - // policyIndices, - // effect, - // getResolverResult, - // ); - // if (!res.authorized) { - // // unauthorized or no decision - // throw new Error(`Unexpected argument ${argDetails}`); - // } - // } - // } - /** * A single type may hold multiple policies * @@ -255,6 +221,7 @@ export class OperationPolicies { const deniersIdx = []; for (const policyIdx of policies) { const res = await getResolverResult(policyIdx, effect); + switch(res) { case "ALLOW": { operands.push(true); @@ -293,15 +260,16 @@ export class OperationPolicies { referencedTypes: Array, req: { effect: EffectType, - authorizedTypes: Record>, + authorizedTypesOnEffect: Set, getResolverResult: GetResolverResult } ) { for (const typeIdx of referencedTypes) { - if (req.authorizedTypes[req.effect].has(typeIdx)) { + if (req.authorizedTypesOnEffect.has(typeIdx)) { continue; } + const allPolicies = this.policiesForType.get(typeIdx) ?? []; const policies = allPolicies.map((p) => typeof p === "number" ? p : p[req.effect] ?? null @@ -312,18 +280,16 @@ export class OperationPolicies { this.getRejectionReason(stageId, typeIdx, req.effect, ["__deny"]), ); } - const res = await this.#composePolicies( policies as number[], req.effect, req.getResolverResult, ); - console.log("verdict for", this.tg.type(typeIdx).title, res.authorized); - switch(res.authorized) { case "ALLOW": { this.#resolvedPolicyCachePerStage.set(stageId, "ALLOW"); + req.authorizedTypesOnEffect.add(typeIdx); return; } case "PASS": { diff --git a/src/typegraph/core/src/lib.rs b/src/typegraph/core/src/lib.rs index e13e0db35..16886c16d 100644 --- a/src/typegraph/core/src/lib.rs +++ b/src/typegraph/core/src/lib.rs @@ -285,13 +285,16 @@ impl wit::core::Guest for Lib { .to_string(); let check = match check { - ContextCheck::NotNull => "value != null".to_string(), + ContextCheck::NotNull => "value != null ? 'ALLOW' : 'DENY'".to_string(), ContextCheck::Value(val) => { - format!("value === {}", serde_json::to_string(&val).unwrap()) + format!( + "value === {} ? 'ALLOW' : 'DENY'", + serde_json::to_string(&val).unwrap() + ) } ContextCheck::Pattern(pattern) => { format!( - "new RegExp({}).test(value)", + "new RegExp({}).test(value) ? 'ALLOW' : 'DENY' ", serde_json::to_string(&pattern).unwrap() ) } diff --git a/src/typegraph/deno/src/types.ts b/src/typegraph/deno/src/types.ts index c037153c1..b556e2438 100644 --- a/src/typegraph/deno/src/types.ts +++ b/src/typegraph/deno/src/types.ts @@ -81,7 +81,7 @@ export function getPolicyChain( export class Typedef { readonly name?: string; - policy: Policy[] | null = null; + policy: WitPolicySpec[] | null = null; constructor(public readonly _id: number) {} @@ -90,15 +90,16 @@ export class Typedef { } withPolicy(policy: PolicySpec[] | PolicySpec): this { - const id = core.withPolicy(this._id, getPolicyChain(policy)); + const chain = getPolicyChain(policy); + const newChain = [...(this.policy ?? []), ...chain]; + const id = core.withPolicy(this._id, newChain); - const chain = Array.isArray(policy) ? policy : [policy]; return new Proxy(this, { get(target, prop, receiver) { if (prop === "_id") { return id; } else if (prop === "policy") { - return chain; + return newChain; } else { return Reflect.get(target, prop, receiver); } diff --git a/src/typegraph/python/typegraph/t.py b/src/typegraph/python/typegraph/t.py index 5e6215d0c..8433ffdf7 100644 --- a/src/typegraph/python/typegraph/t.py +++ b/src/typegraph/python/typegraph/t.py @@ -75,13 +75,14 @@ def __repr__(self): def with_policy(self, *policies: Optional[PolicySpec]) -> Self: policy_chain = get_policy_chain(policies) - res = core.with_policy(store, self._id, policy_chain) + new_policy_chain = (self.policy_chain or []) + policy_chain + res = core.with_policy(store, self._id, new_policy_chain) if isinstance(res, Err): raise ErrorStack(res.value) ret = copy.copy(self) ret._id = res.value - ret.policy_chain = policy_chain + ret.policy_chain = new_policy_chain return ret def rename(self, name: str) -> Self: diff --git a/tests/policies/effects_py.py b/tests/policies/effects_py.py index 0bbbd0689..9a46bd0c1 100644 --- a/tests/policies/effects_py.py +++ b/tests/policies/effects_py.py @@ -11,14 +11,13 @@ def effects_py(g: Graph): deno = DenoRuntime() public = Policy.public() admin_only = Policy.context("role", "admin") + deny_all = deno.policy("deny_all", "() => 'DENY'") user = t.struct( { "id": t.integer(), "email": t.email(), - "password_hash": t.string().with_policy( - deno.policy("deny_all", "() => false") - ), + "password_hash": t.string().with_policy(deny_all), }, name="User", ).with_policy(Policy.on(read=public, update=admin_only, delete=admin_only)) diff --git a/tests/policies/policies.py b/tests/policies/policies.py index 78e308364..273ad8287 100644 --- a/tests/policies/policies.py +++ b/tests/policies/policies.py @@ -10,14 +10,6 @@ def policies(g: Graph): deno = DenoRuntime() - _secret_data = t.struct( - { - "username": t.string(), - "data": t.string(), - }, - name="SecretData", - ) - fn = deno.identity( t.struct({"a": t.integer()}), ) @@ -25,10 +17,13 @@ def policies(g: Graph): g.auth(Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})) g.expose( - pol_true=fn.with_policy(deno.policy("true", "() => true")), - pol_false=fn.with_policy(deno.policy("false", "() => false")), + pol_pass=fn.with_policy(deno.policy("pass", "() => 'PASS'")), + pol_deny=fn.with_policy(deno.policy("deny", "() => 'DENY'")), pol_two=fn.with_policy( - deno.policy("eq_two", "(_args, { context }) => Number(context.a) === 2") + deno.policy( + "eq_two", + "(_args, { context }) => Number(context.a) === 2 ? 'ALLOW' : 'DENY'", + ) ), ns=t.struct( { diff --git a/tests/policies/policies_composition.py b/tests/policies/policies_composition.py new file mode 100644 index 000000000..a8a70fb72 --- /dev/null +++ b/tests/policies/policies_composition.py @@ -0,0 +1,76 @@ +# Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +# SPDX-License-Identifier: MPL-2.0 + +from typegraph import typegraph, t, Graph +from typegraph.runtimes import DenoRuntime + + +@typegraph() +def policies_composition(g: Graph): + deno = DenoRuntime() + + def ctxread(pol_name: str): + return deno.policy(pol_name, code=f"(_, {{ context }}) => context.{pol_name}") + + deny = deno.policy("denyAll", code="() => 'DENY' ") + allow = deno.policy("allowAll", code="() => 'ALLOW' ") + pass_through = deno.policy("passThrough", code="() => 'PASS' ") # alt public + + a = t.struct( + { + "a": t.string(), + "b": t.string().with_policy(deny), + } + ).rename("A") + b = t.struct( + { + "b": t.string().with_policy(allow), + } + ).rename("B") + + g.expose( + simple_traversal_comp=deno.identity( + t.struct( + { + "one": t.struct( + { + "two": t.either([t.integer(), t.string()]).with_policy( + deny + ), + "three": t.either([t.integer(), t.string()]).with_policy( + allow + ), + } + ).with_policy(ctxread("control_value")), + } + ) + ).with_policy(pass_through), + single_field_comp=deno.identity( + t.struct( + { + "abc": t.string() + .with_policy(ctxread("A"), ctxread("B")) + .with_policy(ctxread("C")) + } + ) + ).with_policy(pass_through), + identity=deno.identity( + t.struct( + { + "zero": t.string().rename("Zero"), + "one": t.string() + .rename("One") + .with_policy(pass_through, ctxread("D")) + .with_policy(allow), + "two": t.struct( + { + "three": t.either([a, b]) + .rename("Three") + .with_policy(allow), + "four": t.string().rename("Four"), + } + ).with_policy(pass_through), + } + ), + ).with_policy(pass_through), + ) diff --git a/tests/policies/policies_composition_test.ts b/tests/policies/policies_composition_test.ts new file mode 100644 index 000000000..b659847c6 --- /dev/null +++ b/tests/policies/policies_composition_test.ts @@ -0,0 +1,134 @@ +// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. +// SPDX-License-Identifier: MPL-2.0 + +import { gql, Meta } from "../utils/mod.ts"; + +Meta.test("Basic composition through traversal spec", async (t) => { + const e = await t.engine("policies/policies_composition.py"); + + await t.should("allow child if parent is set to 'ALLOW'", async () => { + await gql` + query { + simple_traversal_comp(one: { two: 2, three: "three" }) { # pass + one { + two # deny + three # allow + } #! allow + } + } + ` + .withContext({ + control_value: "ALLOW" + }) + .expectData({ + simple_traversal_comp: { + one: { + two: 2, + three: "three" + } + }, + }) + .on(e); + }); + + + await t.should("skip parent and go further with 'PASS'", async () => { + await gql` + query { + simple_traversal_comp(one: { two: 2, three: "three" }) { # pass + one { + two # deny + three # allow + } #! pass + } + } + ` + .withContext({ + control_value: "PASS" + }) + .expectErrorContains("('either') at '.simple_traversal_comp.one.two'") + .on(e); + }); + + + await t.should("deny and stop if parent is set to 'DENY'", async () => { + await gql` + query { + simple_traversal_comp(one: { two: 2, three: "three" }) { # pass + one { + two # deny + three # allow + } #! deny + } + } + ` + .withContext({ + control_value: "DENY" + }) + .expectErrorContains("('object') at '.simple_traversal_comp.one'") + .on(e); + }); +}); + + + +Meta.test("Basic chain composition on a single field spec", async (t) => { + const e = await t.engine("policies/policies_composition.py"); + + await t.should("have PASS not affecting the outcome (version 1)", async () => { + await gql` + query { + single_field_comp(abc: "enter" ) { + abc + } + } + ` + .withContext({ + A: "PASS", + B: "ALLOW", + C: "PASS" + }) + .expectData({ + single_field_comp: { + abc: "enter" + } + }) + .on(e); + }); + + + await t.should("have PASS not affecting the outcome (version 2)", async () => { + await gql` + query { + single_field_comp(abc: "enter" ) { + abc + } + } + ` + .withContext({ + A: "PASS", + B: "DENY", + C: "PASS" + }) + .expectErrorContains("Authorization failed for policy 'B'") + .on(e); + }); + + + await t.should("have DENY ruling", async () => { + await gql` + query { + single_field_comp(abc: "enter" ) { + abc + } + } + ` + .withContext({ + A: "PASS", + B: "DENY", + C: "ALLOW" + }) + .expectErrorContains("Authorization failed for policy 'B'") + .on(e); + }); +}); diff --git a/tests/policies/policies_test.ts b/tests/policies/policies_test.ts index 988734b6a..aaf2ba3ab 100644 --- a/tests/policies/policies_test.ts +++ b/tests/policies/policies_test.ts @@ -29,13 +29,13 @@ Meta.test("Policies", async (t) => { await t.should("have public access", async () => { await gql` query { - pol_true(a: 1) { + pol_pass(a: 1) { a } } ` .expectData({ - pol_true: { + pol_pass: { a: 1, }, }) @@ -45,7 +45,7 @@ Meta.test("Policies", async (t) => { await t.should("have no access", async () => { await gql` query { - pol_false(a: 1) { + pol_deny(a: 1) { a } } From 9275a67a8d034463245240a46a0b912641364321 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Fri, 13 Dec 2024 20:01:08 +0300 Subject: [PATCH 04/11] wip: post-merge --- src/typegate/src/engine/planner/policies.ts | 5 ++ tests/policies/policies_composition.py | 47 ++++++++-------- tests/policies/policies_composition_test.ts | 59 ++++++++++++++++++--- 3 files changed, 79 insertions(+), 32 deletions(-) diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index faa5d8145..327f3eec9 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -242,6 +242,9 @@ export class OperationPolicies { } } + console.log("got operands", operands); + console.log("got deniers", deniersIdx); + if (operands.length == 0) { return { authorized: "PASS" }; } else { @@ -280,6 +283,8 @@ export class OperationPolicies { this.getRejectionReason(stageId, typeIdx, req.effect, ["__deny"]), ); } + + console.log("type", this.tg.type(typeIdx).title, policies) const res = await this.#composePolicies( policies as number[], req.effect, diff --git a/tests/policies/policies_composition.py b/tests/policies/policies_composition.py index a8a70fb72..c4cdf5102 100644 --- a/tests/policies/policies_composition.py +++ b/tests/policies/policies_composition.py @@ -16,18 +16,6 @@ def ctxread(pol_name: str): allow = deno.policy("allowAll", code="() => 'ALLOW' ") pass_through = deno.policy("passThrough", code="() => 'PASS' ") # alt public - a = t.struct( - { - "a": t.string(), - "b": t.string().with_policy(deny), - } - ).rename("A") - b = t.struct( - { - "b": t.string().with_policy(allow), - } - ).rename("B") - g.expose( simple_traversal_comp=deno.identity( t.struct( @@ -54,23 +42,32 @@ def ctxread(pol_name: str): } ) ).with_policy(pass_through), - identity=deno.identity( + traversal_comp=deno.identity( t.struct( { - "zero": t.string().rename("Zero"), - "one": t.string() - .rename("One") - .with_policy(pass_through, ctxread("D")) - .with_policy(allow), - "two": t.struct( + "one": t.struct( { - "three": t.either([a, b]) - .rename("Three") - .with_policy(allow), - "four": t.string().rename("Four"), + "two": t.struct( + { + "three": t.either( + [ + t.struct({"a": t.integer()}).rename( + "First" + ), + t.struct( + { + "b": t.integer() + .rename("Second") + .with_policy(ctxread("depth_4")) + } + ), + ] + ).with_policy(ctxread("depth_3")) + } + ).with_policy(ctxread("depth_2")) } - ).with_policy(pass_through), + ).with_policy(ctxread("depth_1")) } - ), + ) ).with_policy(pass_through), ) diff --git a/tests/policies/policies_composition_test.ts b/tests/policies/policies_composition_test.ts index b659847c6..ebfed9d29 100644 --- a/tests/policies/policies_composition_test.ts +++ b/tests/policies/policies_composition_test.ts @@ -115,20 +115,65 @@ Meta.test("Basic chain composition on a single field spec", async (t) => { }); - await t.should("have DENY ruling", async () => { + // await t.should("have DENY ruling", async () => { + // await gql` + // query { + // single_field_comp(abc: "enter" ) { + // abc + // } + // } + // ` + // .withContext({ + // A: "PASS", + // B: "DENY", + // C: "ALLOW" + // }) + // .expectErrorContains("Authorization failed for policy 'B'") + // .on(e); + // }); +}); + +/* +Meta.test("Traversal composition", async (t) => { + const e = await t.engine("policies/policies_composition.py"); + + const one = { + two: { + three: { + a: 1, + b: 2 + } + } + }; + + await t.should("work: traversal 1", async () => { await gql` query { - single_field_comp(abc: "enter" ) { - abc + traversal_comp(one: $one) { + one { + two { + three { + ... on First { a } + ... on Second { + b # d4 + } + } # d3 + } # d2 + } # d1 } } ` + .withVars({ one }) .withContext({ - A: "PASS", - B: "DENY", - C: "ALLOW" + depth_1: "PASS", + depth_2: "ALLOW", + depth_3: "PASS", + depth_4: "PASS" }) - .expectErrorContains("Authorization failed for policy 'B'") + .expectData({ + traversal_comp: { one } + }) .on(e); }); }); +*/ From fd4b4111cc8dfcb0bf5d53519124952ea7fdc7a2 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Mon, 16 Dec 2024 20:07:51 +0300 Subject: [PATCH 05/11] wip: working impl after hoisting + more tests --- src/typegate/src/engine/planner/mod.ts | 74 ++--- src/typegate/src/engine/planner/policies.ts | 295 +++++++++++--------- tests/policies/policies_composition.py | 8 +- tests/policies/policies_composition_test.ts | 83 ++++-- 4 files changed, 248 insertions(+), 212 deletions(-) diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index 9e22abcf5..51bf45db8 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -18,18 +18,14 @@ import { } from "../../typegraph/type_node.ts"; import { closestWord, unparse } from "../../utils.ts"; import { collectArgs, type ComputeArg } from "./args.ts"; -import { - type OperationPolicies, - OperationPoliciesBuilder, -} from "./policies.ts"; +import { OperationPolicies, StageMetadata } from "./policies.ts"; import { getLogger } from "../../log.ts"; import { generateVariantMatcher } from "../typecheck/matching_variant.ts"; import { mapValues } from "@std/collections/map-values"; import { DependencyResolver } from "./dependency_resolver.ts"; import { Runtime } from "../../runtimes/Runtime.ts"; import { getInjection } from "../../typegraph/utils.ts"; -import { Injection, PolicyIndices } from "../../typegraph/types.ts"; -import { getInjectionValues } from "./injection_utils.ts"; +import { Injection } from "../../typegraph/types.ts"; const logger = getLogger(import.meta); @@ -70,7 +66,6 @@ export interface Plan { */ export class Planner { rawArgs: Record = {}; - policiesBuilder: OperationPoliciesBuilder; constructor( readonly operation: ast.OperationDefinitionNode, @@ -78,10 +73,6 @@ export class Planner { private readonly tg: TypeGraph, private readonly verbose: boolean, ) { - const { timer_policy_eval_retries } = tg.typegate.config.base; - this.policiesBuilder = new OperationPoliciesBuilder(tg, { - timer_policy_eval_retries, - }); } getPlan(): Plan { @@ -112,13 +103,25 @@ export class Planner { {} as Record, ); + const orderedStageMetadata = [] as Array; for (const stage of stages) { stage.varTypes = varTypes; + + orderedStageMetadata.push({ + stageId: stage.id(), + typeIdx: stage.props.typeIdx, + isTopLevel: stage.props.parent ? false : true + }); } + const { timer_policy_eval_retries } = this.tg.typegate.config.base; + const operationPolicies = new OperationPolicies(this.tg, orderedStageMetadata, { + timer_policy_eval_retries + }); + return { stages, - policies: this.policiesBuilder.build(), + policies: operationPolicies, }; } @@ -398,18 +401,11 @@ export class Planner { } const fieldType = this.tg.type(node.typeIdx); - // TODO CHECK does this work with aliases - // TODO array or null?? - const policies = - (this.tg.type(parent.typeIdx, Type.OBJECT).policies ?? {})[node.name] ?? - []; - const stages = fieldType.type !== Type.FUNCTION - ? this.traverseValueField(node, policies) + ? this.traverseValueField(node) : this.traverseFuncField( node, this.tg.type(parent.typeIdx, Type.OBJECT).properties, - policies, ); return stages; @@ -441,7 +437,6 @@ export class Planner { */ private traverseValueField( node: Node, - policies: PolicyIndices[], ): ComputeStage[] { const outjection = node.scope && this.#getOutjection(node.scope!); if (outjection) { @@ -495,7 +490,6 @@ export class Planner { private traverseFuncField( node: Node, parentProps: Record, - policies: PolicyIndices[], ): ComputeStage[] { const stages: ComputeStage[] = []; const deps = []; @@ -567,31 +561,21 @@ export class Planner { }); stages.push(stage); - this.policiesBuilder.push( - stage.id(), - node.typeIdx, - collected.policies, - policies, - ); - - { - // nested quantifiers - let wrappedTypeIdx = outputIdx; - let wrappedType = this.tg.type(wrappedTypeIdx); - while (isQuantifier(wrappedType)) { - wrappedTypeIdx = getWrappedType(wrappedType); - wrappedType = this.tg.type(wrappedTypeIdx); - } - - stages.push( - ...this.traverse( - { ...node, typeIdx: wrappedTypeIdx, parentStage: stage }, - stage, - ), - ); + // nested quantifiers + let wrappedOutputIdx = outputIdx; + let wrappedType = this.tg.type(wrappedOutputIdx); + while (isQuantifier(wrappedType)) { + wrappedOutputIdx = getWrappedType(wrappedType); + wrappedType = this.tg.type(wrappedOutputIdx); } - this.policiesBuilder.pop(stage.id()); + stages.push( + ...this.traverse( + { ...node, typeIdx: wrappedOutputIdx, parentStage: stage }, + stage, + ), + ); + return stages; } diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index 5e8f5b325..acf02367f 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -7,7 +7,6 @@ import type { Context, Info, PolicyIdx, - PolicyList, Resolver, StageId, TypeIdx, @@ -15,19 +14,23 @@ import type { import type { EffectType, PolicyIndices } from "../../typegraph/types.ts"; import { ensure } from "../../utils.ts"; import { getLogger } from "../../log.ts"; -import { Type } from "../../typegraph/type_node.ts"; -import type { ArgPolicies } from "./args.ts"; +import { getWrappedType, isFunction, isQuantifier, Type } from "../../typegraph/type_node.ts"; import { BadContext } from "../../errors.ts"; export type PolicyResolverOutput = "DENY" | "ALLOW" | "PASS" | (unknown & {}); type GetResolverResult = (polIdx: PolicyIdx, effect: EffectType) => Promise; -export interface FunctionSubtreeData { +export interface StageMetadata { + stageId: string; typeIdx: TypeIdx; isTopLevel: boolean; - // types referenced in descendant nodes (that is not a descendent of a descendent function) - referencedTypes: Map>; +} + + +interface ComposePolicyOperand { + canonFieldName: string; + index: PolicyIdx; } type CheckResult = @@ -35,48 +38,78 @@ type CheckResult = | { authorized: "PASS" } | { authorized: "DENY"; - policiesFailedIdx: Array; + policiesFailed: Array; }; export type OperationPoliciesConfig = { timer_policy_eval_retries: number; }; +interface PolicyForStage { + canonFieldName: string; + /** Each item is either a PolicyIndicesByEffect or a number */ + indices: Array +} + + export class OperationPolicies { - // should be private -- but would not be testable - functions: Map; - #policiesForType: Map; - #resolvers: Map; + #stageToPolicies: Map> = new Map(); + #resolvers: Map = new Map(); - #resolvedPolicyCachePerStage: Map = new Map(); constructor( private tg: TypeGraph, - builder: OperationPoliciesBuilder, - config: OperationPoliciesConfig, + private orderedStageMetadata: Array, + private config: OperationPoliciesConfig, ) { - this.functions = builder.subtrees; - this.#policiesForType = new Map(); - for (const [stageId, subtree] of this.functions.entries()) { - const { funcTypeIdx, topLevel, policies } = subtree; - this.#policiesForType.set(stageId, policies); + this.#prepareStageToPolicies(); + this.#preallocateResolvers(); + } + + + #prepareStageToPolicies() { + this.#stageToPolicies = new Map(); + for (const { stageId, typeIdx: rawIdx } of this.orderedStageMetadata) { + const policies = this.#getPolicies(rawIdx); + this.#stageToPolicies.set(stageId, policies); + console.log("> found", stageId, policies); // top-level functions must have policies - if (topLevel && policies.length === 0) { - const details = [ - `top-level function '${this.tg.type(funcTypeIdx).title}'`, - `at '${stageId}'`, - ].join(" "); - throw new Error( - `No authorization policy took decision for ${details}'`, - ); - } + const isTopLevel = stageId.split(".").length == 1; + const policyCount = policies.reduce((total, { indices }) => total + indices.length, 0); + // FIXME: policy on function not collected? + // if (isTopLevel && policyCount === 0) { + // const details = [ + // `top-level function '${this.tg.type(rawIdx).title}'`, + // `at '${stageId}'`, + // ].join(" "); + // throw new Error( + // `No authorization policy took decision for ${details}'`, + // ); + // } } + } + #preallocateResolvers() { this.#resolvers = new Map(); - const policies = new Set([...this.#policiesForType.values()].flat()); - for (const idx of policies) { - for (const polIdx of iterIndices(idx)) { + const policyIndicesWithDup = Array.from(this.#stageToPolicies.values()) + .map((policyPerName) => { + const indices = policyPerName.map((({ indices }) => indices)); + return indices.flat(); + }) + .flat(); + + const policyIndices = new Set(policyIndicesWithDup); + + for (const indicesData of policyIndices) { + let toPrepare = [] as Array; + if (typeof indicesData == "number") { + toPrepare = [indicesData]; + } else { + toPrepare = Object.values(indicesData); + } + + for (const polIdx of toPrepare) { const mat = this.tg.policyMaterializer(this.tg.policy(polIdx)); const runtime = this.tg.runtimeReferences[mat.runtime] as DenoRuntime; ensure( @@ -86,7 +119,7 @@ export class OperationPolicies { if (!this.#resolvers.has(polIdx)) { this.#resolvers.set( polIdx, - runtime.delegate(mat, false, config.timer_policy_eval_retries), + runtime.delegate(mat, false, this.config.timer_policy_eval_retries), ); } } @@ -133,29 +166,39 @@ export class OperationPolicies { return res; }; - // TODO refactor: too much indentation - outerIter: for (const [stageId, subtree] of this.functions) { - for (const [priorStageId, verdict] of this.#resolvedPolicyCachePerStage.entries()) { + const resolvedPolicyCachePerStage: Map = new Map(); + + outerIter: for (const stageMeta of this.orderedStageMetadata) { + const { stageId } = stageMeta; + + console.log("verdict for", stageId); + + for (const [priorStageId, verdict] of resolvedPolicyCachePerStage.entries()) { if (stageId.startsWith(priorStageId) && verdict == "ALLOW") { continue outerIter; } // elif deny => already thrown } - const { effect, res } = await this.#checkStageAuthorization(stageId, subtree, getResolverResult); + const { effect, res } = await this.#checkStageAuthorization(stageMeta, getResolverResult); + switch(res.authorized) { case "ALLOW": { - this.#resolvedPolicyCachePerStage.set(stageId, "ALLOW"); - return false; + resolvedPolicyCachePerStage.set(stageId, "ALLOW"); + continue; } case "PASS": { - this.#resolvedPolicyCachePerStage.set(stageId, "PASS"); - return; + resolvedPolicyCachePerStage.set(stageId, "PASS"); + continue; } default: { - this.#resolvedPolicyCachePerStage.set(stageId, res.authorized); - const policyNames = res.policiesFailedIdx.map((idx) => this.tg.policy(idx).name); + resolvedPolicyCachePerStage.set(stageId, res.authorized); + const policyNames = res.policiesFailed.map((operand) => ({ + name: this.tg.policy(operand.index).name, + concernedField: operand.canonFieldName + })); + throw new BadContext( - this.getRejectionReason(stageId, effect, policyNames), + this.getRejectionReason(stageId, effect, policyNames), ); } } @@ -165,21 +208,18 @@ export class OperationPolicies { getRejectionReason( stageId: StageId, effect: EffectType, - policyNames: Array, + policiesData: Array<{ name: string, concernedField: string }>, ): string { - // if (policyNames.length == 0) { - // // invalid state? - // } - const details = policyNames - .map((policyName) => [ - `policy '${policyName}'`, + const details = policiesData + .map(({ name, concernedField }) => [ + `policy '${name}'`, `with effect '${effect}'`, - `at '.${stageId}'`, + `at '.${stageId}.${concernedField}'`, ].join(" ")); return `Authorization failed for ${details.join("; ")}`; } - /** + /** * A single type may hold multiple policies * * * `ALLOW`: ALLOW & P = P @@ -190,14 +230,14 @@ export class OperationPolicies { * PASS does not participate. **/ async #composePolicies( - policies: PolicyIdx[], + policies: Array, effect: EffectType, getResolverResult: GetResolverResult, ): Promise { const operands = []; const deniersIdx = []; - for (const policyIdx of policies) { - const res = await getResolverResult(policyIdx, effect); + for (const operand of policies) { + const res = await getResolverResult(operand.index, effect); switch(res) { case "ALLOW": { @@ -206,7 +246,7 @@ export class OperationPolicies { } case "DENY": { operands.push(false); - deniersIdx.push(policyIdx); + deniersIdx.push(operand); break; } case "PASS": { @@ -224,102 +264,87 @@ export class OperationPolicies { if (operands.every((_bool) => _bool)) { return { authorized: "ALLOW" }; } else { - return { authorized: "DENY", policiesFailedIdx: deniersIdx } + return { authorized: "DENY", policiesFailed: deniersIdx } } } } - async #checkStageAuthorization(stageId: string, subtree: SubtreeData, getResolverResult: GetResolverResult) { - const effect = this.tg.materializer( - this.tg.type(subtree.funcTypeIdx, Type.FUNCTION).materializer, - ).effect.effect ?? "read"; + #getPolicies(typeIdx: number): Array { + let nestedTypeIdx = typeIdx; + let nestedSchema = this.tg.type(nestedTypeIdx); - const policies = subtree.policies.map((p) => { - if (typeof p === "number") { - return p; - } - return p[effect] ?? null; - }); + if (isFunction(nestedSchema)) { + // TODO: collect the policies on the function as part of the oeprands + nestedTypeIdx = nestedSchema.output; + nestedSchema = this.tg.type(nestedTypeIdx); + } - if (policies.some((idx) => idx == null)) { - throw new BadContext( - this.getRejectionReason(stageId, effect, ["__deny"]), - ); + while (isQuantifier(nestedSchema)) { + nestedTypeIdx = getWrappedType(nestedSchema); + nestedSchema = this.tg.type(nestedTypeIdx); } - return { - effect, - res: await this.#composePolicies( - policies as number[], - effect, - getResolverResult, - ) - }; + let out: Record> = {}; + if(nestedSchema.type == "object") { + out = nestedSchema.policies ?? {}; + } + + return Object.entries(out).map(([k, v]) => ({ + canonFieldName: k, + indices: v + })) } -} -interface SubtreeData { - stageId: StageId; - funcTypeIdx: TypeIdx; - // TODO inputPolicies: Map - argPolicies: ArgPolicies; - policies: PolicyIndices[]; - topLevel: boolean; -} + #getEffectOrDefault(typeIdx: number) { + let effect = "read" as EffectType; + const node = this.tg.type(typeIdx); + if (isFunction(node)) { + const matIdx = this.tg.type(typeIdx, Type.FUNCTION).materializer; + effect = this.tg.materializer(matIdx).effect.effect ?? effect; + } -export class OperationPoliciesBuilder { - // stack of function stages - stack: SubtreeData[] = []; - subtrees: Map = new Map(); - current: SubtreeData | null = null; + return effect; + } - constructor( - private tg: TypeGraph, - private config: OperationPoliciesConfig, - ) {} - // set current function stage - push( - stageId: StageId, - funcTypeIdx: TypeIdx, - argPolicies: ArgPolicies, - policies: PolicyIndices[], + async #checkStageAuthorization( + { stageId, typeIdx }: StageMetadata, + getResolverResult: GetResolverResult ) { - const subtreeData = { - stageId, - funcTypeIdx, - argPolicies, - policies, - topLevel: this.stack.length === 0, - }; - this.current = subtreeData; - this.stack.push(subtreeData); - this.subtrees.set(stageId, subtreeData); - } + const effect = this.#getEffectOrDefault(typeIdx); - // set current function stage to parent function stage - pop(stageId: StageId) { - ensure(this.stack.pop()!.stageId === stageId, "unexpected: invalid state"); - const top = this.stack.pop(); - if (top == null) { - this.current == null; - } else { - this.stack.push(top); - this.current = top; - } - } + const policiesForStage = this.#stageToPolicies.get(stageId) ?? []; + const policies = []; + for (const { canonFieldName, indices } of policiesForStage) { + for (const index of indices) { - build(): OperationPolicies { - return new OperationPolicies(this.tg, this, this.config); - } -} + if (typeof index == "number") { + policies.push({ canonFieldName, index }) + } else { + const actualIndex = index[effect] ?? null; + if (actualIndex == null) { + throw new BadContext( + this.getRejectionReason(stageId, effect, [{ + name: "__deny", + concernedField: canonFieldName + }]), + ); + } + + -function* iterIndices(indices: PolicyIndices): IterableIterator { - if (typeof indices === "number") { - yield indices; - } else { - for (const idx of Object.values(indices) as number[]) { - yield idx; + policies.push({ canonFieldName, index: actualIndex }) + } + } } + + return { + effect, + res: await this.#composePolicies( + policies, + effect, + getResolverResult, + ) + }; } } diff --git a/tests/policies/policies_composition.py b/tests/policies/policies_composition.py index c4cdf5102..8ca1ac9e9 100644 --- a/tests/policies/policies_composition.py +++ b/tests/policies/policies_composition.py @@ -56,11 +56,11 @@ def ctxread(pol_name: str): ), t.struct( { - "b": t.integer() - .rename("Second") - .with_policy(ctxread("depth_4")) + "b": t.integer().with_policy( + ctxread("depth_4") + ) } - ), + ).rename("Second"), ] ).with_policy(ctxread("depth_3")) } diff --git a/tests/policies/policies_composition_test.ts b/tests/policies/policies_composition_test.ts index ebfed9d29..a6a4f3783 100644 --- a/tests/policies/policies_composition_test.ts +++ b/tests/policies/policies_composition_test.ts @@ -46,7 +46,7 @@ Meta.test("Basic composition through traversal spec", async (t) => { .withContext({ control_value: "PASS" }) - .expectErrorContains("('either') at '.simple_traversal_comp.one.two'") + .expectErrorContains(".simple_traversal_comp.one.two") .on(e); }); @@ -65,7 +65,7 @@ Meta.test("Basic composition through traversal spec", async (t) => { .withContext({ control_value: "DENY" }) - .expectErrorContains("('object') at '.simple_traversal_comp.one'") + .expectErrorContains(".simple_traversal_comp.one") .on(e); }); }); @@ -115,38 +115,36 @@ Meta.test("Basic chain composition on a single field spec", async (t) => { }); - // await t.should("have DENY ruling", async () => { - // await gql` - // query { - // single_field_comp(abc: "enter" ) { - // abc - // } - // } - // ` - // .withContext({ - // A: "PASS", - // B: "DENY", - // C: "ALLOW" - // }) - // .expectErrorContains("Authorization failed for policy 'B'") - // .on(e); - // }); + await t.should("have DENY ruling", async () => { + await gql` + query { + single_field_comp(abc: "enter" ) { + abc + } + } + ` + .withContext({ + A: "PASS", + B: "DENY", + C: "ALLOW" + }) + .expectErrorContains("Authorization failed for policy 'B'") + .on(e); + }); }); -/* + Meta.test("Traversal composition", async (t) => { const e = await t.engine("policies/policies_composition.py"); - const one = { + const noopInput = { two: { three: { a: 1, - b: 2 } } }; - - await t.should("work: traversal 1", async () => { + await t.should("have PASS acting as a no-op upon traversal (version 1)", async () => { await gql` query { traversal_comp(one: $one) { @@ -155,15 +153,15 @@ Meta.test("Traversal composition", async (t) => { three { ... on First { a } ... on Second { - b # d4 - } + b # d4 + } } # d3 } # d2 } # d1 } } ` - .withVars({ one }) + .withVars({ one: noopInput }) .withContext({ depth_1: "PASS", depth_2: "ALLOW", @@ -171,9 +169,38 @@ Meta.test("Traversal composition", async (t) => { depth_4: "PASS" }) .expectData({ - traversal_comp: { one } + traversal_comp: { + one: noopInput + } }) .on(e); }); + + await t.should("have PASS acting as a no-op upon traversal (version 1)", async () => { + await gql` + query { + traversal_comp(one: $one) { + one { + two { + three { + ... on First { a } + ... on Second { + b # d4 + } + } # d3 + } # d2 + } # d1 + } + } + ` + .withVars({ one: noopInput }) + .withContext({ + depth_1: "PASS", + depth_2: "DENY", // stop! + depth_3: "ALLOW", + depth_4: "ALLOW" + }) + .expectErrorContains(".traversal_comp.one.two") + .on(e); + }); }); -*/ From d387d0e4ab10254a1a1fc4ea3666b7c51eb41b62 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Tue, 17 Dec 2024 21:55:15 +0300 Subject: [PATCH 06/11] feat: handle union/either edgecases --- src/typegate/src/engine/planner/mod.ts | 7 +- src/typegate/src/engine/planner/policies.ts | 294 +++++++++++------- .../src/typegraphs/introspection.json | 292 +++-------------- .../src/typegraphs/prisma_migration.json | 105 ++----- src/typegate/src/typegraphs/typegate.json | 158 ++++------ src/typegraph/core/src/global_store.rs | 4 + src/typegraph/core/src/typedef/struct_.rs | 10 +- tests/policies/policies_composition.py | 11 +- tests/policies/policies_composition_test.ts | 66 +++- 9 files changed, 409 insertions(+), 538 deletions(-) diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index 51bf45db8..f5cc71dac 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -106,9 +106,14 @@ export class Planner { const orderedStageMetadata = [] as Array; for (const stage of stages) { stage.varTypes = varTypes; + const stageId = stage.id(); + if (stageId.startsWith("__schema")) { + // TODO: allow and reuse previous stage policy? + continue; + } orderedStageMetadata.push({ - stageId: stage.id(), + stageId, typeIdx: stage.props.typeIdx, isTopLevel: stage.props.parent ? false : true }); diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index acf02367f..6628aa244 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -1,6 +1,7 @@ // Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. // SPDX-License-Identifier: MPL-2.0 +import { type Logger } from "@std/log"; import { DenoRuntime } from "../../runtimes/deno/deno.ts"; import type { TypeGraph } from "../../typegraph/mod.ts"; import type { @@ -14,12 +15,21 @@ import type { import type { EffectType, PolicyIndices } from "../../typegraph/types.ts"; import { ensure } from "../../utils.ts"; import { getLogger } from "../../log.ts"; -import { getWrappedType, isFunction, isQuantifier, Type } from "../../typegraph/type_node.ts"; +import { + getWrappedType, + isEither, + isFunction, + isQuantifier, + isUnion, + Type, +} from "../../typegraph/type_node.ts"; import { BadContext } from "../../errors.ts"; export type PolicyResolverOutput = "DENY" | "ALLOW" | "PASS" | (unknown & {}); -type GetResolverResult = (polIdx: PolicyIdx, effect: EffectType) => Promise; - +type GetResolverResult = ( + polIdx: PolicyIdx, + effect: EffectType, +) => Promise; export interface StageMetadata { stageId: string; @@ -27,7 +37,6 @@ export interface StageMetadata { isTopLevel: boolean; } - interface ComposePolicyOperand { canonFieldName: string; index: PolicyIdx; @@ -48,9 +57,12 @@ export type OperationPoliciesConfig = { interface PolicyForStage { canonFieldName: string; /** Each item is either a PolicyIndicesByEffect or a number */ - indices: Array + indices: Array; } +// This is arbitrary, but must be unique in such a way that the user produced +// stages do not share the same id. +const EXPOSE_STAGE_ID = ""; export class OperationPolicies { #stageToPolicies: Map> = new Map(); @@ -61,32 +73,44 @@ export class OperationPolicies { private orderedStageMetadata: Array, private config: OperationPoliciesConfig, ) { - this.#prepareStageToPolicies(); this.#preallocateResolvers(); } - #prepareStageToPolicies() { this.#stageToPolicies = new Map(); - for (const { stageId, typeIdx: rawIdx } of this.orderedStageMetadata) { - const policies = this.#getPolicies(rawIdx); + + // Note: policies for exposed functions are hoisted on the root struct (index 0) + // If a function has a policy it overrides the definition on the root + const exposePolicies = this.#getPolicies(0); + const exposePolicyCount = exposePolicies.reduce( + (total, { indices }) => total + indices.length, + 0, + ); + this.#stageToPolicies.set(EXPOSE_STAGE_ID, exposePolicies); + + for ( + const { stageId, typeIdx: maybeWrappedIdx } of this.orderedStageMetadata + ) { + const policies = this.#getPolicies(maybeWrappedIdx); this.#stageToPolicies.set(stageId, policies); console.log("> found", stageId, policies); // top-level functions must have policies const isTopLevel = stageId.split(".").length == 1; - const policyCount = policies.reduce((total, { indices }) => total + indices.length, 0); - // FIXME: policy on function not collected? - // if (isTopLevel && policyCount === 0) { - // const details = [ - // `top-level function '${this.tg.type(rawIdx).title}'`, - // `at '${stageId}'`, - // ].join(" "); - // throw new Error( - // `No authorization policy took decision for ${details}'`, - // ); - // } + const policyCount = policies.reduce( + (total, { indices }) => total + indices.length, + exposePolicyCount, + ); + if (isTopLevel && policyCount === 0) { + const details = [ + `top-level function '${this.tg.type(maybeWrappedIdx).title}'`, + `at '${stageId}'`, + ].join(" "); + throw new Error( + `No authorization policy took decision for ${details}'`, + ); + } } } @@ -94,7 +118,7 @@ export class OperationPolicies { this.#resolvers = new Map(); const policyIndicesWithDup = Array.from(this.#stageToPolicies.values()) .map((policyPerName) => { - const indices = policyPerName.map((({ indices }) => indices)); + const indices = policyPerName.map(({ indices }) => indices); return indices.flat(); }) .flat(); @@ -126,62 +150,60 @@ export class OperationPolicies { } } + /** + * Evaluate each stage policy in traversal order + * + * - `PASS`: continue further, same as no policies + * - `ALLOW`: stops evaluation for parent, and skip any child stage + * - `DENY`: throw an error and stopping everything + */ public async authorize(context: Context, info: Info, verbose: boolean) { const logger = getLogger("policies"); - const cache = new Map(); - - const getResolverResult = async ( - polIdx: PolicyIdx, - effect: EffectType, - ): Promise => { - verbose && - logger.info( - `checking policy '${ - this.tg.policy(polIdx).name - }'[${polIdx}] with effect '${effect}'...`, - ); - if (cache.has(polIdx)) { - return cache.get(polIdx); - } - const resolver = this.#resolvers.get(polIdx); - ensure( - resolver != null, - `Could not find resolver for the policy '${ - this.tg.policy(polIdx).name - }'; effect=${effect}`, - ); - - const res = (await resolver!({ - _: { - parent: {}, - context, - info, - variables: {}, - effect: effect === "read" ? null : effect, - }, - })) as PolicyResolverOutput; - cache.set(polIdx, res); - verbose && logger.info(`> authorize: ${res}`); - return res; - }; - - const resolvedPolicyCachePerStage: Map = new Map(); - - outerIter: for (const stageMeta of this.orderedStageMetadata) { + const outputCache = new Map(); + const getResolverResult = this.#createPolicyEvaluator( + { context, info }, + outputCache, + verbose, + logger, + ); + + const resolvedPolicyCachePerStage: Map = + new Map(); + + const fakeStageMeta = { + isTopLevel: true, + stageId: EXPOSE_STAGE_ID, + typeIdx: 0, // unused, but worth to keep around + } as StageMetadata; + const stageMetaList = [fakeStageMeta, ...this.orderedStageMetadata]; + + outerIter: for (const stageMeta of stageMetaList) { const { stageId } = stageMeta; - console.log("verdict for", stageId); + for ( + const [priorStageId, verdict] of resolvedPolicyCachePerStage.entries() + ) { + const globalAllows = priorStageId == EXPOSE_STAGE_ID && + verdict == "ALLOW"; + if (globalAllows) { + break outerIter; + } - for (const [priorStageId, verdict] of resolvedPolicyCachePerStage.entries()) { - if (stageId.startsWith(priorStageId) && verdict == "ALLOW") { + console.log(" > check prior", priorStageId, "vs", stageId, verdict); + const parentAllows = stageId.startsWith(priorStageId) && + verdict == "ALLOW"; + if (parentAllows) { continue outerIter; } // elif deny => already thrown } - const { effect, res } = await this.#checkStageAuthorization(stageMeta, getResolverResult); - - switch(res.authorized) { + const { effect, res } = await this.#checkStageAuthorization( + stageMeta, + getResolverResult, + ); + + switch (res.authorized) { case "ALLOW": { resolvedPolicyCachePerStage.set(stageId, "ALLOW"); continue; @@ -194,11 +216,11 @@ export class OperationPolicies { resolvedPolicyCachePerStage.set(stageId, res.authorized); const policyNames = res.policiesFailed.map((operand) => ({ name: this.tg.policy(operand.index).name, - concernedField: operand.canonFieldName + concernedField: operand.canonFieldName, })); throw new BadContext( - this.getRejectionReason(stageId, effect, policyNames), + this.getRejectionReason(stageId, effect, policyNames), ); } } @@ -208,27 +230,79 @@ export class OperationPolicies { getRejectionReason( stageId: StageId, effect: EffectType, - policiesData: Array<{ name: string, concernedField: string }>, + policiesData: Array<{ name: string; concernedField: string }>, ): string { - const details = policiesData - .map(({ name, concernedField }) => [ - `policy '${name}'`, - `with effect '${effect}'`, - `at '.${stageId}.${concernedField}'`, - ].join(" ")); - return `Authorization failed for ${details.join("; ")}`; + const getPath = (concernedField: string) => { + if (stageId == EXPOSE_STAGE_ID) { + return [EXPOSE_STAGE_ID, concernedField].join("."); + } + return [EXPOSE_STAGE_ID, stageId, concernedField].join("."); + }; + + const detailsPerPolicy = policiesData + .map(({ name, concernedField }) => + [ + `policy '${name}'`, + `with effect '${effect}'`, + `at '${getPath(concernedField)}'`, + ].join(" ") + ); + return `Authorization failed for ${detailsPerPolicy.join("; ")}`; } - /** + #createPolicyEvaluator( + partialResolverInput: { context: Context; info: Info }, + outputCache: Map, + verbose: boolean, + logger: Logger, + ): GetResolverResult { + return async ( + polIdx: PolicyIdx, + effect: EffectType, + ): Promise => { + verbose && + logger.info( + `checking policy '${ + this.tg.policy(polIdx).name + }'[${polIdx}] with effect '${effect}'...`, + ); + if (outputCache.has(polIdx)) { + return outputCache.get(polIdx); + } + + const resolver = this.#resolvers.get(polIdx); + ensure( + resolver != null, + `Could not find resolver for the policy '${ + this.tg.policy(polIdx).name + }'; effect=${effect}`, + ); + + const res = (await resolver!({ + _: { + parent: {}, + context: partialResolverInput.context, + info: partialResolverInput.info, + variables: {}, + effect: effect === "read" ? null : effect, + }, + })) as PolicyResolverOutput; + outputCache.set(polIdx, res); + verbose && logger.info(`> authorize: ${res}`); + return res; + }; + } + + /** * A single type may hold multiple policies - * - * * `ALLOW`: ALLOW & P = P - * * `DENY`: DENY & P = DENY - * + * + * - `ALLOW`: ALLOW & P = P + * - `DENY`: DENY & P = DENY + * * DENY and ALLOW combine just like booleans with the AND gate - * + * * PASS does not participate. - **/ + */ async #composePolicies( policies: Array, effect: EffectType, @@ -236,24 +310,31 @@ export class OperationPolicies { ): Promise { const operands = []; const deniersIdx = []; - for (const operand of policies) { - const res = await getResolverResult(operand.index, effect); - - switch(res) { + for (const policyOperand of policies) { + const res = await getResolverResult(policyOperand.index, effect); + + switch (res) { case "ALLOW": { operands.push(true); break; } case "DENY": { + // We can fail fast here with a throw + // but we can assume policies are reused accross + // types (so high cache hit) operands.push(false); - deniersIdx.push(operand); + deniersIdx.push(policyOperand); break; } case "PASS": { continue; } default: { - throw new Error(`Could not take decision on value: ${JSON.stringify(res)}, policy must return either "ALLOW", "DENY" or "PASS"`) + throw new Error( + `Could not take decision on value: ${ + JSON.stringify(res) + }, policy must return either "ALLOW", "DENY" or "PASS"`, + ); } } } @@ -264,7 +345,7 @@ export class OperationPolicies { if (operands.every((_bool) => _bool)) { return { authorized: "ALLOW" }; } else { - return { authorized: "DENY", policiesFailed: deniersIdx } + return { authorized: "DENY", policiesFailed: deniersIdx }; } } } @@ -284,15 +365,24 @@ export class OperationPolicies { nestedSchema = this.tg.type(nestedTypeIdx); } + if (isEither(nestedSchema) || isUnion(nestedSchema)) { + const variantIndices = isEither(nestedSchema) + ? nestedSchema.oneOf + : nestedSchema.anyOf; + const policies = variantIndices.map((idx) => this.#getPolicies(idx)) + .flat(); + return policies; + } + let out: Record> = {}; - if(nestedSchema.type == "object") { + if (nestedSchema.type == "object") { out = nestedSchema.policies ?? {}; } return Object.entries(out).map(([k, v]) => ({ canonFieldName: k, - indices: v - })) + indices: v, + })); } #getEffectOrDefault(typeIdx: number) { @@ -306,10 +396,9 @@ export class OperationPolicies { return effect; } - async #checkStageAuthorization( - { stageId, typeIdx }: StageMetadata, - getResolverResult: GetResolverResult + { stageId, typeIdx }: StageMetadata, + getResolverResult: GetResolverResult, ) { const effect = this.#getEffectOrDefault(typeIdx); @@ -317,23 +406,20 @@ export class OperationPolicies { const policies = []; for (const { canonFieldName, indices } of policiesForStage) { for (const index of indices) { - if (typeof index == "number") { - policies.push({ canonFieldName, index }) + policies.push({ canonFieldName, index }); } else { const actualIndex = index[effect] ?? null; if (actualIndex == null) { throw new BadContext( this.getRejectionReason(stageId, effect, [{ name: "__deny", - concernedField: canonFieldName + concernedField: canonFieldName, }]), ); } - - - policies.push({ canonFieldName, index: actualIndex }) + policies.push({ canonFieldName, index: actualIndex }); } } } @@ -344,7 +430,7 @@ export class OperationPolicies { policies, effect, getResolverResult, - ) + ), }; } } diff --git a/src/typegate/src/typegraphs/introspection.json b/src/typegate/src/typegraphs/introspection.json index 8668f4568..66947140e 100644 --- a/src/typegate/src/typegraphs/introspection.json +++ b/src/typegate/src/typegraphs/introspection.json @@ -5,7 +5,7 @@ "title": "introspection", "properties": { "__type": 1, - "__schema": 45 + "__schema": 26 }, "id": [], "required": [ @@ -59,14 +59,14 @@ "properties": { "kind": 6, "name": 7, - "description": 8, - "specifiedByURL": 9, - "fields": 10, - "interfaces": 27, - "possibleTypes": 29, - "enumValues": 31, - "inputFields": 39, - "ofType": 44 + "description": 7, + "specifiedByURL": 7, + "fields": 8, + "interfaces": 18, + "possibleTypes": 18, + "enumValues": 20, + "inputFields": 24, + "ofType": 4 }, "id": [], "required": [], @@ -103,23 +103,11 @@ "item": 3, "default_value": null }, - { - "type": "optional", - "title": "type_description_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "type_specifiedByURL_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, { "type": "function", "title": "type_fields_fn", - "input": 11, - "output": 14, + "input": 9, + "output": 12, "runtimeConfig": null, "materializer": 1, "rate_weight": null, @@ -129,7 +117,7 @@ "type": "object", "title": "type_fields_fn_input", "properties": { - "includeDeprecated": 12 + "includeDeprecated": 10 }, "id": [], "required": [], @@ -140,7 +128,7 @@ { "type": "optional", "title": "type_fields_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "item": 13, + "item": 11, "default_value": null }, { @@ -150,24 +138,24 @@ { "type": "optional", "title": "type_fields_fn_output", - "item": 15, + "item": 13, "default_value": null }, { "type": "list", "title": "type_fields_fn_output", - "items": 16 + "items": 14 }, { "type": "object", "title": "field", "properties": { "name": 3, - "description": 17, - "args": 18, + "description": 7, + "args": 15, "type": 5, - "isDeprecated": 13, - "deprecationReason": 26 + "isDeprecated": 11, + "deprecationReason": 7 }, "id": [], "required": [], @@ -180,55 +168,31 @@ "deprecationReason": [] } }, - { - "type": "optional", - "title": "field_description_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, { "type": "function", "title": "field_args_fn", - "input": 19, - "output": 21, + "input": 9, + "output": 16, "runtimeConfig": null, "materializer": 1, "rate_weight": null, "rate_calls": false }, - { - "type": "object", - "title": "field_args_fn_input", - "properties": { - "includeDeprecated": 20 - }, - "id": [], - "required": [], - "policies": { - "includeDeprecated": [] - } - }, - { - "type": "optional", - "title": "field_args_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "item": 13, - "default_value": null - }, { "type": "list", "title": "field_args_fn_output", - "items": 22 + "items": 17 }, { "type": "object", "title": "input_value", "properties": { "name": 3, - "description": 23, + "description": 7, "type": 5, - "defaultValue": 24, - "isDeprecated": 13, - "deprecationReason": 25 + "defaultValue": 7, + "isDeprecated": 11, + "deprecationReason": 7 }, "id": [], "required": [], @@ -241,34 +205,10 @@ "deprecationReason": [] } }, - { - "type": "optional", - "title": "input_value_description_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "input_value_defaultValue_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "input_value_deprecationReason_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "field_deprecationReason_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, { "type": "optional", "title": "type_interfaces_type_interfaces_type_list_optional", - "item": 28, + "item": 19, "default_value": null }, { @@ -276,64 +216,35 @@ "title": "type_interfaces_type_list", "items": 5 }, - { - "type": "optional", - "title": "type_possibleTypes_type_possibleTypes_type_list_optional", - "item": 30, - "default_value": null - }, - { - "type": "list", - "title": "type_possibleTypes_type_list", - "items": 5 - }, { "type": "function", "title": "type_enumValues_fn", - "input": 32, - "output": 34, + "input": 9, + "output": 21, "runtimeConfig": null, "materializer": 1, "rate_weight": null, "rate_calls": false }, - { - "type": "object", - "title": "type_enumValues_fn_input", - "properties": { - "includeDeprecated": 33 - }, - "id": [], - "required": [], - "policies": { - "includeDeprecated": [] - } - }, - { - "type": "optional", - "title": "type_enumValues_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "item": 13, - "default_value": null - }, { "type": "optional", "title": "type_enumValues_fn_output", - "item": 35, + "item": 22, "default_value": null }, { "type": "list", "title": "type_enumValues_fn_output", - "items": 36 + "items": 23 }, { "type": "object", "title": "enum_value", "properties": { "name": 3, - "description": 37, - "isDeprecated": 13, - "deprecationReason": 38 + "description": 7, + "isDeprecated": 11, + "deprecationReason": 7 }, "id": [], "required": [], @@ -344,68 +255,27 @@ "deprecationReason": [] } }, - { - "type": "optional", - "title": "enum_value_description_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "enum_value_deprecationReason_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, { "type": "function", "title": "type_inputFields_fn", - "input": 40, - "output": 42, + "input": 9, + "output": 25, "runtimeConfig": null, "materializer": 1, "rate_weight": null, "rate_calls": false }, - { - "type": "object", - "title": "type_inputFields_fn_input", - "properties": { - "includeDeprecated": 41 - }, - "id": [], - "required": [], - "policies": { - "includeDeprecated": [] - } - }, - { - "type": "optional", - "title": "type_inputFields_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "item": 13, - "default_value": null - }, { "type": "optional", "title": "type_inputFields_fn_output", - "item": 43, - "default_value": null - }, - { - "type": "list", - "title": "type_inputFields_fn_output", - "items": 22 - }, - { - "type": "optional", - "title": "type_ofType_type_optional", - "item": 5, + "item": 16, "default_value": null }, { "type": "function", "title": "root___schema_fn", - "input": 46, - "output": 47, + "input": 27, + "output": 28, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -422,12 +292,12 @@ "type": "object", "title": "schema", "properties": { - "description": 48, - "types": 49, + "description": 7, + "types": 19, "queryType": 5, - "mutationType": 50, - "subscriptionType": 51, - "directives": 52 + "mutationType": 4, + "subscriptionType": 4, + "directives": 29 }, "id": [], "required": [], @@ -440,43 +310,20 @@ "directives": [] } }, - { - "type": "optional", - "title": "schema_description_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "list", - "title": "schema_types_type_list", - "items": 5 - }, - { - "type": "optional", - "title": "schema_mutationType_type_optional", - "item": 5, - "default_value": null - }, - { - "type": "optional", - "title": "schema_subscriptionType_type_optional", - "item": 5, - "default_value": null - }, { "type": "list", "title": "schema_directives_directive_list", - "items": 53 + "items": 30 }, { "type": "object", "title": "directive", "properties": { "name": 3, - "description": 54, - "isRepeatable": 13, - "locations": 55, - "args": 57 + "description": 7, + "isRepeatable": 11, + "locations": 31, + "args": 15 }, "id": [], "required": [], @@ -488,16 +335,10 @@ "args": [] } }, - { - "type": "optional", - "title": "directive_description_root___type_fn_input_name_string_optional", - "item": 3, - "default_value": null - }, { "type": "list", "title": "directive_locations_directive_location_list", - "items": 56 + "items": 32 }, { "type": "string", @@ -523,39 +364,6 @@ "\"INPUT_OBJECT\"", "\"INPUT_FIELD_DEFINITION\"" ] - }, - { - "type": "function", - "title": "directive_args_fn", - "input": 58, - "output": 60, - "runtimeConfig": null, - "materializer": 1, - "rate_weight": null, - "rate_calls": false - }, - { - "type": "object", - "title": "directive_args_fn_input", - "properties": { - "includeDeprecated": 59 - }, - "id": [], - "required": [], - "policies": { - "includeDeprecated": [] - } - }, - { - "type": "optional", - "title": "directive_args_fn_input_includeDeprecated_type_fields_fn_input_includeDeprecated_boolean_optional", - "item": 13, - "default_value": null - }, - { - "type": "list", - "title": "directive_args_fn_output", - "items": 22 } ], "materializers": [ diff --git a/src/typegate/src/typegraphs/prisma_migration.json b/src/typegate/src/typegraphs/prisma_migration.json index d93c6d7f9..c6803a017 100644 --- a/src/typegate/src/typegraphs/prisma_migration.json +++ b/src/typegate/src/typegraphs/prisma_migration.json @@ -5,10 +5,10 @@ "title": "typegate/prisma_migration", "properties": { "diff": 1, - "apply": 8, - "create": 14, - "deploy": 21, - "reset": 27 + "apply": 7, + "create": 11, + "deploy": 14, + "reset": 18 }, "id": [], "required": [ @@ -80,7 +80,7 @@ "type": "object", "title": "root_diff_fn_output", "properties": { - "diff": 7, + "diff": 4, "runtimeName": 3 }, "id": [], @@ -90,17 +90,11 @@ "runtimeName": [] } }, - { - "type": "optional", - "title": "root_diff_fn_output_diff_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, { "type": "function", "title": "root_apply_fn", - "input": 9, - "output": 12, + "input": 8, + "output": 9, "runtimeConfig": null, "materializer": 2, "rate_weight": null, @@ -111,8 +105,8 @@ "title": "root_apply_fn_input", "properties": { "typegraph": 3, - "runtime": 10, - "migrations": 11, + "runtime": 4, + "migrations": 4, "resetDatabase": 5 }, "id": [], @@ -124,24 +118,12 @@ "resetDatabase": [] } }, - { - "type": "optional", - "title": "root_apply_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "root_apply_fn_input_migrations_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, { "type": "object", "title": "root_apply_fn_output", "properties": { "databaseReset": 5, - "appliedMigrations": 13 + "appliedMigrations": 10 }, "id": [], "required": [], @@ -158,8 +140,8 @@ { "type": "function", "title": "root_create_fn", - "input": 15, - "output": 18, + "input": 12, + "output": 13, "runtimeConfig": null, "materializer": 3, "rate_weight": null, @@ -170,10 +152,10 @@ "title": "root_create_fn_input", "properties": { "typegraph": 3, - "runtime": 16, + "runtime": 4, "name": 3, "apply": 5, - "migrations": 17 + "migrations": 4 }, "id": [], "required": [], @@ -185,25 +167,13 @@ "migrations": [] } }, - { - "type": "optional", - "title": "root_create_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "root_create_fn_input_migrations_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, { "type": "object", "title": "root_create_fn_output", "properties": { "createdMigrationName": 3, - "applyError": 19, - "migrations": 20, + "applyError": 4, + "migrations": 4, "runtimeName": 3 }, "id": [], @@ -215,23 +185,11 @@ "runtimeName": [] } }, - { - "type": "optional", - "title": "root_create_fn_output_applyError_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, - { - "type": "optional", - "title": "root_create_fn_output_migrations_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, { "type": "function", "title": "root_deploy_fn", - "input": 22, - "output": 24, + "input": 15, + "output": 16, "runtimeConfig": null, "materializer": 4, "rate_weight": null, @@ -242,7 +200,7 @@ "title": "root_deploy_fn_input", "properties": { "typegraph": 3, - "runtime": 23, + "runtime": 4, "migrations": 3 }, "id": [], @@ -253,18 +211,12 @@ "migrations": [] } }, - { - "type": "optional", - "title": "root_deploy_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null - }, { "type": "object", "title": "root_deploy_fn_output", "properties": { - "migrationCount": 25, - "appliedMigrations": 26 + "migrationCount": 17, + "appliedMigrations": 10 }, "id": [], "required": [], @@ -277,15 +229,10 @@ "type": "integer", "title": "root_deploy_fn_output_migrationCount_integer" }, - { - "type": "list", - "title": "root_deploy_fn_output_appliedMigrations_root_diff_fn_input_typegraph_string_list", - "items": 3 - }, { "type": "function", "title": "root_reset_fn", - "input": 28, + "input": 19, "output": 5, "runtimeConfig": null, "materializer": 5, @@ -297,7 +244,7 @@ "title": "root_reset_fn_input", "properties": { "typegraph": 3, - "runtime": 29 + "runtime": 4 }, "id": [], "required": [], @@ -305,12 +252,6 @@ "typegraph": [], "runtime": [] } - }, - { - "type": "optional", - "title": "root_reset_fn_input_runtime_root_diff_fn_input_typegraph_string_optional", - "item": 3, - "default_value": null } ], "materializers": [ diff --git a/src/typegate/src/typegraphs/typegate.json b/src/typegate/src/typegraphs/typegate.json index 46de8eb8c..022664b41 100644 --- a/src/typegate/src/typegraphs/typegate.json +++ b/src/typegate/src/typegraphs/typegate.json @@ -9,13 +9,13 @@ "addTypegraph": 12, "removeTypegraphs": 22, "argInfoByPath": 26, - "findAvailableOperations": 40, - "findPrismaModels": 48, - "execRawPrismaRead": 54, - "execRawPrismaCreate": 66, - "execRawPrismaUpdate": 67, - "execRawPrismaDelete": 68, - "queryPrismaModel": 69 + "findAvailableOperations": 37, + "findPrismaModels": 45, + "execRawPrismaRead": 51, + "execRawPrismaCreate": 62, + "execRawPrismaUpdate": 63, + "execRawPrismaDelete": 64, + "queryPrismaModel": 65 }, "id": [], "required": [ @@ -307,7 +307,7 @@ "type": "function", "title": "root_argInfoByPath_fn", "input": 27, - "output": 30, + "output": 29, "runtimeConfig": null, "materializer": 6, "rate_weight": null, @@ -333,18 +333,13 @@ }, { "type": "list", - "title": "root_argInfoByPath_fn_input_argPaths_root_argInfoByPath_fn_input_argPaths_Typegraph_name_string_list_list", - "items": 29 - }, - { - "type": "list", - "title": "root_argInfoByPath_fn_input_argPaths_Typegraph_name_string_list", - "items": 5 + "title": "root_argInfoByPath_fn_input_argPaths_root_removeTypegraphs_fn_input_names_Typegraph_name_string_list_list", + "items": 24 }, { "type": "list", "title": "root_argInfoByPath_fn_output", - "items": 31 + "items": 30 }, { "type": "object", @@ -353,11 +348,11 @@ "optional": 25, "title": 5, "type": 5, - "enum": 32, - "default": 34, - "format": 35, - "policies": 36, - "fields": 37 + "enum": 31, + "default": 21, + "format": 33, + "policies": 24, + "fields": 34 }, "id": [], "required": [], @@ -375,7 +370,7 @@ { "type": "optional", "title": "TypeInfo_enum_TypeInfo_enum_root_addTypegraph_fn_input_fromString_string_json_list_optional", - "item": 33, + "item": 32, "default_value": null }, { @@ -383,40 +378,29 @@ "title": "TypeInfo_enum_root_addTypegraph_fn_input_fromString_string_json_list", "items": 14 }, - { - "type": "optional", - "title": "TypeInfo_default_root_addTypegraph_fn_input_fromString_string_json_optional", - "item": 14, - "default_value": null - }, { "type": "optional", "title": "TypeInfo_format_Typegraph_name_string_optional", "item": 5, "default_value": null }, - { - "type": "list", - "title": "TypeInfo_policies_Typegraph_name_string_list", - "items": 5 - }, { "type": "optional", "title": "TypeInfo_fields_TypeInfo_fields_TypeInfo_fields_struct_list_optional", - "item": 38, + "item": 35, "default_value": null }, { "type": "list", "title": "TypeInfo_fields_TypeInfo_fields_struct_list", - "items": 39 + "items": 36 }, { "type": "object", "title": "TypeInfo_fields_struct", "properties": { - "subPath": 29, - "termNode": 31 + "subPath": 24, + "termNode": 30 }, "id": [], "required": [], @@ -428,8 +412,8 @@ { "type": "function", "title": "root_findAvailableOperations_fn", - "input": 41, - "output": 42, + "input": 38, + "output": 39, "runtimeConfig": null, "materializer": 7, "rate_weight": null, @@ -450,17 +434,17 @@ { "type": "list", "title": "root_findAvailableOperations_fn_output", - "items": 43 + "items": 40 }, { "type": "object", "title": "OperationInfo", "properties": { "name": 5, - "type": 44, - "inputs": 45, - "output": 31, - "outputItem": 47 + "type": 41, + "inputs": 42, + "output": 30, + "outputItem": 44 }, "id": [], "required": [], @@ -483,14 +467,14 @@ { "type": "list", "title": "OperationInfo_inputs_OperationInfo_inputs_struct_list", - "items": 46 + "items": 43 }, { "type": "object", "title": "OperationInfo_inputs_struct", "properties": { "name": 5, - "type": 31 + "type": 30 }, "id": [], "required": [], @@ -502,14 +486,14 @@ { "type": "optional", "title": "OperationInfo_outputItem_TypeInfo_optional", - "item": 31, + "item": 30, "default_value": null }, { "type": "function", "title": "root_findPrismaModels_fn", - "input": 41, - "output": 49, + "input": 38, + "output": 46, "runtimeConfig": null, "materializer": 8, "rate_weight": null, @@ -518,7 +502,7 @@ { "type": "list", "title": "root_findPrismaModels_fn_output", - "items": 50 + "items": 47 }, { "type": "object", @@ -526,7 +510,7 @@ "properties": { "name": 5, "runtime": 5, - "fields": 51 + "fields": 48 }, "id": [], "required": [], @@ -539,7 +523,7 @@ { "type": "list", "title": "PrismaModelInfo_fields_PrismaModelInfo_fields_struct_list", - "items": 52 + "items": 49 }, { "type": "object", @@ -547,7 +531,7 @@ "properties": { "name": 5, "as_id": 25, - "type": 53 + "type": 50 }, "id": [], "required": [], @@ -564,10 +548,10 @@ "optional": 25, "title": 5, "type": 5, - "enum": 32, - "default": 34, - "format": 35, - "policies": 36 + "enum": 31, + "default": 21, + "format": 33, + "policies": 24 }, "id": [], "required": [], @@ -584,7 +568,7 @@ { "type": "function", "title": "root_execRawPrismaRead_fn", - "input": 55, + "input": 52, "output": 14, "runtimeConfig": null, "materializer": 9, @@ -597,7 +581,7 @@ "properties": { "typegraph": 5, "runtime": 5, - "query": 56 + "query": 53 }, "id": [], "required": [], @@ -611,16 +595,16 @@ "type": "either", "title": "PrismaQuery", "oneOf": [ - 57, - 60 + 54, + 56 ] }, { "type": "object", "title": "PrismaSingleQuery", "properties": { - "modelName": 58, - "action": 59, + "modelName": 33, + "action": 55, "query": 14 }, "id": [], @@ -631,12 +615,6 @@ "query": [] } }, - { - "type": "optional", - "title": "PrismaSingleQuery_modelName_Typegraph_name_string_optional", - "item": 5, - "default_value": null - }, { "type": "string", "title": "PrismaQueryTag", @@ -665,8 +643,8 @@ "type": "object", "title": "PrismaBatchQuery", "properties": { - "batch": 61, - "transaction": 62 + "batch": 57, + "transaction": 58 }, "id": [], "required": [], @@ -678,19 +656,19 @@ { "type": "list", "title": "PrismaBatchQuery_batch_PrismaSingleQuery_list", - "items": 57 + "items": 54 }, { "type": "optional", "title": "PrismaBatchQuery_transaction_PrismaBatchQuery_transaction_struct_optional", - "item": 63, + "item": 59, "default_value": null }, { "type": "object", "title": "PrismaBatchQuery_transaction_struct", "properties": { - "isolationLevel": 64 + "isolationLevel": 60 }, "id": [], "required": [], @@ -701,7 +679,7 @@ { "type": "optional", "title": "PrismaBatchQuery_transaction_struct_isolationLevel_PrismaBatchQuery_transaction_struct_isolationLevel_string_enum_optional", - "item": 65, + "item": 61, "default_value": null }, { @@ -721,7 +699,7 @@ { "type": "function", "title": "root_execRawPrismaCreate_fn", - "input": 55, + "input": 52, "output": 14, "runtimeConfig": null, "materializer": 10, @@ -731,7 +709,7 @@ { "type": "function", "title": "root_execRawPrismaUpdate_fn", - "input": 55, + "input": 52, "output": 14, "runtimeConfig": null, "materializer": 11, @@ -741,7 +719,7 @@ { "type": "function", "title": "root_execRawPrismaDelete_fn", - "input": 55, + "input": 52, "output": 14, "runtimeConfig": null, "materializer": 12, @@ -751,8 +729,8 @@ { "type": "function", "title": "root_queryPrismaModel_fn", - "input": 70, - "output": 72, + "input": 66, + "output": 68, "runtimeConfig": null, "materializer": 13, "rate_weight": null, @@ -765,8 +743,8 @@ "typegraph": 5, "runtime": 5, "model": 5, - "offset": 71, - "limit": 71 + "offset": 67, + "limit": 67 }, "id": [], "required": [], @@ -786,9 +764,9 @@ "type": "object", "title": "root_queryPrismaModel_fn_output", "properties": { - "fields": 73, - "rowCount": 71, - "data": 74 + "fields": 48, + "rowCount": 67, + "data": 32 }, "id": [], "required": [], @@ -797,16 +775,6 @@ "rowCount": [], "data": [] } - }, - { - "type": "list", - "title": "root_queryPrismaModel_fn_output_fields_PrismaModelInfo_fields_struct_list", - "items": 52 - }, - { - "type": "list", - "title": "root_queryPrismaModel_fn_output_data_root_addTypegraph_fn_input_fromString_string_json_list", - "items": 14 } ], "materializers": [ diff --git a/src/typegraph/core/src/global_store.rs b/src/typegraph/core/src/global_store.rs index 6d20142eb..700770420 100644 --- a/src/typegraph/core/src/global_store.rs +++ b/src/typegraph/core/src/global_store.rs @@ -485,6 +485,10 @@ impl TypeId { Ok(matches!(self.as_xdef()?.type_def, TypeDef::Func(_))) } + pub fn is_struct(&self) -> Result { + Ok(matches!(self.as_xdef()?.type_def, TypeDef::Struct(_))) + } + pub fn resolve_quant(&self) -> Result { let type_id = *self; match type_id.as_xdef()?.type_def { diff --git a/src/typegraph/core/src/typedef/struct_.rs b/src/typegraph/core/src/typedef/struct_.rs index 36993ad1b..3f7338e1f 100644 --- a/src/typegraph/core/src/typedef/struct_.rs +++ b/src/typegraph/core/src/typedef/struct_.rs @@ -157,12 +157,18 @@ pub fn extend_policy_chain(chain: &mut Vec, type_id: TypeId) -> Resu } TypeDef::Union(inner) => { for tpe_id in inner.data.variants.iter() { - extend_policy_chain(chain, TypeId(*tpe_id))?; + let tpe = TypeId(*tpe_id); + if !tpe.is_struct()? { + extend_policy_chain(chain, tpe)?; + } } } TypeDef::Either(inner) => { for tpe_id in inner.data.variants.iter() { - extend_policy_chain(chain, TypeId(*tpe_id))?; + let tpe = TypeId(*tpe_id); + if !tpe.is_struct()? { + extend_policy_chain(chain, tpe)?; + } } } TypeDef::Func(inner) => { diff --git a/tests/policies/policies_composition.py b/tests/policies/policies_composition.py index 8ca1ac9e9..2cff51aa9 100644 --- a/tests/policies/policies_composition.py +++ b/tests/policies/policies_composition.py @@ -49,6 +49,9 @@ def ctxread(pol_name: str): { "two": t.struct( { + # Note: + # The policy on each variant is not hoisted + # on the parent struct "three": t.either( [ t.struct({"a": t.integer()}).rename( @@ -56,8 +59,12 @@ def ctxread(pol_name: str): ), t.struct( { - "b": t.integer().with_policy( - ctxread("depth_4") + "b": t.struct( + { + "c": t.integer().with_policy( + ctxread("depth_4") + ) + } ) } ).rename("Second"), diff --git a/tests/policies/policies_composition_test.ts b/tests/policies/policies_composition_test.ts index a6a4f3783..9d28dfdf4 100644 --- a/tests/policies/policies_composition_test.ts +++ b/tests/policies/policies_composition_test.ts @@ -46,7 +46,7 @@ Meta.test("Basic composition through traversal spec", async (t) => { .withContext({ control_value: "PASS" }) - .expectErrorContains(".simple_traversal_comp.one.two") + .expectErrorContains("'.simple_traversal_comp.one.two'") .on(e); }); @@ -65,7 +65,7 @@ Meta.test("Basic composition through traversal spec", async (t) => { .withContext({ control_value: "DENY" }) - .expectErrorContains(".simple_traversal_comp.one") + .expectErrorContains("'.simple_traversal_comp.one'") .on(e); }); }); @@ -137,13 +137,24 @@ Meta.test("Basic chain composition on a single field spec", async (t) => { Meta.test("Traversal composition", async (t) => { const e = await t.engine("policies/policies_composition.py"); - const noopInput = { + const inputA = { two: { three: { a: 1, } } }; + + const inputB = { + two: { + three: { + b: { + c: 2 + }, + } + } + }; + await t.should("have PASS acting as a no-op upon traversal (version 1)", async () => { await gql` query { @@ -153,7 +164,9 @@ Meta.test("Traversal composition", async (t) => { three { ... on First { a } ... on Second { - b # d4 + b { + c # d4 + } } } # d3 } # d2 @@ -161,7 +174,7 @@ Meta.test("Traversal composition", async (t) => { } } ` - .withVars({ one: noopInput }) + .withVars({ one: inputA }) .withContext({ depth_1: "PASS", depth_2: "ALLOW", @@ -170,13 +183,13 @@ Meta.test("Traversal composition", async (t) => { }) .expectData({ traversal_comp: { - one: noopInput + one: inputA } }) .on(e); }); - await t.should("have PASS acting as a no-op upon traversal (version 1)", async () => { + await t.should("have PASS acting as a no-op upon traversal (version 2)", async () => { await gql` query { traversal_comp(one: $one) { @@ -185,7 +198,9 @@ Meta.test("Traversal composition", async (t) => { three { ... on First { a } ... on Second { - b # d4 + b { + c # d4 + } } } # d3 } # d2 @@ -193,14 +208,45 @@ Meta.test("Traversal composition", async (t) => { } } ` - .withVars({ one: noopInput }) + .withVars({ one: inputA }) .withContext({ depth_1: "PASS", depth_2: "DENY", // stop! depth_3: "ALLOW", depth_4: "ALLOW" }) - .expectErrorContains(".traversal_comp.one.two") + .expectErrorContains("'.traversal_comp.one.two'") + .on(e); + }); + + + await t.should("DENY when a protected field on a either variant is encountered", async () => { + await gql` + query { + traversal_comp(one: $one) { + one { + two { + three { + ... on First { a } + ... on Second { + b { + c # d4 + } + } + } # d3 + } # d2 + } # d1 + } + } + ` + .withVars({ one: inputB }) + .withContext({ + depth_1: "PASS", + depth_2: "PASS", + depth_3: "PASS", + depth_4: "DENY" + }) + .expectErrorContains("'.traversal_comp.one.two.three$Second.b.c'") .on(e); }); }); From 60149e2f31d679cbcd4bbcb399ecd5288f412136 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Wed, 18 Dec 2024 22:01:28 +0300 Subject: [PATCH 07/11] feat: reuse effect on parent materializer + exclude non selected fields --- src/typegate/src/engine/planner/mod.ts | 3 +- src/typegate/src/engine/planner/policies.ts | 86 ++++++++++++++------- tests/policies/effects_py.py | 7 +- tests/policies/policies_composition.py | 77 +++++++++--------- tests/policies/policies_composition_test.ts | 8 +- tests/policies/policies_test.ts | 17 +--- 6 files changed, 113 insertions(+), 85 deletions(-) diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index f5cc71dac..72c89e329 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -115,7 +115,8 @@ export class Planner { orderedStageMetadata.push({ stageId, typeIdx: stage.props.typeIdx, - isTopLevel: stage.props.parent ? false : true + isTopLevel: stage.props.parent ? false : true, + node: stage.props.node // actual non aliased name }); } diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index 6628aa244..235d00119 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -35,6 +35,7 @@ export interface StageMetadata { stageId: string; typeIdx: TypeIdx; isTopLevel: boolean; + node: string; } interface ComposePolicyOperand { @@ -83,26 +84,23 @@ export class OperationPolicies { // Note: policies for exposed functions are hoisted on the root struct (index 0) // If a function has a policy it overrides the definition on the root const exposePolicies = this.#getPolicies(0); - const exposePolicyCount = exposePolicies.reduce( - (total, { indices }) => total + indices.length, - 0, - ); + const funcWithPolicies = exposePolicies + .filter(({ indices }) => indices.length > 0) + .map(({ canonFieldName }) => canonFieldName); + this.#stageToPolicies.set(EXPOSE_STAGE_ID, exposePolicies); for ( - const { stageId, typeIdx: maybeWrappedIdx } of this.orderedStageMetadata + const { stageId, typeIdx: maybeWrappedIdx, node } of this + .orderedStageMetadata ) { const policies = this.#getPolicies(maybeWrappedIdx); this.#stageToPolicies.set(stageId, policies); - console.log("> found", stageId, policies); + // console.log("> found", stageId, policies, node, this.#findSelectedFields(stageId)); // top-level functions must have policies const isTopLevel = stageId.split(".").length == 1; - const policyCount = policies.reduce( - (total, { indices }) => total + indices.length, - exposePolicyCount, - ); - if (isTopLevel && policyCount === 0) { + if (isTopLevel && !funcWithPolicies.includes(node)) { const details = [ `top-level function '${this.tg.type(maybeWrappedIdx).title}'`, `at '${stageId}'`, @@ -174,12 +172,21 @@ export class OperationPolicies { const fakeStageMeta = { isTopLevel: true, stageId: EXPOSE_STAGE_ID, - typeIdx: 0, // unused, but worth to keep around + typeIdx: 0, } as StageMetadata; const stageMetaList = [fakeStageMeta, ...this.orderedStageMetadata]; - outerIter: for (const stageMeta of stageMetaList) { - const { stageId } = stageMeta; + let activeEffect = this.#getEffectOrNull(fakeStageMeta.typeIdx) ?? "read"; // root + + outerIter: for (const { stageId, typeIdx } of stageMetaList) { + const newEffect = this.#getEffectOrNull(typeIdx); + if (newEffect != null) { + activeEffect = newEffect; + } + console.log( + ` > stage ${stageId} :: ${activeEffect}`, + resolvedPolicyCachePerStage.get(stageId) ?? "", + ); for ( const [priorStageId, verdict] of resolvedPolicyCachePerStage.entries() @@ -190,7 +197,6 @@ export class OperationPolicies { break outerIter; } - console.log(" > check prior", priorStageId, "vs", stageId, verdict); const parentAllows = stageId.startsWith(priorStageId) && verdict == "ALLOW"; if (parentAllows) { @@ -198,8 +204,9 @@ export class OperationPolicies { } // elif deny => already thrown } - const { effect, res } = await this.#checkStageAuthorization( - stageMeta, + const res = await this.#checkStageAuthorization( + stageId, + activeEffect, getResolverResult, ); @@ -220,7 +227,7 @@ export class OperationPolicies { })); throw new BadContext( - this.getRejectionReason(stageId, effect, policyNames), + this.getRejectionReason(stageId, activeEffect, policyNames), ); } } @@ -339,6 +346,12 @@ export class OperationPolicies { } } + // console.info( + // "Composing", + // effect, + // policies.map((p) => [p.canonFieldName, p.index]), + // ); + if (operands.length == 0) { return { authorized: "PASS" }; } else { @@ -385,8 +398,8 @@ export class OperationPolicies { })); } - #getEffectOrDefault(typeIdx: number) { - let effect = "read" as EffectType; + #getEffectOrNull(typeIdx: number) { + let effect = null; const node = this.tg.type(typeIdx); if (isFunction(node)) { const matIdx = this.tg.type(typeIdx, Type.FUNCTION).materializer; @@ -397,14 +410,20 @@ export class OperationPolicies { } async #checkStageAuthorization( - { stageId, typeIdx }: StageMetadata, + stageId: string, + effect: EffectType, getResolverResult: GetResolverResult, ) { - const effect = this.#getEffectOrDefault(typeIdx); + const selectedFields = this.#findSelectedFields(stageId); const policiesForStage = this.#stageToPolicies.get(stageId) ?? []; const policies = []; for (const { canonFieldName, indices } of policiesForStage) { + // Note: canonFieldName is the field on the type (but not the alias if any!) + if (!selectedFields.includes(canonFieldName)) { + continue; + } + for (const index of indices) { if (typeof index == "number") { policies.push({ canonFieldName, index }); @@ -424,13 +443,22 @@ export class OperationPolicies { } } - return { + return await this.#composePolicies( + policies, effect, - res: await this.#composePolicies( - policies, - effect, - getResolverResult, - ), - }; + getResolverResult, + ); + } + + #findSelectedFields(targetStageId: string) { + return this.orderedStageMetadata.map(({ stageId, node }) => { + const chunks = stageId.split("."); + const parent = chunks.slice(0, -1).join("."); + if (parent == "" && targetStageId == EXPOSE_STAGE_ID) { + return node; + } + + return targetStageId == parent ? node : null; + }).filter((name) => name != null); } } diff --git a/tests/policies/effects_py.py b/tests/policies/effects_py.py index 9a46bd0c1..4de867507 100644 --- a/tests/policies/effects_py.py +++ b/tests/policies/effects_py.py @@ -11,13 +11,16 @@ def effects_py(g: Graph): deno = DenoRuntime() public = Policy.public() admin_only = Policy.context("role", "admin") - deny_all = deno.policy("deny_all", "() => 'DENY'") + deny_all = deno.policy("deny_all", "_ => 'DENY'") + # allow_all = deno.policy("allow_all", "_ => 'ALLOW'") user = t.struct( { "id": t.integer(), "email": t.email(), - "password_hash": t.string().with_policy(deny_all), + "password_hash": t.string().with_policy( + Policy.on(read=deny_all, update=admin_only) + ), }, name="User", ).with_policy(Policy.on(read=public, update=admin_only, delete=admin_only)) diff --git a/tests/policies/policies_composition.py b/tests/policies/policies_composition.py index 2cff51aa9..94784abcd 100644 --- a/tests/policies/policies_composition.py +++ b/tests/policies/policies_composition.py @@ -1,7 +1,8 @@ # Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. # SPDX-License-Identifier: MPL-2.0 -from typegraph import typegraph, t, Graph +from typegraph import typegraph, effects, t, Graph +from typegraph.policy import Policy from typegraph.runtimes import DenoRuntime @@ -16,6 +17,41 @@ def ctxread(pol_name: str): allow = deno.policy("allowAll", code="() => 'ALLOW' ") pass_through = deno.policy("passThrough", code="() => 'PASS' ") # alt public + big_struct = t.struct( + { + "one": t.struct( + { + "two": t.struct( + { + # Note: + # The policy on each variant is not hoisted + # on the parent struct + "three": t.either( + [ + t.struct({"a": t.integer()}).rename("First"), + t.struct( + { + "b": t.struct( + { + "c": t.integer().with_policy( + Policy.on( + read=allow, + update=ctxread("depth_4"), + ) + ) + } + ) + } + ).rename("Second"), + ] + ).with_policy(ctxread("depth_3")) + } + ).with_policy(Policy.on(read=deny, update=ctxread("depth_2"))) + } + ).with_policy(ctxread("depth_1")) + } + ) + g.expose( simple_traversal_comp=deno.identity( t.struct( @@ -42,39 +78,10 @@ def ctxread(pol_name: str): } ) ).with_policy(pass_through), - traversal_comp=deno.identity( - t.struct( - { - "one": t.struct( - { - "two": t.struct( - { - # Note: - # The policy on each variant is not hoisted - # on the parent struct - "three": t.either( - [ - t.struct({"a": t.integer()}).rename( - "First" - ), - t.struct( - { - "b": t.struct( - { - "c": t.integer().with_policy( - ctxread("depth_4") - ) - } - ) - } - ).rename("Second"), - ] - ).with_policy(ctxread("depth_3")) - } - ).with_policy(ctxread("depth_2")) - } - ).with_policy(ctxread("depth_1")) - } - ) + traversal_comp=deno.func( + big_struct, + big_struct, + code="({ one }) => ({ one })", + effect=effects.update(), ).with_policy(pass_through), ) diff --git a/tests/policies/policies_composition_test.ts b/tests/policies/policies_composition_test.ts index 9d28dfdf4..daa1869d8 100644 --- a/tests/policies/policies_composition_test.ts +++ b/tests/policies/policies_composition_test.ts @@ -134,7 +134,7 @@ Meta.test("Basic chain composition on a single field spec", async (t) => { }); -Meta.test("Traversal composition", async (t) => { +Meta.test("Traversal composition on a per effect policy setup", async (t) => { const e = await t.engine("policies/policies_composition.py"); const inputA = { @@ -157,7 +157,7 @@ Meta.test("Traversal composition", async (t) => { await t.should("have PASS acting as a no-op upon traversal (version 1)", async () => { await gql` - query { + mutation { traversal_comp(one: $one) { one { two { @@ -191,7 +191,7 @@ Meta.test("Traversal composition", async (t) => { await t.should("have PASS acting as a no-op upon traversal (version 2)", async () => { await gql` - query { + mutation { traversal_comp(one: $one) { one { two { @@ -222,7 +222,7 @@ Meta.test("Traversal composition", async (t) => { await t.should("DENY when a protected field on a either variant is encountered", async () => { await gql` - query { + mutation { traversal_comp(one: $one) { one { two { diff --git a/tests/policies/policies_test.ts b/tests/policies/policies_test.ts index aaf2ba3ab..b217f8138 100644 --- a/tests/policies/policies_test.ts +++ b/tests/policies/policies_test.ts @@ -226,7 +226,7 @@ Meta.test("Policies for effects", async (t) => { secrets: await genSecretKey(config), }); - await t.should("succeeed", async () => { + await t.should("succeed", async () => { await gql` query { findUser(id: 12) { @@ -250,6 +250,7 @@ Meta.test("Policies for effects", async (t) => { updateUser(id: 12, set: { email: "john.doe@example.com" }) { id email + password_hash # deny if role!=admin (here undefined) on effect update } } ` @@ -261,7 +262,7 @@ Meta.test("Policies for effects", async (t) => { findUser(id: 12) { id email - password_hash + password_hash # deny on effect read } } ` @@ -286,17 +287,5 @@ Meta.test("Policies for effects", async (t) => { }) .withContext({ role: "admin" }) .on(e); - - await gql` - query { - findUser(id: 12) { - id - email - password_hash - } - } - ` - .expectErrorContains("Authorization failed") - .on(e); }); }); From a0e0f6031ca3bf9960905f8658b4f27d34a923c1 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Thu, 19 Dec 2024 18:59:57 +0300 Subject: [PATCH 08/11] fix: outdated tests/snapshots + doc updates --- .../docs/concepts/mental-model/index.mdx | 6 +- .../docs/reference/policies/index.mdx | 34 +- .../docs/tutorials/metatype-basics/index.mdx | 4 +- examples/typegraphs/execute.py | 2 +- examples/typegraphs/execute.ts | 2 +- examples/typegraphs/func.py | 2 +- examples/typegraphs/func.ts | 2 +- examples/typegraphs/math.py | 2 +- examples/typegraphs/math.ts | 2 +- examples/typegraphs/policies-example.py | 7 +- examples/typegraphs/policies.py | 8 +- examples/typegraphs/policies.ts | 8 +- .../typegraphs/programmable-api-gateway.py | 4 +- .../typegraphs/programmable-api-gateway.ts | 2 +- examples/typegraphs/reduce.py | 2 +- examples/typegraphs/reduce.ts | 2 +- examples/typegraphs/rest.py | 2 +- examples/typegraphs/rest.ts | 2 +- examples/typegraphs/roadmap-policies.py | 2 +- examples/typegraphs/roadmap-policies.ts | 2 +- src/typegate/src/engine/planner/mod.ts | 2 +- src/typegate/src/engine/planner/policies.ts | 31 +- .../src/typegraphs/prisma_migration.json | 2 +- .../src/typegraphs/prisma_migration.py | 3 +- ...core__tests__successful_serialization.snap | 19 +- tests/auth/auth.py | 7 +- .../__snapshots__/typegraph_test.ts.snap | 284 +++++++++-------- tests/e2e/typegraph/typegraphs/deno/simple.ts | 2 +- .../e2e/typegraph/typegraphs/python/simple.py | 2 +- .../__snapshots__/planner_test.ts.snap | 132 ++++---- tests/planner/planner_test.ts | 18 +- .../__snapshots__/query_parsers_test.ts.snap | 96 +++--- .../__snapshots__/graphql_test.ts.snap | 296 +++++++++++------- .../grpc/__snapshots__/grpc_test.ts.snap | 58 ++-- .../runtimes/kv/__snapshots__/kv_test.ts.snap | 128 ++++---- .../runtimes/s3/__snapshots__/s3_test.ts.snap | 103 +++--- .../__snapshots__/temporal_test.ts.snap | 171 +++++----- .../typegate_prisma_test.ts.snap | 8 +- 38 files changed, 810 insertions(+), 649 deletions(-) diff --git a/docs/metatype.dev/docs/concepts/mental-model/index.mdx b/docs/metatype.dev/docs/concepts/mental-model/index.mdx index 4d1032b73..fb947e7f1 100644 --- a/docs/metatype.dev/docs/concepts/mental-model/index.mdx +++ b/docs/metatype.dev/docs/concepts/mental-model/index.mdx @@ -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). { diff --git a/docs/metatype.dev/docs/reference/policies/index.mdx b/docs/metatype.dev/docs/reference/policies/index.mdx index 8bc5ace88..63f0b7399 100644 --- a/docs/metatype.dev/docs/reference/policies/index.mdx +++ b/docs/metatype.dev/docs/reference/policies/index.mdx @@ -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. @@ -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` diff --git a/docs/metatype.dev/docs/tutorials/metatype-basics/index.mdx b/docs/metatype.dev/docs/tutorials/metatype-basics/index.mdx index 228bdd870..d3f87ac18 100644 --- a/docs/metatype.dev/docs/tutorials/metatype-basics/index.mdx +++ b/docs/metatype.dev/docs/tutorials/metatype-basics/index.mdx @@ -581,7 +581,7 @@ typegraph("roadmap", (g) => { const admins = deno.policy( "admins", ` - (_args, { context }) => !!context.username + (_args, { context }) => !!context.username ? 'ALLOW' : 'DENY' `, ); @@ -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( diff --git a/examples/typegraphs/execute.py b/examples/typegraphs/execute.py index ed46b9971..7467a45cb 100644 --- a/examples/typegraphs/execute.py +++ b/examples/typegraphs/execute.py @@ -53,7 +53,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", """ - (_args, { context }) => !!context.username + (_args, { context }) => !!context.username ? 'ALLOW' : 'DENY' """, ) diff --git a/examples/typegraphs/execute.ts b/examples/typegraphs/execute.ts index b78951bbe..55c111c1f 100644 --- a/examples/typegraphs/execute.ts +++ b/examples/typegraphs/execute.ts @@ -52,7 +52,7 @@ await typegraph( const admins = deno.policy( "admins", ` - (_args, { context }) => !!context.username + (_args, { context }) => !!context.username ? 'ALLOW' : 'DENY' `, ); diff --git a/examples/typegraphs/func.py b/examples/typegraphs/func.py index 5ad86ba02..aa6d5bb17 100644 --- a/examples/typegraphs/func.py +++ b/examples/typegraphs/func.py @@ -58,7 +58,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ) # skip:end diff --git a/examples/typegraphs/func.ts b/examples/typegraphs/func.ts index 944d6d7c9..e2f1f17ca 100644 --- a/examples/typegraphs/func.ts +++ b/examples/typegraphs/func.ts @@ -56,7 +56,7 @@ await typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ); // skip:end diff --git a/examples/typegraphs/math.py b/examples/typegraphs/math.py index 054954b87..90fe78a26 100644 --- a/examples/typegraphs/math.py +++ b/examples/typegraphs/math.py @@ -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( diff --git a/examples/typegraphs/math.ts b/examples/typegraphs/math.ts index 3c7879c5e..c0bc7f815 100644 --- a/examples/typegraphs/math.ts +++ b/examples/typegraphs/math.ts @@ -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 diff --git a/examples/typegraphs/policies-example.py b/examples/typegraphs/policies-example.py index 749f540f5..cc3a2ff25 100644 --- a/examples/typegraphs/policies-example.py +++ b/examples/typegraphs/policies-example.py @@ -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' " + ) diff --git a/examples/typegraphs/policies.py b/examples/typegraphs/policies.py index 35a53d077..d701796bd 100644 --- a/examples/typegraphs/policies.py +++ b/examples/typegraphs/policies.py @@ -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"])) diff --git a/examples/typegraphs/policies.ts b/examples/typegraphs/policies.ts index 1517a4d79..0f0d44339 100644 --- a/examples/typegraphs/policies.ts +++ b/examples/typegraphs/policies.ts @@ -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"])); diff --git a/examples/typegraphs/programmable-api-gateway.py b/examples/typegraphs/programmable-api-gateway.py index 284c01b0d..8e9446ced 100644 --- a/examples/typegraphs/programmable-api-gateway.py +++ b/examples/typegraphs/programmable-api-gateway.py @@ -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: diff --git a/examples/typegraphs/programmable-api-gateway.ts b/examples/typegraphs/programmable-api-gateway.ts index 08b5fd003..b07471438 100644 --- a/examples/typegraphs/programmable-api-gateway.ts +++ b/examples/typegraphs/programmable-api-gateway.ts @@ -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 diff --git a/examples/typegraphs/reduce.py b/examples/typegraphs/reduce.py index f8e099321..7a5029fef 100644 --- a/examples/typegraphs/reduce.py +++ b/examples/typegraphs/reduce.py @@ -51,7 +51,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ) g.expose( diff --git a/examples/typegraphs/reduce.ts b/examples/typegraphs/reduce.ts index b485e7ac8..fda53d83d 100644 --- a/examples/typegraphs/reduce.ts +++ b/examples/typegraphs/reduce.ts @@ -49,7 +49,7 @@ typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ); g.expose( diff --git a/examples/typegraphs/rest.py b/examples/typegraphs/rest.py index c0b62c8dc..22595a3c8 100644 --- a/examples/typegraphs/rest.py +++ b/examples/typegraphs/rest.py @@ -51,7 +51,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ) g.expose( diff --git a/examples/typegraphs/rest.ts b/examples/typegraphs/rest.ts index 75e4bd3e7..1f3d6cd82 100644 --- a/examples/typegraphs/rest.ts +++ b/examples/typegraphs/rest.ts @@ -49,7 +49,7 @@ typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ); g.expose( diff --git a/examples/typegraphs/roadmap-policies.py b/examples/typegraphs/roadmap-policies.py index 791355c2b..271e43818 100644 --- a/examples/typegraphs/roadmap-policies.py +++ b/examples/typegraphs/roadmap-policies.py @@ -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 diff --git a/examples/typegraphs/roadmap-policies.ts b/examples/typegraphs/roadmap-policies.ts index 6cb0ef185..d58ce5c07 100644 --- a/examples/typegraphs/roadmap-policies.ts +++ b/examples/typegraphs/roadmap-policies.ts @@ -50,7 +50,7 @@ typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username", + "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", ); g.expose( diff --git a/src/typegate/src/engine/planner/mod.ts b/src/typegate/src/engine/planner/mod.ts index 72c89e329..59d77e2a6 100644 --- a/src/typegate/src/engine/planner/mod.ts +++ b/src/typegate/src/engine/planner/mod.ts @@ -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; } diff --git a/src/typegate/src/engine/planner/policies.ts b/src/typegate/src/engine/planner/policies.ts index 235d00119..420cdc581 100644 --- a/src/typegate/src/engine/planner/policies.ts +++ b/src/typegate/src/engine/planner/policies.ts @@ -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; } @@ -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; @@ -178,15 +181,11 @@ 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) ?? "", - ); for ( const [priorStageId, verdict] of resolvedPolicyCachePerStage.entries() @@ -194,13 +193,13 @@ export class OperationPolicies { 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 } @@ -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 }>, @@ -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 { @@ -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); } @@ -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, }]), @@ -461,4 +453,9 @@ export class OperationPolicies { return targetStageId == parent ? node : null; }).filter((name) => name != null); } + + // for testing + get stageToPolicies() { + return this.#stageToPolicies; + } } diff --git a/src/typegate/src/typegraphs/prisma_migration.json b/src/typegate/src/typegraphs/prisma_migration.json index c6803a017..1a4da15dd 100644 --- a/src/typegate/src/typegraphs/prisma_migration.json +++ b/src/typegate/src/typegraphs/prisma_migration.json @@ -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": [] } }, diff --git a/src/typegate/src/typegraphs/prisma_migration.py b/src/typegate/src/typegraphs/prisma_migration.py index ddc96df00..1e88ff7a6 100644 --- a/src/typegate/src/typegraphs/prisma_migration.py +++ b/src/typegate/src/typegraphs/prisma_migration.py @@ -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"])) diff --git a/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap b/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap index 048b0a6ae..29f6ea430 100644 --- a/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap +++ b/src/typegraph/core/src/snapshots/typegraph_core__tests__successful_serialization.snap @@ -7,7 +7,6 @@ expression: typegraph.0 { "type": "object", "title": "test", - "policies": [], "properties": { "one": 1 }, @@ -19,7 +18,6 @@ expression: typegraph.0 { "type": "function", "title": "root_one_fn", - "policies": [], "input": 2, "output": 4, "runtimeConfig": null, @@ -30,44 +28,43 @@ expression: typegraph.0 { "type": "object", "title": "root_one_fn_input", - "policies": [], "properties": { "one": 3, "two": 4, "three": 5 }, "id": [], - "required": [] + "required": [], + "policies": { + "one": [], + "two": [], + "three": [] + } }, { "type": "integer", - "title": "root_one_fn_input_one_integer", - "policies": [] + "title": "root_one_fn_input_one_integer" }, { "type": "integer", "title": "root_one_fn_input_two_integer", - "policies": [], "minimum": 12, "maximum": 44 }, { "type": "optional", "title": "root_one_fn_input_three_root_one_fn_input_three_root_one_fn_input_three_float_list_optional", - "policies": [], "item": 6, "default_value": null }, { "type": "list", "title": "root_one_fn_input_three_root_one_fn_input_three_float_list", - "policies": [], "items": 7 }, { "type": "float", - "title": "root_one_fn_input_three_float", - "policies": [] + "title": "root_one_fn_input_three_float" } ], "materializers": [ diff --git a/tests/auth/auth.py b/tests/auth/auth.py index e3a1703de..d40ea412d 100644 --- a/tests/auth/auth.py +++ b/tests/auth/auth.py @@ -17,9 +17,12 @@ def auth(g: Graph): remote = HttpRuntime("https://api.github.com") public = Policy.public() - private = deno.policy("private", "(_args, { context }) => !!context.user1") + private = deno.policy( + "private", "(_args, { context }) => !!context.user1 ? 'ALLOW' : 'DENY'" + ) with_token = deno.policy( - "with_token", "(_args, { context }) => { return !!context.accessToken; }" + "with_token", + "(_args, { context }) => { return !!context.accessToken ? 'ALLOW' : 'DENY'; }", ) x = t.struct({"x": t.integer()}) diff --git a/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap b/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap index b81a657e9..65a1bbec0 100644 --- a/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap +++ b/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap @@ -7,21 +7,22 @@ snapshot[`typegraphs creation 1`] = ` { "type": "object", "title": "test-complex-types", - "policies": [], "properties": { "test": 1 }, "id": [], "required": [ "test" - ] + ], + "policies": { + "test": [ + 0 + ] + } }, { "type": "function", "title": "root_test_fn", - "policies": [ - 0 - ], "input": 2, "output": 18, "runtimeConfig": null, @@ -32,7 +33,6 @@ snapshot[`typegraphs creation 1`] = ` { "type": "object", "title": "ComplexType", - "policies": [], "properties": { "a_string": 3, "a_float": 4, @@ -45,24 +45,32 @@ snapshot[`typegraphs creation 1`] = ` "an_email": 17 }, "id": [], - "required": [] + "required": [], + "policies": { + "a_string": [], + "a_float": [], + "an_enum": [], + "an_integer_enum": [], + "a_float_enum": [], + "a_struct": [], + "nested": [], + "nested_with_ref": [], + "an_email": [] + } }, { "type": "string", - "title": "ComplexType_a_string_string", - "policies": [] + "title": "ComplexType_a_string_string" }, { "type": "float", "title": "ComplexType_a_float_float", - "policies": [], "minimum": 1.0, "multipleOf": 2.0 }, { "type": "string", "title": "ComplexType_an_enum_string_enum", - "policies": [], "enum": [ "\\\\"one\\\\"", "\\\\"two\\\\"" @@ -71,7 +79,6 @@ snapshot[`typegraphs creation 1`] = ` { "type": "integer", "title": "ComplexType_an_integer_enum_integer_enum", - "policies": [], "enum": [ "1", "2" @@ -80,7 +87,6 @@ snapshot[`typegraphs creation 1`] = ` { "type": "float", "title": "ComplexType_a_float_enum_float", - "policies": [], "enum": [ "1.5", "2.5" @@ -89,35 +95,33 @@ snapshot[`typegraphs creation 1`] = ` { "type": "object", "title": "ComplexType_a_struct_struct", - "policies": [], "properties": { "value": 9 }, "id": [], - "required": [] + "required": [], + "policies": { + "value": [] + } }, { "type": "float", - "title": "ComplexType_a_struct_struct_value_float", - "policies": [] + "title": "ComplexType_a_struct_struct_value_float" }, { "type": "optional", "title": "ComplexType_nested_ComplexType_nested_ComplexType_nested_either_list_optional", - "policies": [], "item": 11, "default_value": null }, { "type": "list", "title": "ComplexType_nested_ComplexType_nested_either_list", - "policies": [], "items": 12 }, { "type": "either", "title": "ComplexType_nested_either", - "policies": [], "oneOf": [ 3, 13 @@ -125,44 +129,42 @@ snapshot[`typegraphs creation 1`] = ` }, { "type": "integer", - "title": "ComplexType_nested_either_t1_integer", - "policies": [] + "title": "ComplexType_nested_either_t1_integer" }, { "type": "object", "title": "SomeType", - "policies": [], "properties": { "one": 15, "two": 16 }, "id": [], - "required": [] + "required": [], + "policies": { + "one": [], + "two": [] + } }, { "type": "list", "title": "Two", - "policies": [], "items": 13, "minItems": 3 }, { "type": "optional", "title": "SomeType_two_SomeType_optional", - "policies": [], "item": 14, "default_value": null }, { "type": "string", "title": "ComplexType_an_email_string_email", - "policies": [], "format": "email" }, { "type": "boolean", - "title": "root_test_fn_output", - "policies": [] + "title": "root_test_fn_output" } ], "materializers": [ @@ -186,7 +188,7 @@ snapshot[`typegraphs creation 1`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } } ], @@ -270,7 +272,6 @@ snapshot[`typegraphs creation 2`] = ` { "type": "object", "title": "test-multiple-runtimes", - "policies": [], "properties": { "add": 1, "multiply": 4 @@ -279,14 +280,19 @@ snapshot[`typegraphs creation 2`] = ` "required": [ "add", "multiply" - ] + ], + "policies": { + "add": [ + 0 + ], + "multiply": [ + 0 + ] + } }, { "type": "function", "title": "root_add_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, "runtimeConfig": null, @@ -297,25 +303,24 @@ snapshot[`typegraphs creation 2`] = ` { "type": "object", "title": "root_add_fn_input", - "policies": [], "properties": { "first": 3, "second": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "first": [], + "second": [] + } }, { "type": "float", - "title": "root_add_fn_input_first_float", - "policies": [] + "title": "root_add_fn_input_first_float" }, { "type": "function", "title": "root_multiply_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, "runtimeConfig": null, @@ -345,7 +350,7 @@ snapshot[`typegraphs creation 2`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { @@ -414,7 +419,6 @@ snapshot[`typegraphs creation 3`] = ` { "type": "object", "title": "test-types", - "policies": [], "properties": { "one": 1, "two": 6, @@ -425,14 +429,22 @@ snapshot[`typegraphs creation 3`] = ` "one", "two", "three" - ] + ], + "policies": { + "one": [ + 0 + ], + "two": [ + 1 + ], + "three": [ + 2 + ] + } }, { "type": "function", "title": "root_one_fn", - "policies": [ - 0 - ], "input": 2, "output": 5, "runtimeConfig": null, @@ -443,38 +455,35 @@ snapshot[`typegraphs creation 3`] = ` { "type": "object", "title": "root_one_fn_input", - "policies": [], "properties": { "a": 3, "b": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "a": [], + "b": [] + } }, { "type": "integer", - "title": "root_one_fn_input_a_integer", - "policies": [] + "title": "root_one_fn_input_a_integer" }, { "type": "integer", "title": "root_one_fn_input_b_integer", - "policies": [], "minimum": 12 }, { "type": "integer", "title": "root_one_fn_output", - "policies": [], "minimum": 12, "maximum": 43 }, { "type": "function", "title": "root_two_fn", - "policies": [ - 1 - ], "input": 7, "output": 8, "runtimeConfig": null, @@ -485,31 +494,34 @@ snapshot[`typegraphs creation 3`] = ` { "type": "object", "title": "User", - "policies": [], "properties": { "id": 3, "post": 8 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "post": [] + } }, { "type": "object", "title": "Post", - "policies": [], "properties": { "id": 3, "author": 7 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "author": [] + } }, { "type": "function", "title": "root_three_fn", - "policies": [ - 2 - ], "input": 2, "output": 2, "runtimeConfig": null, @@ -599,7 +611,7 @@ snapshot[`typegraphs creation 3`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } } ], @@ -664,21 +676,22 @@ snapshot[`typegraphs creation 4`] = ` { "type": "object", "title": "test-complex-types", - "policies": [], "properties": { "test": 1 }, "id": [], "required": [ "test" - ] + ], + "policies": { + "test": [ + 0 + ] + } }, { "type": "function", "title": "root_test_fn", - "policies": [ - 0 - ], "input": 2, "output": 18, "runtimeConfig": null, @@ -689,7 +702,6 @@ snapshot[`typegraphs creation 4`] = ` { "type": "object", "title": "ComplexType", - "policies": [], "properties": { "a_string": 3, "a_float": 4, @@ -702,24 +714,32 @@ snapshot[`typegraphs creation 4`] = ` "an_email": 17 }, "id": [], - "required": [] + "required": [], + "policies": { + "a_string": [], + "a_float": [], + "an_enum": [], + "an_integer_enum": [], + "a_float_enum": [], + "a_struct": [], + "nested": [], + "nested_with_ref": [], + "an_email": [] + } }, { "type": "string", - "title": "ComplexType_a_string_string", - "policies": [] + "title": "ComplexType_a_string_string" }, { "type": "float", "title": "ComplexType_a_float_float", - "policies": [], "minimum": 1.0, "multipleOf": 2.0 }, { "type": "string", "title": "ComplexType_an_enum_string_enum", - "policies": [], "enum": [ "\\\\"one\\\\"", "\\\\"two\\\\"" @@ -728,7 +748,6 @@ snapshot[`typegraphs creation 4`] = ` { "type": "integer", "title": "ComplexType_an_integer_enum_integer_enum", - "policies": [], "enum": [ "1", "2" @@ -737,7 +756,6 @@ snapshot[`typegraphs creation 4`] = ` { "type": "float", "title": "ComplexType_a_float_enum_float", - "policies": [], "enum": [ "1.5", "2.5" @@ -746,35 +764,33 @@ snapshot[`typegraphs creation 4`] = ` { "type": "object", "title": "ComplexType_a_struct_struct", - "policies": [], "properties": { "value": 9 }, "id": [], - "required": [] + "required": [], + "policies": { + "value": [] + } }, { "type": "float", - "title": "ComplexType_a_struct_struct_value_float", - "policies": [] + "title": "ComplexType_a_struct_struct_value_float" }, { "type": "optional", "title": "ComplexType_nested_ComplexType_nested_ComplexType_nested_either_list_optional", - "policies": [], "item": 11, "default_value": null }, { "type": "list", "title": "ComplexType_nested_ComplexType_nested_either_list", - "policies": [], "items": 12 }, { "type": "either", "title": "ComplexType_nested_either", - "policies": [], "oneOf": [ 3, 13 @@ -782,44 +798,42 @@ snapshot[`typegraphs creation 4`] = ` }, { "type": "integer", - "title": "ComplexType_nested_either_t1_integer", - "policies": [] + "title": "ComplexType_nested_either_t1_integer" }, { "type": "object", "title": "SomeType", - "policies": [], "properties": { "one": 15, "two": 16 }, "id": [], - "required": [] + "required": [], + "policies": { + "one": [], + "two": [] + } }, { "type": "list", "title": "Two", - "policies": [], "items": 13, "minItems": 3 }, { "type": "optional", "title": "SomeType_two_SomeType_optional", - "policies": [], "item": 14, "default_value": null }, { "type": "string", "title": "ComplexType_an_email_string_email", - "policies": [], "format": "email" }, { "type": "boolean", - "title": "root_test_fn_output", - "policies": [] + "title": "root_test_fn_output" } ], "materializers": [ @@ -843,7 +857,7 @@ snapshot[`typegraphs creation 4`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } } ], @@ -927,7 +941,6 @@ snapshot[`typegraphs creation 5`] = ` { "type": "object", "title": "test-multiple-runtimes", - "policies": [], "properties": { "add": 1, "multiply": 4 @@ -936,14 +949,19 @@ snapshot[`typegraphs creation 5`] = ` "required": [ "add", "multiply" - ] + ], + "policies": { + "add": [ + 0 + ], + "multiply": [ + 0 + ] + } }, { "type": "function", "title": "root_add_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, "runtimeConfig": null, @@ -954,25 +972,24 @@ snapshot[`typegraphs creation 5`] = ` { "type": "object", "title": "root_add_fn_input", - "policies": [], "properties": { "first": 3, "second": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "first": [], + "second": [] + } }, { "type": "float", - "title": "root_add_fn_input_first_float", - "policies": [] + "title": "root_add_fn_input_first_float" }, { "type": "function", "title": "root_multiply_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, "runtimeConfig": null, @@ -1002,7 +1019,7 @@ snapshot[`typegraphs creation 5`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { @@ -1071,7 +1088,6 @@ snapshot[`typegraphs creation 6`] = ` { "type": "object", "title": "test-types", - "policies": [], "properties": { "one": 1, "two": 6, @@ -1082,14 +1098,22 @@ snapshot[`typegraphs creation 6`] = ` "one", "two", "three" - ] + ], + "policies": { + "one": [ + 0 + ], + "two": [ + 1 + ], + "three": [ + 2 + ] + } }, { "type": "function", "title": "root_one_fn", - "policies": [ - 0 - ], "input": 2, "output": 5, "runtimeConfig": null, @@ -1100,38 +1124,35 @@ snapshot[`typegraphs creation 6`] = ` { "type": "object", "title": "root_one_fn_input", - "policies": [], "properties": { "a": 3, "b": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "a": [], + "b": [] + } }, { "type": "integer", - "title": "root_one_fn_input_a_integer", - "policies": [] + "title": "root_one_fn_input_a_integer" }, { "type": "integer", "title": "root_one_fn_input_b_integer", - "policies": [], "minimum": 12 }, { "type": "integer", "title": "root_one_fn_output", - "policies": [], "minimum": 12, "maximum": 43 }, { "type": "function", "title": "root_two_fn", - "policies": [ - 1 - ], "input": 7, "output": 8, "runtimeConfig": null, @@ -1142,31 +1163,34 @@ snapshot[`typegraphs creation 6`] = ` { "type": "object", "title": "User", - "policies": [], "properties": { "id": 3, "post": 8 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "post": [] + } }, { "type": "object", "title": "Post", - "policies": [], "properties": { "id": 3, "author": 7 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "author": [] + } }, { "type": "function", "title": "root_three_fn", - "policies": [ - 2 - ], "input": 2, "output": 2, "runtimeConfig": null, @@ -1256,7 +1280,7 @@ snapshot[`typegraphs creation 6`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } } ], diff --git a/tests/e2e/typegraph/typegraphs/deno/simple.ts b/tests/e2e/typegraph/typegraphs/deno/simple.ts index 7546b95cd..eff5d4815 100644 --- a/tests/e2e/typegraph/typegraphs/deno/simple.ts +++ b/tests/e2e/typegraph/typegraphs/deno/simple.ts @@ -41,7 +41,7 @@ typegraph("test-types", (g: any) => { .func(user, post, { code: "(user) => ({ id: 12, user })", }) - .withPolicy(deno.policy("deny", "() => false")), + .withPolicy(deno.policy("deny", "() => 'DENY'")), three: deno .import(s1, s1, { name: "three", module: "scripts/three.ts" }) .withPolicy(pub), diff --git a/tests/e2e/typegraph/typegraphs/python/simple.py b/tests/e2e/typegraph/typegraphs/python/simple.py index 0deca0f76..052da9d48 100644 --- a/tests/e2e/typegraph/typegraphs/python/simple.py +++ b/tests/e2e/typegraph/typegraphs/python/simple.py @@ -23,7 +23,7 @@ def test_types(g: Graph): g.expose( one=deno.func(s1, b, code="() => 12").with_policy(internal), two=deno.func(user, post, code="(user) => ({ id: 12, user })").with_policy( - deno.policy("deny", "() => false") + deno.policy("deny", "() => 'DENY'") ), three=deno.import_(s1, s1, name="three", module="scripts/three.ts").with_policy( public diff --git a/tests/planner/__snapshots__/planner_test.ts.snap b/tests/planner/__snapshots__/planner_test.ts.snap index d62e8a890..3a67182e5 100644 --- a/tests/planner/__snapshots__/planner_test.ts.snap +++ b/tests/planner/__snapshots__/planner_test.ts.snap @@ -95,81 +95,75 @@ snapshot[`planner 1`] = ` snapshot[`planner 2`] = ` { - one: { - funcType: { - input: 2, - materializer: 0, - output: 3, - policies: [ + "": [ + { + canonFieldName: "one", + indices: [ 0, ], - rate_calls: false, - rate_weight: null, - runtimeConfig: null, - title: "root_one_fn", - type: "function", }, - referencedTypes: { - "one.email": [ - { - title: "root_one_fn_output_email_string_email", - type: "string", - }, - ], - "one.id": [ - { - title: "root_one_fn_output_id_string_uuid", - type: "string", - }, - ], - "one.nested": [ - { - title: "root_one_fn_output_nested_struct", - type: "object", - }, - ], - "one.nested.first": [ - { - title: "root_one_fn_output_nested_struct_first_string", - type: "string", - }, - ], - "one.nested.second": [ - { - title: "root_one_fn_output_nested_struct_second_root_one_fn_output_nested_struct_second_float_list", - type: "list", - }, - { - title: "root_one_fn_output_nested_struct_second_float", - type: "float", - }, - ], - "one.nested.third": [ - { - title: "root_one_fn_output_nested_struct_third_root_one_fn_output_nested_struct_third_boolean_optional", - type: "optional", - }, - { - title: "root_one_fn_output_nested_struct_third_boolean", - type: "boolean", - }, + { + canonFieldName: "two", + indices: [ + 0, ], - one: [ - { - title: "root_one_fn", - type: "function", - }, - { - title: "root_one_fn_output", - type: "object", - }, - { - title: "root_one_fn_input", - type: "object", - }, + }, + { + canonFieldName: "three", + indices: [ + 0, ], }, - }, + ], + "one.email": [], + "one.id": [], + "one.nested": [ + { + canonFieldName: "first", + indices: [], + }, + { + canonFieldName: "second", + indices: [], + }, + { + canonFieldName: "third", + indices: [], + }, + ], + "one.nested.first": [], + "one.nested.second": [], + "one.nested.third": [], + one: [ + { + canonFieldName: "id", + indices: [], + }, + { + canonFieldName: "email", + indices: [], + }, + { + canonFieldName: "nested", + indices: [], + }, + { + canonFieldName: "union1", + indices: [], + }, + { + canonFieldName: "union2", + indices: [], + }, + { + canonFieldName: "from_union1", + indices: [], + }, + { + canonFieldName: "from_union2", + indices: [], + }, + ], } `; diff --git a/tests/planner/planner_test.ts b/tests/planner/planner_test.ts index c0797c6a3..a071ff5a3 100644 --- a/tests/planner/planner_test.ts +++ b/tests/planner/planner_test.ts @@ -36,22 +36,8 @@ Meta.test("planner", async (t) => { }); await t.should("generate the right policy tree", async () => { - const fns = Object.fromEntries(plan.policies.functions); - await t.assertSnapshot( - mapValues(fns, (subtree) => { - const funcType = e.tg.type(subtree.funcTypeIdx); - return { - funcType, - referencedTypes: mapValues( - Object.fromEntries(subtree.referencedTypes), - (types) => - types.map((idx) => - filterKeys(e.tg.type(idx), (k) => ["type", "title"].includes(k)) - ), - ), - }; - }), - ); + const stageToPolicies = Object.fromEntries(plan.policies.stageToPolicies); + await t.assertSnapshot(stageToPolicies); }); await t.should("fail when required selections are missing", async () => { diff --git a/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap b/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap index 74d5ae5d6..17f4819f8 100644 --- a/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap +++ b/tests/query_parsers/__snapshots__/query_parsers_test.ts.snap @@ -4,7 +4,11 @@ snapshot[`GraphQL parser 1`] = ` [ { id: [], - policies: [], + policies: { + user: [ + 0, + ], + }, properties: { mutation: 18, query: 17, @@ -17,9 +21,15 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [ - 0, - ], + policies: { + find: [ + 0, + ], + profile: [], + update: [ + 0, + ], + }, properties: { find: 2, profile: 8, @@ -31,11 +41,8 @@ snapshot[`GraphQL parser 1`] = ` }, { input: 3, - materializer: 1, + materializer: 0, output: 5, - policies: [ - 0, - ], rate_calls: false, rate_weight: null, runtimeConfig: null, @@ -46,7 +53,9 @@ snapshot[`GraphQL parser 1`] = ` id: [ "id", ], - policies: [], + policies: { + id: [], + }, properties: { id: 4, }, @@ -55,7 +64,6 @@ snapshot[`GraphQL parser 1`] = ` type: "object", }, { - policies: [], title: "UserId", type: "string", }, @@ -63,7 +71,10 @@ snapshot[`GraphQL parser 1`] = ` id: [ "id", ], - policies: [], + policies: { + id: [], + name: [], + }, properties: { id: 4, name: 6, @@ -73,17 +84,13 @@ snapshot[`GraphQL parser 1`] = ` type: "object", }, { - policies: [], title: "User_name_string", type: "string", }, { input: 5, - materializer: 2, + materializer: 1, output: 5, - policies: [ - 0, - ], rate_calls: false, rate_weight: null, runtimeConfig: null, @@ -92,7 +99,14 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [], + policies: { + picture: [ + 0, + ], + setPicture: [ + 0, + ], + }, properties: { picture: 9, setPicture: 12, @@ -103,11 +117,8 @@ snapshot[`GraphQL parser 1`] = ` }, { input: 3, - materializer: 3, + materializer: 2, output: 10, - policies: [ - 0, - ], rate_calls: false, rate_weight: null, runtimeConfig: null, @@ -118,7 +129,10 @@ snapshot[`GraphQL parser 1`] = ` id: [ "id", ], - policies: [], + policies: { + id: [], + url: [], + }, properties: { id: 4, url: 11, @@ -129,17 +143,13 @@ snapshot[`GraphQL parser 1`] = ` }, { format: "uri", - policies: [], title: "Picture_url_string_uri", type: "string", }, { input: 10, - materializer: 4, + materializer: 3, output: 10, - policies: [ - 0, - ], rate_calls: false, rate_weight: null, runtimeConfig: null, @@ -148,9 +158,11 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [ - 0, - ], + policies: { + picture: [ + 0, + ], + }, properties: { picture: 9, }, @@ -160,9 +172,11 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [ - 0, - ], + policies: { + setPicture: [ + 0, + ], + }, properties: { setPicture: 12, }, @@ -172,7 +186,11 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [], + policies: { + find: [ + 0, + ], + }, properties: { find: 2, profile: 13, @@ -185,7 +203,11 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [], + policies: { + update: [ + 0, + ], + }, properties: { profile: 14, update: 7, @@ -198,7 +220,7 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [], + policies: {}, properties: { user: 15, }, @@ -210,7 +232,7 @@ snapshot[`GraphQL parser 1`] = ` }, { id: [], - policies: [], + policies: {}, properties: { user: 16, }, diff --git a/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap b/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap index d8d578d95..4517c2ad0 100644 --- a/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap +++ b/tests/runtimes/graphql/__snapshots__/graphql_test.ts.snap @@ -6,7 +6,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "graphql", - "policies": [], "properties": { "user": 1, "users": 5, @@ -21,14 +20,28 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "createUser", "create_message", "messages" - ] + ], + "policies": { + "user": [ + 0 + ], + "users": [ + 0 + ], + "createUser": [ + 0 + ], + "create_message": [ + 0 + ], + "messages": [ + 0 + ] + } }, { "type": "function", "title": "root_user_fn", - "policies": [ - 0 - ], "input": 2, "output": 4, "runtimeConfig": null, @@ -39,24 +52,24 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "root_user_fn_input", - "policies": [], "properties": { "id": 3 }, "id": [ "id" ], - "required": [] + "required": [], + "policies": { + "id": [] + } }, { "type": "string", - "title": "root_user_fn_input_id_string", - "policies": [] + "title": "root_user_fn_input_id_string" }, { "type": "object", "title": "User", - "policies": [], "properties": { "id": 3, "name": 3 @@ -64,14 +77,15 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "id": [ "id" ], - "required": [] + "required": [], + "policies": { + "id": [], + "name": [] + } }, { "type": "function", "title": "root_users_fn", - "policies": [ - 0 - ], "input": 6, "output": 7, "runtimeConfig": null, @@ -82,7 +96,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "root_users_fn_input", - "policies": [], "properties": {}, "id": [], "required": [] @@ -90,25 +103,23 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "root_users_fn_output", - "policies": [], "properties": { "data": 8 }, "id": [], - "required": [] + "required": [], + "policies": { + "data": [] + } }, { "type": "list", "title": "root_users_fn_output_data_User_list", - "policies": [], "items": 4 }, { "type": "function", "title": "root_createUser_fn", - "policies": [ - 0 - ], "input": 10, "output": 4, "runtimeConfig": null, @@ -119,31 +130,34 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "root_createUser_fn_input", - "policies": [], "properties": { "input": 11 }, "id": [], - "required": [] + "required": [], + "policies": { + "input": [] + } }, { "type": "object", "title": "CreateUserInput", - "policies": [], "properties": { "name": 3, "username": 3, "email": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [], + "username": [], + "email": [] + } }, { "type": "function", "title": "root_create_message_fn", - "policies": [ - 0 - ], "input": 17, "output": 20, "runtimeConfig": null, @@ -154,7 +168,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "message", - "policies": [], "properties": { "id": 14, "title": 3, @@ -164,17 +177,21 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "id": [ "id" ], - "required": [] + "required": [], + "policies": { + "id": [], + "title": [], + "user_id": [], + "user": [] + } }, { "type": "integer", - "title": "message_create_input_id_integer", - "policies": [] + "title": "message_create_input_id_integer" }, { "type": "function", "title": "message_output_user_fn", - "policies": [], "input": 2, "output": 16, "injections": { @@ -195,43 +212,46 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "optional", "title": "message_output_user_fn_output", - "policies": [], "item": 4, "default_value": null }, { "type": "object", "title": "root_create_message_fn_input", - "policies": [], "properties": { "data": 18 }, "id": [], - "required": [] + "required": [], + "policies": { + "data": [] + } }, { "type": "object", "title": "message_create_input", - "policies": [], "properties": { "id": 19, "title": 3, "user_id": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "title": [], + "user_id": [] + } }, { "type": "optional", "title": "message_create_input_id_message_create_input_id_integer_optional", - "policies": [], "item": 14, "default_value": null }, { "type": "object", "title": "message_output", - "policies": [], "properties": { "id": 14, "title": 3, @@ -241,14 +261,17 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "id": [ "id" ], - "required": [] + "required": [], + "policies": { + "id": [], + "title": [], + "user_id": [], + "user": [] + } }, { "type": "function", "title": "root_messages_fn", - "policies": [ - 0 - ], "input": 22, "output": 73, "runtimeConfig": null, @@ -259,7 +282,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "message_query_input", - "policies": [], "properties": { "where": 23, "orderBy": 54, @@ -269,19 +291,25 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "distinct": 70 }, "id": [], - "required": [] + "required": [], + "policies": { + "where": [], + "orderBy": [], + "take": [], + "skip": [], + "cursor": [], + "distinct": [] + } }, { "type": "optional", "title": "message_query_input_where_message_query_where_input_optional", - "policies": [], "item": 24, "default_value": null }, { "type": "object", "title": "message_query_where_input", - "policies": [], "properties": { "id": 25, "title": 36, @@ -291,26 +319,31 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "NOT": 23 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "title": [], + "user_id": [], + "AND": [], + "OR": [], + "NOT": [] + } }, { "type": "optional", "title": "message_query_where_input_id__prisma_integer_filter_ex_optional", - "policies": [], "item": 26, "default_value": null }, { "type": "optional", "title": "_prisma_integer_filter_ex", - "policies": [], "item": 27, "default_value": null }, { "type": "union", "title": "message_query_where_input_id_union", - "policies": [], "anyOf": [ 28, 35 @@ -319,7 +352,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "either", "title": "_prisma_integer_filter", - "policies": [], "oneOf": [ 14, 29, @@ -332,27 +364,30 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "_prisma_integer_filter_t1_struct", - "policies": [], "properties": { "equals": 14 }, "id": [], - "required": [] + "required": [], + "policies": { + "equals": [] + } }, { "type": "object", "title": "_prisma_integer_filter_t2_struct", - "policies": [], "properties": { "not": 14 }, "id": [], - "required": [] + "required": [], + "policies": { + "not": [] + } }, { "type": "object", "title": "_prisma_integer_filter_t3_struct", - "policies": [], "properties": { "lt": 19, "gt": 19, @@ -360,62 +395,70 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "gte": 19 }, "id": [], - "required": [] + "required": [], + "policies": { + "lt": [], + "gt": [], + "lte": [], + "gte": [] + } }, { "type": "object", "title": "_prisma_integer_filter_t4_struct", - "policies": [], "properties": { "in": 33 }, "id": [], - "required": [] + "required": [], + "policies": { + "in": [] + } }, { "type": "list", "title": "_prisma_integer_filter_t4_struct_in_message_create_input_id_integer_list", - "policies": [], "items": 14 }, { "type": "object", "title": "_prisma_integer_filter_t5_struct", - "policies": [], "properties": { "notIn": 33 }, "id": [], - "required": [] + "required": [], + "policies": { + "notIn": [] + } }, { "type": "object", "title": "message_query_where_input_id_union_t1_struct", - "policies": [], "properties": { "not": 28 }, "id": [], - "required": [] + "required": [], + "policies": { + "not": [] + } }, { "type": "optional", "title": "message_query_where_input_title__prisma_string_filter_ex_optional", - "policies": [], "item": 37, "default_value": null }, { "type": "optional", "title": "_prisma_string_filter_ex", - "policies": [], "item": 38, "default_value": null }, { "type": "union", "title": "message_query_where_input_title_union", - "policies": [], "anyOf": [ 39, 51 @@ -424,7 +467,6 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "union", "title": "_prisma_string_filter", - "policies": [], "anyOf": [ 3, 40, @@ -439,71 +481,79 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "_prisma_string_filter_t1_struct", - "policies": [], "properties": { "equals": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "equals": [] + } }, { "type": "object", "title": "_prisma_string_filter_t2_struct", - "policies": [], "properties": { "not": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "not": [] + } }, { "type": "object", "title": "_prisma_string_filter_t3_struct", - "policies": [], "properties": { "in": 43 }, "id": [], - "required": [] + "required": [], + "policies": { + "in": [] + } }, { "type": "list", "title": "_prisma_string_filter_t3_struct_in_root_user_fn_input_id_string_list", - "policies": [], "items": 3 }, { "type": "object", "title": "_prisma_string_filter_t4_struct", - "policies": [], "properties": { "notIn": 43 }, "id": [], - "required": [] + "required": [], + "policies": { + "notIn": [] + } }, { "type": "object", "title": "_prisma_string_filter_t5_struct", - "policies": [], "properties": { "contains": 3, "mode": 46 }, "id": [], - "required": [] + "required": [], + "policies": { + "contains": [], + "mode": [] + } }, { "type": "optional", "title": "_prisma_string_filter_t5_struct_mode__prisma_string_filter_t5_struct_mode_string_enum_optional", - "policies": [], "item": 47, "default_value": null }, { "type": "string", "title": "_prisma_string_filter_t5_struct_mode_string_enum", - "policies": [], "enum": [ "\\\\"insensitive\\\\"" ] @@ -511,90 +561,94 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "_prisma_string_filter_t6_struct", - "policies": [], "properties": { "search": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "search": [] + } }, { "type": "object", "title": "_prisma_string_filter_t7_struct", - "policies": [], "properties": { "startsWith": 50, "endsWith": 50 }, "id": [], - "required": [] + "required": [], + "policies": { + "startsWith": [], + "endsWith": [] + } }, { "type": "optional", "title": "_prisma_string_filter_t7_struct_startsWith_root_user_fn_input_id_string_optional", - "policies": [], "item": 3, "default_value": null }, { "type": "object", "title": "message_query_where_input_title_union_t1_struct", - "policies": [], "properties": { "not": 39 }, "id": [], - "required": [] + "required": [], + "policies": { + "not": [] + } }, { "type": "optional", "title": "message_query_where_input_AND_message_query_where_input_AND_message_query_where_input_list_optional", - "policies": [], "item": 53, "default_value": null }, { "type": "list", "title": "message_query_where_input_AND_message_query_where_input_list", - "policies": [], "items": 24 }, { "type": "optional", "title": "message_query_input_orderBy_message_order_by_optional", - "policies": [], "item": 55, "default_value": null }, { "type": "list", "title": "message_order_by", - "policies": [], "items": 56 }, { "type": "object", "title": "message_query_input_orderBy_struct", - "policies": [], "properties": { "id": 57, "title": 57, "user_id": 57 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "title": [], + "user_id": [] + } }, { "type": "optional", "title": "_prisma_sort", - "policies": [], "item": 58, "default_value": null }, { "type": "union", "title": "message_query_input_orderBy_struct_id_union", - "policies": [], "anyOf": [ 59, 60 @@ -603,17 +657,18 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "message_query_input_orderBy_struct_id_union_t0_struct", - "policies": [], "properties": { "sort": 60 }, "id": [], - "required": [] + "required": [], + "policies": { + "sort": [] + } }, { "type": "string", "title": "_prisma_sort_order", - "policies": [], "enum": [ "\\\\"asc\\\\"", "\\\\"desc\\\\"" @@ -622,40 +677,34 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "optional", "title": "message_query_input_take__take_optional", - "policies": [], "item": 62, "default_value": null }, { "type": "integer", "title": "_take", - "policies": [], "exclusiveMinimum": 0 }, { "type": "optional", "title": "message_query_input_skip__skip_optional", - "policies": [], "item": 64, "default_value": null }, { "type": "integer", "title": "_skip", - "policies": [], "minimum": 0 }, { "type": "optional", "title": "message_query_input_cursor_message_cursor_optional", - "policies": [], "item": 66, "default_value": null }, { "type": "union", "title": "message_cursor", - "policies": [], "anyOf": [ 67, 68, @@ -665,52 +714,55 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "object", "title": "message_cursor_t0_struct", - "policies": [], "properties": { "id": 14 }, "id": [ "id" ], - "required": [] + "required": [], + "policies": { + "id": [] + } }, { "type": "object", "title": "message_cursor_t1_struct", - "policies": [], "properties": { "title": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "title": [] + } }, { "type": "object", "title": "message_cursor_t2_struct", - "policies": [], "properties": { "user_id": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "user_id": [] + } }, { "type": "optional", "title": "message_query_input_distinct_message_keys_union_optional", - "policies": [], "item": 71, "default_value": null }, { "type": "list", "title": "message_keys_union", - "policies": [], "items": 72 }, { "type": "string", "title": "message_query_input_distinct_string_enum", - "policies": [], "enum": [ "\\\\"id\\\\"", "\\\\"title\\\\"", @@ -720,13 +772,11 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` { "type": "list", "title": "root_messages_fn_output", - "policies": [], "items": 74 }, { "type": "object", "title": "message_with_nested_count", - "policies": [], "properties": { "id": 14, "title": 3, @@ -734,7 +784,13 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "user": 15 }, "id": [], - "required": [] + "required": [], + "policies": { + "id": [], + "title": [], + "user_id": [], + "user": [] + } } ], "materializers": [ @@ -757,7 +813,7 @@ snapshot[`Typegraph generation with GraphQL runtime 1`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { diff --git a/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap b/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap index 2979f2aed..c8f82853f 100644 --- a/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap +++ b/tests/runtimes/grpc/__snapshots__/grpc_test.ts.snap @@ -7,21 +7,22 @@ snapshot[`Typegraph using grpc 1`] = ` { "type": "object", "title": "helloworld", - "policies": [], "properties": { "greet": 1 }, "id": [], "required": [ "greet" - ] + ], + "policies": { + "greet": [ + 0 + ] + } }, { "type": "function", "title": "root_greet_fn", - "policies": [ - 0 - ], "input": 2, "output": 5, "runtimeConfig": null, @@ -32,34 +33,36 @@ snapshot[`Typegraph using grpc 1`] = ` { "type": "object", "title": "root_greet_fn_input", - "policies": [], "properties": { "name": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [] + } }, { "type": "optional", "title": "root_greet_fn_input_name_root_greet_fn_input_name_string_optional", - "policies": [], "item": 4, "default_value": null }, { "type": "string", - "title": "root_greet_fn_input_name_string", - "policies": [] + "title": "root_greet_fn_input_name_string" }, { "type": "object", "title": "root_greet_fn_output", - "policies": [], "properties": { "message": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "message": [] + } } ], "materializers": [ @@ -82,7 +85,7 @@ snapshot[`Typegraph using grpc 1`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } } ], @@ -140,21 +143,22 @@ snapshot[`Typegraph using grpc 2`] = ` { "type": "object", "title": "helloworld", - "policies": [], "properties": { "greet": 1 }, "id": [], "required": [ "greet" - ] + ], + "policies": { + "greet": [ + 0 + ] + } }, { "type": "function", "title": "root_greet_fn", - "policies": [ - 0 - ], "input": 2, "output": 5, "runtimeConfig": null, @@ -165,34 +169,36 @@ snapshot[`Typegraph using grpc 2`] = ` { "type": "object", "title": "root_greet_fn_input", - "policies": [], "properties": { "name": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "name": [] + } }, { "type": "optional", "title": "root_greet_fn_input_name_root_greet_fn_input_name_string_optional", - "policies": [], "item": 4, "default_value": null }, { "type": "string", - "title": "root_greet_fn_input_name_string", - "policies": [] + "title": "root_greet_fn_input_name_string" }, { "type": "object", "title": "root_greet_fn_output", - "policies": [], "properties": { "message": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "message": [] + } } ], "materializers": [ @@ -215,7 +221,7 @@ snapshot[`Typegraph using grpc 2`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } } ], diff --git a/tests/runtimes/kv/__snapshots__/kv_test.ts.snap b/tests/runtimes/kv/__snapshots__/kv_test.ts.snap index a47e3b94e..948e9875a 100644 --- a/tests/runtimes/kv/__snapshots__/kv_test.ts.snap +++ b/tests/runtimes/kv/__snapshots__/kv_test.ts.snap @@ -7,7 +7,6 @@ snapshot[`Typegraph using kv 1`] = ` { "type": "object", "title": "kv", - "policies": [], "properties": { "get": 1, "set": 5, @@ -22,14 +21,28 @@ snapshot[`Typegraph using kv 1`] = ` "delete", "keys", "values" - ] + ], + "policies": { + "get": [ + 0 + ], + "set": [ + 0 + ], + "delete": [ + 0 + ], + "keys": [ + 0 + ], + "values": [ + 0 + ] + } }, { "type": "function", "title": "root_get_fn", - "policies": [ - 0 - ], "input": 2, "output": 4, "runtimeConfig": null, @@ -40,31 +53,28 @@ snapshot[`Typegraph using kv 1`] = ` { "type": "object", "title": "root_get_fn_input", - "policies": [], "properties": { "key": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "key": [] + } }, { "type": "string", - "title": "root_get_fn_input_key_string", - "policies": [] + "title": "root_get_fn_input_key_string" }, { "type": "optional", "title": "root_get_fn_output", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "root_set_fn", - "policies": [ - 0 - ], "input": 6, "output": 3, "runtimeConfig": null, @@ -75,20 +85,20 @@ snapshot[`Typegraph using kv 1`] = ` { "type": "object", "title": "root_set_fn_input", - "policies": [], "properties": { "key": 3, "value": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "key": [], + "value": [] + } }, { "type": "function", "title": "root_delete_fn", - "policies": [ - 0 - ], "input": 2, "output": 8, "runtimeConfig": null, @@ -98,15 +108,11 @@ snapshot[`Typegraph using kv 1`] = ` }, { "type": "integer", - "title": "root_delete_fn_output", - "policies": [] + "title": "root_delete_fn_output" }, { "type": "function", "title": "root_keys_fn", - "policies": [ - 0 - ], "input": 10, "output": 11, "runtimeConfig": null, @@ -117,25 +123,23 @@ snapshot[`Typegraph using kv 1`] = ` { "type": "object", "title": "root_keys_fn_input", - "policies": [], "properties": { "filter": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "filter": [] + } }, { "type": "list", "title": "root_keys_fn_output", - "policies": [], "items": 3 }, { "type": "function", "title": "root_values_fn", - "policies": [ - 0 - ], "input": 10, "output": 11, "runtimeConfig": null, @@ -162,7 +166,7 @@ snapshot[`Typegraph using kv 1`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { @@ -255,7 +259,6 @@ snapshot[`Typegraph using kv 2`] = ` { "type": "object", "title": "kv", - "policies": [], "properties": { "get": 1, "set": 5, @@ -270,14 +273,28 @@ snapshot[`Typegraph using kv 2`] = ` "delete", "keys", "values" - ] + ], + "policies": { + "get": [ + 0 + ], + "set": [ + 0 + ], + "delete": [ + 0 + ], + "keys": [ + 0 + ], + "values": [ + 0 + ] + } }, { "type": "function", "title": "root_get_fn", - "policies": [ - 0 - ], "input": 2, "output": 4, "runtimeConfig": null, @@ -288,31 +305,28 @@ snapshot[`Typegraph using kv 2`] = ` { "type": "object", "title": "root_get_fn_input", - "policies": [], "properties": { "key": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "key": [] + } }, { "type": "string", - "title": "root_get_fn_input_key_string", - "policies": [] + "title": "root_get_fn_input_key_string" }, { "type": "optional", "title": "root_get_fn_output", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "root_set_fn", - "policies": [ - 0 - ], "input": 6, "output": 3, "runtimeConfig": null, @@ -323,20 +337,20 @@ snapshot[`Typegraph using kv 2`] = ` { "type": "object", "title": "root_set_fn_input", - "policies": [], "properties": { "key": 3, "value": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "key": [], + "value": [] + } }, { "type": "function", "title": "root_delete_fn", - "policies": [ - 0 - ], "input": 2, "output": 8, "runtimeConfig": null, @@ -346,15 +360,11 @@ snapshot[`Typegraph using kv 2`] = ` }, { "type": "integer", - "title": "root_delete_fn_output", - "policies": [] + "title": "root_delete_fn_output" }, { "type": "function", "title": "root_keys_fn", - "policies": [ - 0 - ], "input": 10, "output": 11, "runtimeConfig": null, @@ -365,25 +375,23 @@ snapshot[`Typegraph using kv 2`] = ` { "type": "object", "title": "root_keys_fn_input", - "policies": [], "properties": { "filter": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "filter": [] + } }, { "type": "list", "title": "root_keys_fn_output", - "policies": [], "items": 3 }, { "type": "function", "title": "root_values_fn", - "policies": [ - 0 - ], "input": 10, "output": 11, "runtimeConfig": null, @@ -410,7 +418,7 @@ snapshot[`Typegraph using kv 2`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { diff --git a/tests/runtimes/s3/__snapshots__/s3_test.ts.snap b/tests/runtimes/s3/__snapshots__/s3_test.ts.snap index 7ffa4caed..48f5b3fdc 100644 --- a/tests/runtimes/s3/__snapshots__/s3_test.ts.snap +++ b/tests/runtimes/s3/__snapshots__/s3_test.ts.snap @@ -6,7 +6,6 @@ snapshot[`s3 typegraphs 1`] = ` { "type": "object", "title": "s3", - "policies": [], "properties": { "listObjects": 1, "getDownloadUrl": 10, @@ -21,14 +20,28 @@ snapshot[`s3 typegraphs 1`] = ` "signTextUploadUrl", "upload", "uploadMany" - ] + ], + "policies": { + "listObjects": [ + 0 + ], + "getDownloadUrl": [ + 0 + ], + "signTextUploadUrl": [ + 0 + ], + "upload": [ + 0 + ], + "uploadMany": [ + 0 + ] + } }, { "type": "function", "title": "root_listObjects_fn", - "policies": [ - 0 - ], "input": 2, "output": 5, "runtimeConfig": null, @@ -39,70 +52,70 @@ snapshot[`s3 typegraphs 1`] = ` { "type": "object", "title": "root_listObjects_fn_input", - "policies": [], "properties": { "path": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "path": [] + } }, { "type": "optional", "title": "root_listObjects_fn_input_path_root_listObjects_fn_input_path_string_optional", - "policies": [], "item": 4, "default_value": null }, { "type": "string", - "title": "root_listObjects_fn_input_path_string", - "policies": [] + "title": "root_listObjects_fn_input_path_string" }, { "type": "object", "title": "root_listObjects_fn_output", - "policies": [], "properties": { "keys": 6, "prefix": 9 }, "id": [], - "required": [] + "required": [], + "policies": { + "keys": [], + "prefix": [] + } }, { "type": "list", "title": "root_listObjects_fn_output_keys_root_listObjects_fn_output_keys_struct_list", - "policies": [], "items": 7 }, { "type": "object", "title": "root_listObjects_fn_output_keys_struct", - "policies": [], "properties": { "key": 4, "size": 8 }, "id": [], - "required": [] + "required": [], + "policies": { + "key": [], + "size": [] + } }, { "type": "integer", - "title": "root_listObjects_fn_output_keys_struct_size_integer", - "policies": [] + "title": "root_listObjects_fn_output_keys_struct_size_integer" }, { "type": "list", "title": "root_listObjects_fn_output_prefix_root_listObjects_fn_input_path_string_list", - "policies": [], "items": 4 }, { "type": "function", "title": "root_getDownloadUrl_fn", - "policies": [ - 0 - ], "input": 11, "output": 12, "runtimeConfig": null, @@ -113,25 +126,23 @@ snapshot[`s3 typegraphs 1`] = ` { "type": "object", "title": "root_getDownloadUrl_fn_input", - "policies": [], "properties": { "path": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "path": [] + } }, { "type": "string", "title": "root_getDownloadUrl_fn_output", - "policies": [], "format": "uri" }, { "type": "function", "title": "root_signTextUploadUrl_fn", - "policies": [ - 0 - ], "input": 14, "output": 12, "runtimeConfig": null, @@ -142,20 +153,20 @@ snapshot[`s3 typegraphs 1`] = ` { "type": "object", "title": "root_signTextUploadUrl_fn_input", - "policies": [], "properties": { "length": 8, "path": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "length": [], + "path": [] + } }, { "type": "function", "title": "root_upload_fn", - "policies": [ - 0 - ], "input": 16, "output": 18, "runtimeConfig": null, @@ -166,33 +177,31 @@ snapshot[`s3 typegraphs 1`] = ` { "type": "object", "title": "root_upload_fn_input", - "policies": [], "properties": { "file": 17, "path": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "file": [], + "path": [] + } }, { "type": "file", "title": "root_upload_fn_input_file_file", - "policies": [], "mimeTypes": [ "text/plain" ] }, { "type": "boolean", - "title": "root_upload_fn_output", - "policies": [] + "title": "root_upload_fn_output" }, { "type": "function", "title": "root_uploadMany_fn", - "policies": [ - 0 - ], "input": 20, "output": 18, "runtimeConfig": null, @@ -203,31 +212,31 @@ snapshot[`s3 typegraphs 1`] = ` { "type": "object", "title": "root_uploadMany_fn_input", - "policies": [], "properties": { "prefix": 21, "files": 22 }, "id": [], - "required": [] + "required": [], + "policies": { + "prefix": [], + "files": [] + } }, { "type": "optional", "title": "root_uploadMany_fn_input_prefix_root_listObjects_fn_input_path_string_optional", - "policies": [], "item": 4, "default_value": "" }, { "type": "list", "title": "root_uploadMany_fn_input_files_root_uploadMany_fn_input_files_file_list", - "policies": [], "items": 23 }, { "type": "file", - "title": "root_uploadMany_fn_input_files_file", - "policies": [] + "title": "root_uploadMany_fn_input_files_file" } ], "materializers": [ @@ -250,7 +259,7 @@ snapshot[`s3 typegraphs 1`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { diff --git a/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap b/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap index f2793011e..acbebcf37 100644 --- a/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap +++ b/tests/runtimes/temporal/__snapshots__/temporal_test.ts.snap @@ -7,7 +7,6 @@ snapshot[`Typegraph using temporal 1`] = ` { "type": "object", "title": "temporal", - "policies": [], "properties": { "start": 1, "query": 6, @@ -20,14 +19,25 @@ snapshot[`Typegraph using temporal 1`] = ` "query", "signal", "describe" - ] + ], + "policies": { + "start": [ + 0 + ], + "query": [ + 0 + ], + "signal": [ + 0 + ], + "describe": [ + 0 + ] + } }, { "type": "function", "title": "root_start_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, "runtimeConfig": null, @@ -38,42 +48,43 @@ snapshot[`Typegraph using temporal 1`] = ` { "type": "object", "title": "root_start_fn_input", - "policies": [], "properties": { "workflow_id": 3, "task_queue": 3, "args": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "task_queue": [], + "args": [] + } }, { "type": "string", - "title": "root_start_fn_input_workflow_id_string", - "policies": [] + "title": "root_start_fn_input_workflow_id_string" }, { "type": "list", "title": "root_start_fn_input_args_root_start_fn_input_args_struct_list", - "policies": [], "items": 5 }, { "type": "object", "title": "root_start_fn_input_args_struct", - "policies": [], "properties": { "some_field": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "some_field": [] + } }, { "type": "function", "title": "root_query_fn", - "policies": [ - 0 - ], "input": 7, "output": 3, "runtimeConfig": null, @@ -84,21 +95,22 @@ snapshot[`Typegraph using temporal 1`] = ` { "type": "object", "title": "root_query_fn_input", - "policies": [], "properties": { "workflow_id": 3, "run_id": 3, "args": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "run_id": [], + "args": [] + } }, { "type": "function", "title": "root_signal_fn", - "policies": [ - 0 - ], "input": 7, "output": 9, "injections": { @@ -118,15 +130,11 @@ snapshot[`Typegraph using temporal 1`] = ` }, { "type": "boolean", - "title": "root_signal_fn_output", - "policies": [] + "title": "root_signal_fn_output" }, { "type": "function", "title": "root_describe_fn", - "policies": [ - 0 - ], "input": 11, "output": 12, "runtimeConfig": null, @@ -137,37 +145,42 @@ snapshot[`Typegraph using temporal 1`] = ` { "type": "object", "title": "root_describe_fn_input", - "policies": [], "properties": { "workflow_id": 3, "run_id": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "run_id": [] + } }, { "type": "object", "title": "root_describe_fn_output", - "policies": [], "properties": { "start_time": 13, "close_time": 13, "state": 13 }, "id": [], - "required": [] + "required": [], + "policies": { + "start_time": [], + "close_time": [], + "state": [] + } }, { "type": "optional", "title": "root_describe_fn_output_start_time_root_describe_fn_output_start_time_integer_optional", - "policies": [], "item": 14, "default_value": null }, { "type": "integer", - "title": "root_describe_fn_output_start_time_integer", - "policies": [] + "title": "root_describe_fn_output_start_time_integer" } ], "materializers": [ @@ -190,7 +203,7 @@ snapshot[`Typegraph using temporal 1`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { @@ -280,7 +293,6 @@ snapshot[`Typegraph using temporal 2`] = ` { "type": "object", "title": "temporal", - "policies": [], "properties": { "startKv": 1, "query": 6, @@ -293,14 +305,25 @@ snapshot[`Typegraph using temporal 2`] = ` "query", "signal", "describe" - ] + ], + "policies": { + "startKv": [ + 0 + ], + "query": [ + 0 + ], + "signal": [ + 0 + ], + "describe": [ + 0 + ] + } }, { "type": "function", "title": "root_startKv_fn", - "policies": [ - 0 - ], "input": 2, "output": 3, "runtimeConfig": null, @@ -311,30 +334,31 @@ snapshot[`Typegraph using temporal 2`] = ` { "type": "object", "title": "root_startKv_fn_input", - "policies": [], "properties": { "workflow_id": 3, "task_queue": 3, "args": 4 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "task_queue": [], + "args": [] + } }, { "type": "string", - "title": "root_startKv_fn_input_workflow_id_string", - "policies": [] + "title": "root_startKv_fn_input_workflow_id_string" }, { "type": "list", "title": "root_startKv_fn_input_args_root_startKv_fn_input_args_struct_list", - "policies": [], "items": 5 }, { "type": "object", "title": "root_startKv_fn_input_args_struct", - "policies": [], "properties": {}, "id": [], "required": [] @@ -342,9 +366,6 @@ snapshot[`Typegraph using temporal 2`] = ` { "type": "function", "title": "root_query_fn", - "policies": [ - 0 - ], "input": 7, "output": 9, "runtimeConfig": null, @@ -355,34 +376,33 @@ snapshot[`Typegraph using temporal 2`] = ` { "type": "object", "title": "root_query_fn_input", - "policies": [], "properties": { "workflow_id": 3, "run_id": 3, "args": 8 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "run_id": [], + "args": [] + } }, { "type": "list", "title": "root_query_fn_input_args_root_startKv_fn_input_workflow_id_string_list", - "policies": [], "items": 3 }, { "type": "optional", "title": "root_query_fn_output", - "policies": [], "item": 3, "default_value": null }, { "type": "function", "title": "root_signal_fn", - "policies": [ - 0 - ], "input": 11, "output": 14, "runtimeConfig": null, @@ -393,43 +413,45 @@ snapshot[`Typegraph using temporal 2`] = ` { "type": "object", "title": "root_signal_fn_input", - "policies": [], "properties": { "workflow_id": 3, "run_id": 3, "args": 12 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "run_id": [], + "args": [] + } }, { "type": "list", "title": "root_signal_fn_input_args_root_signal_fn_input_args_struct_list", - "policies": [], "items": 13 }, { "type": "object", "title": "root_signal_fn_input_args_struct", - "policies": [], "properties": { "key": 3, "value": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "key": [], + "value": [] + } }, { "type": "boolean", - "title": "root_signal_fn_output", - "policies": [] + "title": "root_signal_fn_output" }, { "type": "function", "title": "root_describe_fn", - "policies": [ - 0 - ], "input": 16, "output": 17, "runtimeConfig": null, @@ -440,37 +462,42 @@ snapshot[`Typegraph using temporal 2`] = ` { "type": "object", "title": "root_describe_fn_input", - "policies": [], "properties": { "workflow_id": 3, "run_id": 3 }, "id": [], - "required": [] + "required": [], + "policies": { + "workflow_id": [], + "run_id": [] + } }, { "type": "object", "title": "root_describe_fn_output", - "policies": [], "properties": { "start_time": 18, "close_time": 18, "state": 18 }, "id": [], - "required": [] + "required": [], + "policies": { + "start_time": [], + "close_time": [], + "state": [] + } }, { "type": "optional", "title": "root_describe_fn_output_start_time_root_describe_fn_output_start_time_integer_optional", - "policies": [], "item": 19, "default_value": null }, { "type": "integer", - "title": "root_describe_fn_output_start_time_integer", - "policies": [] + "title": "root_describe_fn_output_start_time_integer" } ], "materializers": [ @@ -493,7 +520,7 @@ snapshot[`Typegraph using temporal 2`] = ` "idempotent": true }, "data": { - "name": "true" + "name": "pass" } }, { diff --git a/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap b/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap index a184622b9..6a15de105 100644 --- a/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap +++ b/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap @@ -53,9 +53,7 @@ snapshot[`typegate: find available operations 1`] = ` enum: null, format: "date-time", optional: false, - policies: [ - '{"create":"__public"}', - ], + policies: [], title: "record_cursor_t3_struct_createdAt_string_datetime", type: "string", }, @@ -113,9 +111,7 @@ snapshot[`typegate: find available operations 1`] = ` enum: null, format: "date-time", optional: false, - policies: [ - '{"create":"__public"}', - ], + policies: [], title: "record_cursor_t3_struct_createdAt_string_datetime", type: "string", }, From 75bea3397d2545fe055d211510b833444fc4bf5f Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Thu, 19 Dec 2024 19:57:40 +0300 Subject: [PATCH 09/11] fix: missed tests --- .../docs/reference/policies/index.mdx | 4 +- src/typegate/src/runtimes/typegate.ts | 43 ++++++++++++------- .../__snapshots__/typegraph_test.ts.snap | 12 +++--- tests/utils/bindings_test.ts | 16 ++++--- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/docs/metatype.dev/docs/reference/policies/index.mdx b/docs/metatype.dev/docs/reference/policies/index.mdx index 63f0b7399..31374d872 100644 --- a/docs/metatype.dev/docs/reference/policies/index.mdx +++ b/docs/metatype.dev/docs/reference/policies/index.mdx @@ -29,7 +29,6 @@ Policies are hierarchical in the sense that the request starts with a denial, an query={require("./policies.graphql")} /> - ## Composition rules ### Traversal order @@ -48,12 +47,13 @@ The evaluation is as follows: - `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` diff --git a/src/typegate/src/runtimes/typegate.ts b/src/typegate/src/runtimes/typegate.ts index 56084f3b1..8d1a08703 100644 --- a/src/typegate/src/runtimes/typegate.ts +++ b/src/typegate/src/runtimes/typegate.ts @@ -554,6 +554,33 @@ function walkPath( ); node = resNode; + const getPolicies = (node: TypeNode) => { + const fieldToPolicies = node.type == "object" + ? Object.entries(node.policies ?? []) + : []; + const ret = []; + for (const [fieldName, indices] of fieldToPolicies) { + const fmtedIndices = indices.map((index) => { + if (typeof index === "number") { + return tg.policy(index).name; + } + + return mapValues(index as Record, (value: number) => { + if (value === null) { + return null; + } + return tg.policy(value).name; + }); + }); + + ret.push( + { fieldName, policies: JSON.stringify(fmtedIndices) }, + ); + } + + return ret; + }; + return { optional: isOptional, title: node.title, @@ -563,20 +590,6 @@ function walkPath( format: format ?? null, fields: node.type == "object" ? collectObjectFields(tg, parent) : null, // TODO enum type on typegraph typegate.py - // FIXME - policies: [], - // policies: node.policies.map((policy) => { - // if (typeof policy === "number") { - // return JSON.stringify(tg.policy(policy).name); - // } - // return JSON.stringify( - // mapValues(policy as Record, (value: number) => { - // if (value === null) { - // return null; - // } - // return tg.policy(value).name; - // }), - // ); - // }), + policies: getPolicies(node), }; } diff --git a/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap b/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap index 65a1bbec0..4f0039f62 100644 --- a/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap +++ b/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap @@ -413,7 +413,7 @@ snapshot[`typegraphs creation 2`] = ` `; snapshot[`typegraphs creation 3`] = ` -'[ +\`[ { "types": [ { @@ -574,7 +574,7 @@ snapshot[`typegraphs creation 3`] = ` "idempotent": true }, "data": { - "script": "var _my_lambda = () => false", + "script": "var _my_lambda = () => 'DENY'", "secrets": [] } }, @@ -666,7 +666,7 @@ snapshot[`typegraphs creation 3`] = ` } } } -]' +]\` `; snapshot[`typegraphs creation 4`] = ` @@ -1082,7 +1082,7 @@ snapshot[`typegraphs creation 5`] = ` `; snapshot[`typegraphs creation 6`] = ` -'[ +\`[ { "types": [ { @@ -1243,7 +1243,7 @@ snapshot[`typegraphs creation 6`] = ` "idempotent": true }, "data": { - "script": "var _my_lambda = () => false", + "script": "var _my_lambda = () => 'DENY'", "secrets": [] } }, @@ -1335,5 +1335,5 @@ snapshot[`typegraphs creation 6`] = ` } } } -]' +]\` `; diff --git a/tests/utils/bindings_test.ts b/tests/utils/bindings_test.ts index 42689515a..627a90d09 100644 --- a/tests/utils/bindings_test.ts +++ b/tests/utils/bindings_test.ts @@ -38,23 +38,27 @@ Deno.test("typegraphValidate", () => { { "type": "object", "title": "introspection", - "policies": [], "properties": { "__type": 1, - "__schema": 64, + "__schema": 26 }, "id": [], "required": [ "__type", - "__schema", + "__schema" ], + "policies": { + "__type": [ + 0 + ], + "__schema": [ + 0 + ] + } }, { "type": "function", "title": "func_79", - "policies": [ - 0, - ], "input": 2, "output": 4, "runtimeConfig": null, From c11642eb191266079ee3a744e8bce880fcba89d2 Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Thu, 19 Dec 2024 21:10:33 +0300 Subject: [PATCH 10/11] fix: last failing test --- .../docs/reference/policies/index.mdx | 6 +- src/typegate/src/typegraphs/typegate.json | 109 ++++++++++-------- src/typegate/src/typegraphs/typegate.py | 4 +- .../typegate_prisma_test.ts.snap | 46 +++++++- .../runtimes/typegate/typegate_prisma_test.ts | 5 +- 5 files changed, 118 insertions(+), 52 deletions(-) diff --git a/docs/metatype.dev/docs/reference/policies/index.mdx b/docs/metatype.dev/docs/reference/policies/index.mdx index 31374d872..50d8f20d4 100644 --- a/docs/metatype.dev/docs/reference/policies/index.mdx +++ b/docs/metatype.dev/docs/reference/policies/index.mdx @@ -43,7 +43,7 @@ If you have `foo.with_policy(A, B).with_policy(C)` for example, it will be merge The evaluation is as follows: -- `ALLOW` and `DENY` compose the same as `true` and `false` +- `ALLOW` and `DENY` compose the same as `true` and `false` under the logical `AND` operator. - `PASS` does not participate. Or more concretely: @@ -54,7 +54,7 @@ Or more concretely: Examples: -- `[DENY, DENY, ALLOW]` -> `DENY` +- `[DENY, DENY, PASS, ALLOW]` -> `DENY` - `[ALLOW, PASS]` -> `ALLOW` - `[PASS, PASS, PASS]` -> `PASS` -- `[]` -> `PASS` +- `[]` -> `PASS` (no policies) diff --git a/src/typegate/src/typegraphs/typegate.json b/src/typegate/src/typegraphs/typegate.json index 022664b41..828df86a1 100644 --- a/src/typegate/src/typegraphs/typegate.json +++ b/src/typegate/src/typegraphs/typegate.json @@ -9,13 +9,13 @@ "addTypegraph": 12, "removeTypegraphs": 22, "argInfoByPath": 26, - "findAvailableOperations": 37, - "findPrismaModels": 45, - "execRawPrismaRead": 51, - "execRawPrismaCreate": 62, - "execRawPrismaUpdate": 63, - "execRawPrismaDelete": 64, - "queryPrismaModel": 65 + "findAvailableOperations": 39, + "findPrismaModels": 47, + "execRawPrismaRead": 53, + "execRawPrismaCreate": 64, + "execRawPrismaUpdate": 65, + "execRawPrismaDelete": 66, + "queryPrismaModel": 67 }, "id": [], "required": [ @@ -351,8 +351,8 @@ "enum": 31, "default": 21, "format": 33, - "policies": 24, - "fields": 34 + "policies": 34, + "fields": 36 }, "id": [], "required": [], @@ -384,16 +384,35 @@ "item": 5, "default_value": null }, + { + "type": "list", + "title": "TypeInfo_policies_TypeInfo_policies_struct_list", + "items": 35 + }, + { + "type": "object", + "title": "TypeInfo_policies_struct", + "properties": { + "fieldName": 5, + "policies": 14 + }, + "id": [], + "required": [], + "policies": { + "fieldName": [], + "policies": [] + } + }, { "type": "optional", "title": "TypeInfo_fields_TypeInfo_fields_TypeInfo_fields_struct_list_optional", - "item": 35, + "item": 37, "default_value": null }, { "type": "list", "title": "TypeInfo_fields_TypeInfo_fields_struct_list", - "items": 36 + "items": 38 }, { "type": "object", @@ -412,8 +431,8 @@ { "type": "function", "title": "root_findAvailableOperations_fn", - "input": 38, - "output": 39, + "input": 40, + "output": 41, "runtimeConfig": null, "materializer": 7, "rate_weight": null, @@ -434,17 +453,17 @@ { "type": "list", "title": "root_findAvailableOperations_fn_output", - "items": 40 + "items": 42 }, { "type": "object", "title": "OperationInfo", "properties": { "name": 5, - "type": 41, - "inputs": 42, + "type": 43, + "inputs": 44, "output": 30, - "outputItem": 44 + "outputItem": 46 }, "id": [], "required": [], @@ -467,7 +486,7 @@ { "type": "list", "title": "OperationInfo_inputs_OperationInfo_inputs_struct_list", - "items": 43 + "items": 45 }, { "type": "object", @@ -492,8 +511,8 @@ { "type": "function", "title": "root_findPrismaModels_fn", - "input": 38, - "output": 46, + "input": 40, + "output": 48, "runtimeConfig": null, "materializer": 8, "rate_weight": null, @@ -502,7 +521,7 @@ { "type": "list", "title": "root_findPrismaModels_fn_output", - "items": 47 + "items": 49 }, { "type": "object", @@ -510,7 +529,7 @@ "properties": { "name": 5, "runtime": 5, - "fields": 48 + "fields": 50 }, "id": [], "required": [], @@ -523,7 +542,7 @@ { "type": "list", "title": "PrismaModelInfo_fields_PrismaModelInfo_fields_struct_list", - "items": 49 + "items": 51 }, { "type": "object", @@ -531,7 +550,7 @@ "properties": { "name": 5, "as_id": 25, - "type": 50 + "type": 52 }, "id": [], "required": [], @@ -551,7 +570,7 @@ "enum": 31, "default": 21, "format": 33, - "policies": 24 + "policies": 34 }, "id": [], "required": [], @@ -568,7 +587,7 @@ { "type": "function", "title": "root_execRawPrismaRead_fn", - "input": 52, + "input": 54, "output": 14, "runtimeConfig": null, "materializer": 9, @@ -581,7 +600,7 @@ "properties": { "typegraph": 5, "runtime": 5, - "query": 53 + "query": 55 }, "id": [], "required": [], @@ -595,8 +614,8 @@ "type": "either", "title": "PrismaQuery", "oneOf": [ - 54, - 56 + 56, + 58 ] }, { @@ -604,7 +623,7 @@ "title": "PrismaSingleQuery", "properties": { "modelName": 33, - "action": 55, + "action": 57, "query": 14 }, "id": [], @@ -643,8 +662,8 @@ "type": "object", "title": "PrismaBatchQuery", "properties": { - "batch": 57, - "transaction": 58 + "batch": 59, + "transaction": 60 }, "id": [], "required": [], @@ -656,19 +675,19 @@ { "type": "list", "title": "PrismaBatchQuery_batch_PrismaSingleQuery_list", - "items": 54 + "items": 56 }, { "type": "optional", "title": "PrismaBatchQuery_transaction_PrismaBatchQuery_transaction_struct_optional", - "item": 59, + "item": 61, "default_value": null }, { "type": "object", "title": "PrismaBatchQuery_transaction_struct", "properties": { - "isolationLevel": 60 + "isolationLevel": 62 }, "id": [], "required": [], @@ -679,7 +698,7 @@ { "type": "optional", "title": "PrismaBatchQuery_transaction_struct_isolationLevel_PrismaBatchQuery_transaction_struct_isolationLevel_string_enum_optional", - "item": 61, + "item": 63, "default_value": null }, { @@ -699,7 +718,7 @@ { "type": "function", "title": "root_execRawPrismaCreate_fn", - "input": 52, + "input": 54, "output": 14, "runtimeConfig": null, "materializer": 10, @@ -709,7 +728,7 @@ { "type": "function", "title": "root_execRawPrismaUpdate_fn", - "input": 52, + "input": 54, "output": 14, "runtimeConfig": null, "materializer": 11, @@ -719,7 +738,7 @@ { "type": "function", "title": "root_execRawPrismaDelete_fn", - "input": 52, + "input": 54, "output": 14, "runtimeConfig": null, "materializer": 12, @@ -729,8 +748,8 @@ { "type": "function", "title": "root_queryPrismaModel_fn", - "input": 66, - "output": 68, + "input": 68, + "output": 70, "runtimeConfig": null, "materializer": 13, "rate_weight": null, @@ -743,8 +762,8 @@ "typegraph": 5, "runtime": 5, "model": 5, - "offset": 67, - "limit": 67 + "offset": 69, + "limit": 69 }, "id": [], "required": [], @@ -764,8 +783,8 @@ "type": "object", "title": "root_queryPrismaModel_fn_output", "properties": { - "fields": 48, - "rowCount": 67, + "fields": 50, + "rowCount": 69, "data": 32 }, "id": [], diff --git a/src/typegate/src/typegraphs/typegate.py b/src/typegate/src/typegraphs/typegate.py index 3bfbcfd2d..5008c472e 100644 --- a/src/typegate/src/typegraphs/typegate.py +++ b/src/typegate/src/typegraphs/typegate.py @@ -178,7 +178,9 @@ def typegate(g: Graph): "enum": t.list(t.json()).optional(), "default": t.json().optional(), "format": t.string().optional(), - "policies": t.list(t.string()), + "policies": t.list( + t.struct({"fieldName": t.string(), "policies": t.json()}) + ), }, name="ShallowTypeInfo", ) diff --git a/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap b/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap index 6a15de105..3dfa21dad 100644 --- a/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap +++ b/tests/runtimes/typegate/__snapshots__/typegate_prisma_test.ts.snap @@ -244,7 +244,28 @@ snapshot[`typegate: find available operations 1`] = ` enum: null, format: null, optional: false, - policies: [], + policies: [ + { + fieldName: "id", + policies: "[]", + }, + { + fieldName: "identities", + policies: "[]", + }, + { + fieldName: "email", + policies: "[]", + }, + { + fieldName: "name", + policies: "[]", + }, + { + fieldName: "messages", + policies: "[]", + }, + ], title: "users", type: "object", }, @@ -302,7 +323,28 @@ snapshot[`typegate: find available operations 1`] = ` enum: null, format: null, optional: false, - policies: [], + policies: [ + { + fieldName: "id", + policies: "[]", + }, + { + fieldName: "identities", + policies: "[]", + }, + { + fieldName: "email", + policies: "[]", + }, + { + fieldName: "name", + policies: "[]", + }, + { + fieldName: "messages", + policies: "[]", + }, + ], title: "users", type: "object", }, diff --git a/tests/runtimes/typegate/typegate_prisma_test.ts b/tests/runtimes/typegate/typegate_prisma_test.ts index 9f15a224d..ed21acdde 100644 --- a/tests/runtimes/typegate/typegate_prisma_test.ts +++ b/tests/runtimes/typegate/typegate_prisma_test.ts @@ -42,7 +42,10 @@ Meta.test({ enum default format - policies + policies { + fieldName + policies + } } } } From f5254100765e47bbded5e0e265537c9c49a8383b Mon Sep 17 00:00:00 2001 From: michael-0acf4 Date: Fri, 20 Dec 2024 15:44:41 +0300 Subject: [PATCH 11/11] fix: insist on PASS for the examples + simplify chain explanation --- .../docs/reference/policies/index.mdx | 22 +++---------------- examples/typegraphs/execute.py | 2 +- examples/typegraphs/execute.ts | 2 +- examples/typegraphs/func.py | 2 +- examples/typegraphs/func.ts | 2 +- examples/typegraphs/policies.py | 2 +- examples/typegraphs/policies.ts | 2 +- .../typegraphs/programmable-api-gateway.py | 2 +- .../typegraphs/programmable-api-gateway.ts | 2 +- examples/typegraphs/reduce.py | 2 +- examples/typegraphs/reduce.ts | 2 +- examples/typegraphs/rest.py | 2 +- examples/typegraphs/rest.ts | 2 +- examples/typegraphs/roadmap-policies.py | 2 +- examples/typegraphs/roadmap-policies.ts | 2 +- src/typegraph/core/src/lib.rs | 6 ++--- 16 files changed, 20 insertions(+), 36 deletions(-) diff --git a/docs/metatype.dev/docs/reference/policies/index.mdx b/docs/metatype.dev/docs/reference/policies/index.mdx index 50d8f20d4..e5f49035c 100644 --- a/docs/metatype.dev/docs/reference/policies/index.mdx +++ b/docs/metatype.dev/docs/reference/policies/index.mdx @@ -37,24 +37,8 @@ Policies are hierarchical in the sense that the request starts with a denial, an - `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 +### Chaining policies -If you have `foo.with_policy(A, B).with_policy(C)` for example, it will be merged into a single chain `[A, B, C]`. +If you have `foo.with_policy(A, B).with_policy(C)` for example, it will evaluated in batch as `[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 -- `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) +If one or more policies fail (`DENY`), the type will be inaccessible. diff --git a/examples/typegraphs/execute.py b/examples/typegraphs/execute.py index 7467a45cb..84284fa0a 100644 --- a/examples/typegraphs/execute.py +++ b/examples/typegraphs/execute.py @@ -53,7 +53,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", """ - (_args, { context }) => !!context.username ? 'ALLOW' : 'DENY' + (_args, { context }) => !!context.username ? 'PASS' : 'DENY' """, ) diff --git a/examples/typegraphs/execute.ts b/examples/typegraphs/execute.ts index 55c111c1f..1e61d0505 100644 --- a/examples/typegraphs/execute.ts +++ b/examples/typegraphs/execute.ts @@ -52,7 +52,7 @@ await typegraph( const admins = deno.policy( "admins", ` - (_args, { context }) => !!context.username ? 'ALLOW' : 'DENY' + (_args, { context }) => !!context.username ? 'PASS' : 'DENY' `, ); diff --git a/examples/typegraphs/func.py b/examples/typegraphs/func.py index aa6d5bb17..cfc4cda60 100644 --- a/examples/typegraphs/func.py +++ b/examples/typegraphs/func.py @@ -58,7 +58,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ) # skip:end diff --git a/examples/typegraphs/func.ts b/examples/typegraphs/func.ts index e2f1f17ca..90ccdfb28 100644 --- a/examples/typegraphs/func.ts +++ b/examples/typegraphs/func.ts @@ -56,7 +56,7 @@ await typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ); // skip:end diff --git a/examples/typegraphs/policies.py b/examples/typegraphs/policies.py index d701796bd..a2557ba66 100644 --- a/examples/typegraphs/policies.py +++ b/examples/typegraphs/policies.py @@ -25,7 +25,7 @@ def policies(g: Graph): ) user_only = deno.policy( "user_only", - "(args, { context }) => context?.username === 'user' ? 'ALLOW' : 'DENY'", + "(args, { context }) => context?.username === 'user' ? 'PASS' : 'DENY'", ) g.auth(Auth.basic(["admin", "user"])) diff --git a/examples/typegraphs/policies.ts b/examples/typegraphs/policies.ts index 0f0d44339..17c9394b2 100644 --- a/examples/typegraphs/policies.ts +++ b/examples/typegraphs/policies.ts @@ -23,7 +23,7 @@ typegraph( ); const user_only = deno.policy( "user_only", - "(args, { context }) => context?.username === 'user' ? 'ALLOW' : 'DENY'", + "(args, { context }) => context?.username === 'user' ? 'PASS' : 'DENY'", ); g.auth(Auth.basic(["admin", "user"])); diff --git a/examples/typegraphs/programmable-api-gateway.py b/examples/typegraphs/programmable-api-gateway.py index 8e9446ced..f78826058 100644 --- a/examples/typegraphs/programmable-api-gateway.py +++ b/examples/typegraphs/programmable-api-gateway.py @@ -18,7 +18,7 @@ def programmable_api_gateway(g: Graph): public = Policy.public() roulette_access = deno.policy( - "roulette", "() => Math.random() < 0.5 ? 'ALLOW' : 'DENY'" + "roulette", "() => Math.random() < 0.5 ? 'PASS' : 'DENY'" ) my_api_format = """ diff --git a/examples/typegraphs/programmable-api-gateway.ts b/examples/typegraphs/programmable-api-gateway.ts index b07471438..ebaf52df3 100644 --- a/examples/typegraphs/programmable-api-gateway.ts +++ b/examples/typegraphs/programmable-api-gateway.ts @@ -17,7 +17,7 @@ typegraph( const pub = Policy.public(); const roulette_access = deno.policy( "roulette", - "() => Math.random() < 0.5 ? 'ALLOW' : 'DENY'", + "() => Math.random() < 0.5 ? 'PASS' : 'DENY'", ); // skip:next-line diff --git a/examples/typegraphs/reduce.py b/examples/typegraphs/reduce.py index 7a5029fef..62afe9c6f 100644 --- a/examples/typegraphs/reduce.py +++ b/examples/typegraphs/reduce.py @@ -51,7 +51,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ) g.expose( diff --git a/examples/typegraphs/reduce.ts b/examples/typegraphs/reduce.ts index fda53d83d..0460066d8 100644 --- a/examples/typegraphs/reduce.ts +++ b/examples/typegraphs/reduce.ts @@ -49,7 +49,7 @@ typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ); g.expose( diff --git a/examples/typegraphs/rest.py b/examples/typegraphs/rest.py index 22595a3c8..1b3aff97d 100644 --- a/examples/typegraphs/rest.py +++ b/examples/typegraphs/rest.py @@ -51,7 +51,7 @@ def roadmap(g: Graph): admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ) g.expose( diff --git a/examples/typegraphs/rest.ts b/examples/typegraphs/rest.ts index 1f3d6cd82..ec242e798 100644 --- a/examples/typegraphs/rest.ts +++ b/examples/typegraphs/rest.ts @@ -49,7 +49,7 @@ typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ); g.expose( diff --git a/examples/typegraphs/roadmap-policies.py b/examples/typegraphs/roadmap-policies.py index 271e43818..dde6b51f9 100644 --- a/examples/typegraphs/roadmap-policies.py +++ b/examples/typegraphs/roadmap-policies.py @@ -56,7 +56,7 @@ def roadmap(g: Graph): # highlight-start admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ) # highlight-end diff --git a/examples/typegraphs/roadmap-policies.ts b/examples/typegraphs/roadmap-policies.ts index d58ce5c07..458ebd9db 100644 --- a/examples/typegraphs/roadmap-policies.ts +++ b/examples/typegraphs/roadmap-policies.ts @@ -50,7 +50,7 @@ typegraph( const admins = deno.policy( "admins", - "(_args, { context }) => !!context.username ? 'ALLOW' : 'DENY'", + "(_args, { context }) => !!context.username ? 'PASS' : 'DENY'", ); g.expose( diff --git a/src/typegraph/core/src/lib.rs b/src/typegraph/core/src/lib.rs index 4c4c60ebb..8c33565b5 100644 --- a/src/typegraph/core/src/lib.rs +++ b/src/typegraph/core/src/lib.rs @@ -214,16 +214,16 @@ impl wit::core::Guest for Lib { .to_string(); let check = match check { - ContextCheck::NotNull => "value != null ? 'ALLOW' : 'DENY'".to_string(), + ContextCheck::NotNull => "value != null ? 'PASS' : 'DENY'".to_string(), ContextCheck::Value(val) => { format!( - "value === {} ? 'ALLOW' : 'DENY'", + "value === {} ? 'PASS' : 'DENY'", serde_json::to_string(&val).unwrap() ) } ContextCheck::Pattern(pattern) => { format!( - "new RegExp({}).test(value) ? 'ALLOW' : 'DENY' ", + "new RegExp({}).test(value) ? 'PASS' : 'DENY' ", serde_json::to_string(&pattern).unwrap() ) }