From 3c5b1173290c5ea6c469e0bba4572fda4b7c7607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl?= Date: Wed, 18 Oct 2023 23:06:17 +0300 Subject: [PATCH] feat(gate,sdk): update auth interface, better oauth2 (#447) --- Cargo.lock | 14 +- examples/demo/demo.py | 3 +- typegate/src/engine/typecheck/input.ts | 76 +++++ .../src/runtimes/python_wasi/python_wasi.ts | 14 +- typegate/src/services/auth/mod.ts | 15 +- .../src/services/auth/protocols/oauth2.ts | 109 +++++-- typegate/src/typegraph/mod.ts | 40 ++- typegate/src/typegraphs/prisma_migration.py | 3 +- typegate/src/typegraphs/typegate.py | 3 +- typegate/tests/auth/auth.py | 26 +- typegate/tests/auth/auth_test.ts | 4 +- .../auto/__snapshots__/auto_test.ts.snap | 50 ++-- .../__snapshots__/typegraph_test.ts.snap | 4 +- .../e2e/typegraph/typegraphs/deno/complex.ts | 7 +- .../typegraph/typegraphs/python/complex.py | 4 +- typegate/tests/policies/effects.py | 7 +- typegate/tests/policies/policies.py | 6 +- typegate/tests/policies/policies_jwt.py | 6 +- .../tests/policies/policies_jwt_format.py | 5 +- .../tests/policies/policies_jwt_injection.py | 6 +- typegraph/core/src/conversion/params.rs | 22 +- typegraph/core/src/global_store.rs | 26 +- typegraph/core/src/lib.rs | 1 - typegraph/core/src/runtimes/mod.rs | 2 +- typegraph/core/src/typegraph.rs | 48 ++- typegraph/core/src/types.rs | 6 + typegraph/core/src/utils/mod.rs | 282 +++++++++++++++++- typegraph/core/wit/typegraph.wit | 31 +- typegraph/deno/src/params.ts | 59 +++- typegraph/deno/src/typegraph.ts | 14 +- typegraph/deno/src/wit.ts | 5 +- typegraph/python/typegraph/graph/params.py | 247 +++++++-------- typegraph/python/typegraph/graph/typegraph.py | 20 +- website/docs/guides/authentication/basic.py | 7 +- website/docs/guides/authentication/jwt.py | 6 +- website/docs/guides/authentication/oauth2.py | 21 +- .../python/typegraph/graph/params.md | 2 +- .../authentication.py | 19 +- .../policies-and-materializers/policies.py | 5 +- website/src/pages/index.py | 9 +- 40 files changed, 882 insertions(+), 352 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62b56e6faf..79d686d22b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,7 +992,7 @@ dependencies = [ [[package]] name = "common" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "anyhow", "base64 0.21.4", @@ -3238,7 +3238,7 @@ dependencies = [ [[package]] name = "macros" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "proc-macro2", "quote", @@ -3331,7 +3331,7 @@ dependencies = [ [[package]] name = "meta-cli" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "anyhow", "assert_cmd", @@ -3741,7 +3741,7 @@ dependencies = [ [[package]] name = "native" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "anyhow", "base64 0.21.4", @@ -7594,7 +7594,7 @@ dependencies = [ [[package]] name = "typegraph_core" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "common", "enum_dispatch", @@ -7621,7 +7621,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typescript" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "anyhow", "dprint-plugin-typescript", @@ -8469,7 +8469,7 @@ dependencies = [ [[package]] name = "xtask" -version = "0.2.2" +version = "0.2.3-0+dev" dependencies = [ "anyhow", "clap", diff --git a/examples/demo/demo.py b/examples/demo/demo.py index 12fdee3c02..b8fdf4a5d3 100644 --- a/examples/demo/demo.py +++ b/examples/demo/demo.py @@ -5,10 +5,11 @@ @typegraph( - auths=[Auth.oauth2.github("openid profile email")], rate=Rate(window_limit=2000, window_sec=60, query_limit=200), ) def public_api(g: Graph): + g.auth(Auth.oauth2_github("openid profile email")) + deno = DenoRuntime() # 1 what / types diff --git a/typegate/src/engine/typecheck/input.ts b/typegate/src/engine/typecheck/input.ts index 8e3622d99d..c4402498be 100644 --- a/typegate/src/engine/typecheck/input.ts +++ b/typegate/src/engine/typecheck/input.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Elastic-2.0 import { TypeGraph } from "../../typegraph/mod.ts"; +import { TypeNode } from "../../typegraph/types.ts"; import { CodeGenerator } from "./code_generator.ts"; import { mapValues } from "std/collections/map_values.ts"; import { @@ -26,6 +27,81 @@ export function generateValidator(tg: TypeGraph, typeIdx: number): Validator { }; } +/** + * Validate against fields/subfields that only appears on the refered type (if object) + * + * # Example: + * + * ## Typegraph + * ``` + * A = t.struct({ + * a: t.either([t.string(), t.integer()]), + * b: t.struct({ c: t.string(), d: t.integer().optional() })) + * .optional() + * }) + * ``` + * ## Validation + * ``` + * const validatorWeak = generateWeakValidator(tg, idxA); + * validatorWeak({ a: 1, b: { c: "hello" } }); // ok + * validatorWeak({ a: "one", b: { c: "hello", whatever: "world" } }); // ok + * validatorWeak({ a: false, b: { c: "hello" } }); // fail + * validatorWeak({ a: 1, whatever: 1234 }); // ok + * validatorWeak({ whatever: 1234 }); // fail + * ``` + */ +export function generateWeakValidator( + tg: TypeGraph, + typeIdx: number, +): Validator { + const validator = generateValidator(tg, typeIdx); + const node = tg.type(typeIdx); + switch (node.type) { + case "object": + return (value: unknown) => { + const filtered = filterDeclaredFields(tg, value, node, {}); + validator(filtered); + }; + case "optional": + return generateWeakValidator(tg, node.item); + default: { + return validator; + } + } +} + +function filterDeclaredFields( + tg: TypeGraph, + value: any, + node: TypeNode, + result: Record, +): unknown { + switch (node.type) { + case "object": { + const explicitlyDeclared = Object.entries(node.properties); + for (const [field, idx] of explicitlyDeclared) { + const nextNode = tg.type(idx); + result[field] = filterDeclaredFields( + tg, + value[field], + nextNode, + {}, + ); + } + return result; + } + case "optional": + return filterDeclaredFields( + tg, + value, + tg.type(node.item), + {}, + ); + default: + return value; + } +} + function functionName(typeIdx: number) { return `validate_${typeIdx}`; } diff --git a/typegate/src/runtimes/python_wasi/python_wasi.ts b/typegate/src/runtimes/python_wasi/python_wasi.ts index 1ce5f057f4..b11d1b4509 100644 --- a/typegate/src/runtimes/python_wasi/python_wasi.ts +++ b/typegate/src/runtimes/python_wasi/python_wasi.ts @@ -3,7 +3,7 @@ import { getLogger } from "../../log.ts"; import { Runtime } from "../Runtime.ts"; -import { RuntimeInitParams } from "../../types.ts"; +import { Resolver, RuntimeInitParams } from "../../types.ts"; import { ComputeStage } from "../../engine/query_engine.ts"; import { PythonWasmMessenger } from "./python_wasm_messenger.ts"; import { path } from "compress/deps.ts"; @@ -145,12 +145,8 @@ export class PythonWasiRuntime extends Runtime { if (stage.props.materializer != null) { const mat = stage.props.materializer; - const { name } = mat.data ?? {}; - const vmId = generateVmIdentifier(mat); return [ - stage.withResolver((args) => - this.w.execute(name as string, { vmId, args }) - ), + stage.withResolver(this.delegate(mat)), ]; } @@ -166,4 +162,10 @@ export class PythonWasiRuntime extends Runtime { return typeof resolver === "function" ? resolver() : resolver; })]; } + + delegate(mat: Materializer): Resolver { + const { name } = mat.data ?? {}; + const vmId = generateVmIdentifier(mat); + return (args: unknown) => this.w.execute(name as string, { vmId, args }); + } } diff --git a/typegate/src/services/auth/mod.ts b/typegate/src/services/auth/mod.ts index 61990f5ccd..435a3a5d9f 100644 --- a/typegate/src/services/auth/mod.ts +++ b/typegate/src/services/auth/mod.ts @@ -6,7 +6,7 @@ import { BasicAuth } from "./protocols/basic.ts"; import { OAuth2Auth } from "./protocols/oauth2.ts"; import type { Auth } from "../../typegraph/types.ts"; -import { SecretManager } from "../../typegraph/mod.ts"; +import { SecretManager, TypeGraph } from "../../typegraph/mod.ts"; import { Protocol } from "./protocols/protocol.ts"; import { DenoRuntime } from "../../runtimes/deno/deno.ts"; @@ -15,21 +15,32 @@ import { QueryEngine } from "../../engine/query_engine.ts"; import { clearCookie, getEncryptedCookie } from "../auth/cookies.ts"; import { getLogger } from "../../log.ts"; import { methodNotAllowed } from "../../services/responses.ts"; +import { Runtime } from "../../runtimes/Runtime.ts"; const logger = getLogger(import.meta); export const nextAuthorizationHeader = "next-authorization"; export const internalAuthName = "internal"; +export type AdditionalAuthParams = { + tg: TypeGraph; + runtimeReferences: Runtime[]; +}; export function initAuth( typegraphName: string, auth: Auth, secretManager: SecretManager, denoRuntime: DenoRuntime, + authParameters: AdditionalAuthParams, ): Promise { switch (auth.protocol) { case "oauth2": - return OAuth2Auth.init(typegraphName, auth, secretManager, denoRuntime); + return OAuth2Auth.init( + typegraphName, + auth, + secretManager, + authParameters, + ); case "basic": return BasicAuth.init(typegraphName, auth, secretManager, denoRuntime); case "jwt": diff --git a/typegate/src/services/auth/protocols/oauth2.ts b/typegate/src/services/auth/protocols/oauth2.ts index ee6244ecdb..92919adf5f 100644 --- a/typegate/src/services/auth/protocols/oauth2.ts +++ b/typegate/src/services/auth/protocols/oauth2.ts @@ -4,7 +4,7 @@ import config from "../../../config.ts"; import { OAuth2Client, OAuth2ClientConfig, Tokens } from "oauth2_client"; import { encrypt, randomUUID, signJWT, verifyJWT } from "../../../crypto.ts"; -import { JWTClaims } from "../mod.ts"; +import { AdditionalAuthParams, JWTClaims } from "../mod.ts"; import { getLogger } from "../../../log.ts"; import { SecretManager } from "../../../typegraph/mod.ts"; import { @@ -13,17 +13,84 @@ import { setEncryptedSessionCookie, } from "../cookies.ts"; import { Protocol } from "./protocol.ts"; -import { DenoRuntime } from "../../../runtimes/deno/deno.ts"; -import { Auth, Materializer } from "../../../typegraph/types.ts"; +import { Auth } from "../../../typegraph/types.ts"; +import { Type } from "../../../typegraph/type_node.ts"; +import { ComputeStage } from "../../../engine/query_engine.ts"; +import * as ast from "graphql/ast"; +import { + generateValidator, + generateWeakValidator, +} from "../../../engine/typecheck/input.ts"; const logger = getLogger(import.meta); +class AuthProfiler { + constructor( + private authParameters: AdditionalAuthParams, + private funcIndex: number, + ) {} + + private getComputeStage(): ComputeStage { + const { tg, runtimeReferences } = this.authParameters; + const funcNode = tg.type(this.funcIndex, Type.FUNCTION); + const mat = tg.materializer(funcNode.materializer); + const runtime = runtimeReferences[mat.runtime]; + + return new ComputeStage({ + operationName: "", + dependencies: [], + args: (x: any) => x, + operationType: ast.OperationTypeNode.QUERY, + outType: tg.type(funcNode.output), + typeIdx: funcNode.input, + runtime: runtime, + materializer: mat, + node: "", + path: [], + batcher: (x) => x, + rateCalls: false, + rateWeight: 0, + effect: null, + }); + } + + async transform(profile: any, url: string) { + const { tg, runtimeReferences } = this.authParameters; + const funcNode = tg.type(this.funcIndex, Type.FUNCTION); + const mat = tg.materializer(funcNode.materializer); + const runtime = runtimeReferences[mat.runtime]; + const validatorInputWeak = generateWeakValidator(tg, funcNode.input); + const validatorOutput = generateValidator(tg, funcNode.output); + + const input = { ...profile, _: { info: { url } } }; + validatorInputWeak(input); + + // Note: this assumes func is a simple t.func(inp, out, mat) + const stages = runtime.materialize( + this.getComputeStage(), + [], + true, + ); + const resolver = stages.pop()?.props.resolver; + if (typeof resolver != "function") { + throw Error( + `invalid resolver, function was expected but got ${typeof resolver} instead`, + ); + } + + const ret = await resolver(input); + validatorOutput(ret); + + return ret; + } +} + export class OAuth2Auth extends Protocol { static init( typegraphName: string, auth: Auth, secretManager: SecretManager, - denoRuntime: DenoRuntime, + authParameters: AdditionalAuthParams, ): Promise { const clientId = secretManager.secretOrFail(`${auth.name}_CLIENT_ID`); const clientSecret = secretManager.secretOrFail( @@ -46,37 +113,22 @@ export class OAuth2Auth extends Protocol { auth.name, clientData, profile_url as string | null, - denoRuntime, - profiler - ? OAuth2Auth.materializerForProfiler(profiler as string) + profiler !== undefined + ? new AuthProfiler( + authParameters, + profiler as number, + ) : null, ), ); } - static materializerForProfiler( - profiler: string, - ): Materializer { - return { - name: "function", - runtime: -1, // dummy - effect: { - effect: "read", - idempotent: true, - }, - data: { - script: `var _my_lambda = ${profiler};`, - }, - } as Materializer; - } - private constructor( typegraphName: string, private authName: string, private clientData: Omit, private profileUrl: string | null, - private denoRuntime: DenoRuntime, - private profiler: Materializer | null, + private authProfiler: AuthProfiler | null, ) { super(typegraphName); } @@ -216,11 +268,8 @@ export class OAuth2Auth extends Protocol { }); let profile = await res.json(); - if (this.profiler) { - profile = await this.denoRuntime.delegate(this.profiler, false)( - // dummy values - { ...profile, _: { info: { url } } }, - ); + if (this.authProfiler) { + profile = await this.authProfiler!.transform(profile, url); } return profile; diff --git a/typegate/src/typegraph/mod.ts b/typegate/src/typegraph/mod.ts index f9280a8317..8c6fee6ef0 100644 --- a/typegate/src/typegraph/mod.ts +++ b/typegate/src/typegraph/mod.ts @@ -40,7 +40,6 @@ import type { } from "./types.ts"; import { InternalAuth } from "../services/auth/protocols/internal.ts"; import { Protocol } from "../services/auth/protocols/protocol.ts"; -import { OAuth2Auth } from "../services/auth/protocols/oauth2.ts"; import { initRuntime } from "../runtimes/mod.ts"; export { Cors, Rate, TypeGraphDS, TypeMaterializer, TypePolicy, TypeRuntime }; @@ -223,14 +222,6 @@ export class TypeGraph { const denoRuntimeIdx = runtimes.findIndex((r) => r.name === "deno"); ensure(denoRuntimeIdx !== -1, "cannot find deno runtime"); - const additionnalAuthMaterializers = meta.auths.filter((auth) => - auth.auth_data.profiler !== null - ).map(( - auth, - ) => - OAuth2Auth.materializerForProfiler(auth.auth_data.profiler! as string) - ); - const runtimeReferences = await Promise.all( runtimes.map((runtime, idx) => { if (runtime.name in staticReference) { @@ -241,11 +232,6 @@ export class TypeGraph { (mat) => mat.runtime === idx, ); - if (idx === denoRuntimeIdx) { - // register auth materializer - materializers.push(...additionnalAuthMaterializers); - } - return initRuntime(runtime.name, { typegraph, typegraphName, @@ -257,9 +243,20 @@ export class TypeGraph { ); const denoRuntime = runtimeReferences[denoRuntimeIdx]; + ensureNonNullable(denoRuntime, "cannot find deno runtime"); const auths = new Map(); + const tg = new TypeGraph( + typegraph, + secretManager, + denoRuntimeIdx, + runtimeReferences, + cors, + auths, + introspection, + ); + for (const auth of meta.auths) { auths.set( auth.name, @@ -268,21 +265,18 @@ export class TypeGraph { auth, secretManager, denoRuntime as DenoRuntime, + { + tg: tg, // required for validation + runtimeReferences, + }, ), ); } + // override "internal" to enforce internal auth auths.set(internalAuthName, await InternalAuth.init(typegraphName)); - return new TypeGraph( - typegraph, - secretManager, - denoRuntimeIdx, - runtimeReferences, - cors, - auths, - introspection, - ); + return tg; } async deinit(): Promise { diff --git a/typegate/src/typegraphs/prisma_migration.py b/typegate/src/typegraphs/prisma_migration.py index ec3c0345d9..c15b1c66fa 100644 --- a/typegate/src/typegraphs/prisma_migration.py +++ b/typegate/src/typegraphs/prisma_migration.py @@ -12,7 +12,6 @@ @typegraph( name="typegate/prisma_migration", - auths=[Auth.basic(["admin"])], rate=Rate( window_sec=60, window_limit=128, @@ -27,6 +26,8 @@ def prisma_migration(g: Graph): "admin_only", code="(_args, { context }) => context.username === 'admin'" ) + g.auth(Auth.basic(["admin"])) + def _get_operation_func(op: PrismaMigrationOperation): params = runtimes.prisma_migration(store, op) if isinstance(params, Err): diff --git a/typegate/src/typegraphs/typegate.py b/typegate/src/typegraphs/typegate.py index 9d358b1818..96706e919c 100644 --- a/typegate/src/typegraphs/typegate.py +++ b/typegate/src/typegraphs/typegate.py @@ -12,7 +12,6 @@ @typegraph( - auths=[Auth.basic(["admin"])], cors=Cors( allow_origin=["*"], allow_credentials=True, @@ -31,6 +30,8 @@ def typegate(g: Graph): "admin_only", code="(_args, { context }) => context.username === 'admin'" ) + g.auth(Auth.basic(["admin"])) + list_typegraphs_mat_id = runtimes.register_typegate_materializer( store, TypegateOperation.LIST_TYPEGRAPHS ) diff --git a/typegate/tests/auth/auth.py b/typegate/tests/auth/auth.py index 596466c03f..7532d639e0 100644 --- a/typegate/tests/auth/auth.py +++ b/typegate/tests/auth/auth.py @@ -1,15 +1,16 @@ from typegraph import typegraph, Policy, t, Graph -from typegraph.graph.params import oauth2 +from typegraph.graph.params import Auth from typegraph.runtimes.deno import DenoRuntime from typegraph.runtimes.http import HttpRuntime +from typegraph.runtimes.python import PythonRuntime @typegraph( name="test_auth", - auths=[oauth2.github("openid profile email")], ) def test_auth(g: Graph): deno = DenoRuntime() + python = PythonRuntime() remote = HttpRuntime("https://api.github.com") public = Policy.public() @@ -20,6 +21,27 @@ def test_auth(g: Graph): x = t.struct({"x": t.integer()}) + # deno runtime + # g.auth(Auth.oauth2_github("openid profile email")) + + # python runtime + g.auth( + Auth.oauth2( + name="github", + authorize_url="https://github.com/login/oauth/authorize", + access_url="https://github.com/login/oauth/access_token", + # https://docs.github.com/en/rest/reference/users?apiVersion=2022-11-28#get-the-authenticated-user + profile_url="https://api.github.com/user", + # profiler="(p) => ({id: p.id})", + profiler=python.from_lambda( + t.struct({"id": t.integer()}), + t.struct({"id": t.integer()}), + lambda p: {"id": p["id"]}, + ), + scopes="openid profile email", + ) + ) + g.expose( public=deno.identity(x).with_policy(public), private=deno.identity(x).with_policy(private), diff --git a/typegate/tests/auth/auth_test.ts b/typegate/tests/auth/auth_test.ts index a4a3a2ba47..33c2a5e3a3 100644 --- a/typegate/tests/auth/auth_test.ts +++ b/typegate/tests/auth/auth_test.ts @@ -17,8 +17,6 @@ import { JWTClaims } from "../../src/services/auth/mod.ts"; import { getSetCookies } from "std/http/cookie.ts"; import { b64decode } from "../../src/utils.ts"; -mf.install(); - Meta.test("Auth", async (t) => { const clientId = "client_id_1"; const clientSecret = "client_secret_1"; @@ -74,6 +72,8 @@ Meta.test("Auth", async (t) => { .on(e); }); + mf.install(); + await t.should( "refuse to oauth2 flow without redirect uri", async () => { diff --git a/typegate/tests/auto/__snapshots__/auto_test.ts.snap b/typegate/tests/auto/__snapshots__/auto_test.ts.snap index 849f63518d..07e9349b18 100644 --- a/typegate/tests/auto/__snapshots__/auto_test.ts.snap +++ b/typegate/tests/auto/__snapshots__/auto_test.ts.snap @@ -1516,60 +1516,60 @@ snapshot[`Auto-tests for index 4`] = ` }, { description: "union type -_string_filter, object_40", +_string_filter, object_45", fields: null, inputFields: null, - name: "union_41In", + name: "union_46In", }, { description: "union type -string_27, object_30, object_31, object_32, object_33, object_36, object_37, object_38", +string_32, object_35, object_36, object_37, object_38, object_41, object_42, object_43", fields: null, inputFields: null, name: "_string_filterIn", }, { - description: "object_30 input type", + description: "object_35 input type", fields: null, inputFields: [ { name: "equals", }, ], - name: "object_30Inp", + name: "object_35Inp", }, { - description: "object_31 input type", + description: "object_36 input type", fields: null, inputFields: [ { name: "not", }, ], - name: "object_31Inp", + name: "object_36Inp", }, { - description: "object_32 input type", + description: "object_37 input type", fields: null, inputFields: [ { name: "in", }, ], - name: "object_32Inp", + name: "object_37Inp", }, { - description: "object_33 input type", + description: "object_38 input type", fields: null, inputFields: [ { name: "notIn", }, ], - name: "object_33Inp", + name: "object_38Inp", }, { - description: "object_36 input type", + description: "object_41 input type", fields: null, inputFields: [ { @@ -1579,20 +1579,20 @@ string_27, object_30, object_31, object_32, object_33, object_36, object_37, obj name: "mode", }, ], - name: "object_36Inp", + name: "object_41Inp", }, { - description: "object_37 input type", + description: "object_42 input type", fields: null, inputFields: [ { name: "search", }, ], - name: "object_37Inp", + name: "object_42Inp", }, { - description: "object_38 input type", + description: "object_43 input type", fields: null, inputFields: [ { @@ -1602,20 +1602,20 @@ string_27, object_30, object_31, object_32, object_33, object_36, object_37, obj name: "endsWith", }, ], - name: "object_38Inp", + name: "object_43Inp", }, { - description: "object_40 input type", + description: "object_45 input type", fields: null, inputFields: [ { name: "not", }, ], - name: "object_40Inp", + name: "object_45Inp", }, { - description: "object_58 input type", + description: "object_63 input type", fields: null, inputFields: [ { @@ -1625,24 +1625,24 @@ string_27, object_30, object_31, object_32, object_33, object_36, object_37, obj name: "message", }, ], - name: "object_58Inp", + name: "object_63Inp", }, { description: "union type -object_55, _SortOrder", +object_60, _SortOrder", fields: null, inputFields: null, - name: "union_56In", + name: "union_61In", }, { - description: "object_55 input type", + description: "object_60 input type", fields: null, inputFields: [ { name: "sort", }, ], - name: "object_55Inp", + name: "object_60Inp", }, { description: "_feedback_CreateInput input type", diff --git a/typegate/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap b/typegate/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap index a5abfc9405..e8034a2ca9 100644 --- a/typegate/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap +++ b/typegate/tests/e2e/typegraph/__snapshots__/typegraph_test.ts.snap @@ -754,7 +754,7 @@ snapshot[`typegraphs creation 3`] = ` "idempotent": true }, "data": { - "code": "file:scripts/three.ts;base64:H4sIAAAAAAAA/+1Y727bNhD3Zz0FoX6ROkXxv6SDNxXb2q4r0GBBmg0DDEOgJdoWKokaSS32DL/J3mYvtiMp65/tuMWSYCt8QGKRdzzeHckfjxfQJIvJ0s1WnUejLtDlcKh+gdq/g36/1+l1h5e9br/bH8j+Xq/ffdHpdp6Aci4wQ6jzb6nt3P+EnqFXNFuxaL4Q6IoILFYZQT///ZeD4iggKSchytOQMCQWBF3RP6M4xug6nwIXvdcS6FfCeERT1He7rvEMfbh+/dtZwTt7F5JURLOIsBG6un5/BjKGMWM0QXKmOcPZAkVJRplAwqn6WiKu/p9hhhO+lf8+FwsHzGfcQTdYkP1jdqZ5u09/RsGh1VbkWrXaMiwHTxLCXXCJbkVfw/eNZhgGpwm5lQH0kHC5YHkgLAMBrdV/SSZNiTkCNmYMryzhRqkgc8Is20FJlHoDB6U4IZ55e0dN26nGCWjLcTQTEGscw9CM0eXKMj8Us5p2Ib/RP1pPyXUM2zACfdyPGYl9YETpXM2oP626MdifxRQLxVZflrS953bBiTwWEczh05kHi90YlfokzRM1Sn5YYxUNRzs3aYkWgamGbCMlO7xxz0H9iYMCms6iubc2P5IVSJl/4Dgn5maPsZUibXGhxr0ARe5Fc3Zfx2XrvgzRulBdKbAbs6SECxI2lpZEcGiYNa6FsOaGPbHtajV3Vfl3MNxnZAY6txurFcwER7GOpvyy9i3/q2rB9Q4wvit3tF72AA6QJ0+RVSrHcUzv/IARdXhxzL0f4V9tdi2wIBiQgXvjSZuTELGgIXDMt29uzR02BbyB/TI2n9d5ZJlRTvZqTfDSx3PicxJ4cDVoRuEuBhgAaYkG7hTzKIBtJSCCP8hv2St3lgILd5HgoH9xaSn+T9BQbLuYiAGGeBJIqkDcRWkI9sppL7tOuzuOkkiAPV9XnN9zwlYFo9Yd0wDHPlkGhHPvouqH3SvIUvhRiZKemXPCzNJD2wjJDEl7/eLs+nL9uDUfaSSzR0pUYZJXhyPLVowsn0K/RjQ3U8ANHMWauzrklb9yHk+qcmd5GlT92tZyJzkNhnCnlMYEp/VNrEeEsAMtG3kvERwiYlZs21W7W8OuBWYVQ22jc6KnpBKuy9vt4TPBI/lfb/hi2M7/BoNB75T/PQHtT8UUsjj3ZmQPkjEdks9WcH+kpXLVKrOs+g1Wg8edndyGSA1+e8DwfvzUpnhNKw5DKA5DT49xpXd+jJNpiK0WYtbzrjqtd3okmbOI8Xq21QLaUg4uKpqG9wtuGj12G8v3jtNOoOUILceFMRP0lWwUM07ugXb4rakrlml16JqpJVy7Xu/zcPNpLhSX0RoprQ7SitBG3U6qDz0vOs1PcubLuKp4wKJM8HOxYIS4gncegY7gPyD/oI3//cHwhP9PQefnn1MAeBNjLmovf/XiBxUHn/zFAP3sly8fNsMBQbdyt71Ls1woxJuOEDzGpoR9Aw1cNTaGIeEVrgCJE/KlhNQ+lYixRthBU7QZ1ZTJg7mGnvU2sR6hGzjRLPxWP8AccOVjSu/Sl/KRZI/U5Lg+ec2SjeIyInKWlrNJmzpfEvFIZvWPWv47mv9dXlzs1P+6w/7p/D8B/Ufrf06RpDn7y3XHS3FYlbfKWgvU5nr1gtfaxJBDwJk2p43akipj9SGnMIxpQ4NmOLIS4Q0HwJbP9KbGKGyoktkKpIY6gYEyjmVey5a92Vb4fpEPfdAkhY5qklUOyipdenCpS6s+mBt/dsngcKKsQBwqVhV723M4J4YqXy3d41C6mzqN6kCvbzYzra3OWhIHNcKaEhl+B8nQlZpkl9IGSV4UjpBcLrVKG7ulvZEYKp0Fw4TGShYktVkzWfUybWNPsqzuIW2P3n6+8kv+6RVRAqAqoWEeQ7udaN1nkg54e9pTbeREJzrRg9I/dTR9KgAeAAA=" + "code": "file:scripts/three.ts;base64:H4sIAAAAAAAA/+1Y727bNhD3Zz0FoX6ROkXxv2SDNxXb2q4r0GBBmg0DDEOgJdoWKokaSS32DL/J3mYvtiMp65/tuMWSYC18QGKRd/zx7kgejxfQJIvJ0s1WnUejLtDlcKh+gdq/g/5g2Ol1h5e9br/bH8j+Xq/fG3S6nSegnAvMEOr8V2ob95nQM/SSZisWzRcCXRGBxSoj6Jd//nZQHAUk5SREeRoShsSCoCv6VxTHGF3nU+Cid1oC/UYYj2iK+m7XNZ6h99evfj8reGdvQ5KKaBYRNkJX1+/OQMYwZowmSM40ZzhboCjJKBNIOFVfS8TV/zPMcMK38j/kYuGA+ow76AYLsn/MzjRv9uFnFAxabUWuVastw3KwJCHcBZPoVvQVfN9ohmFwmpBb6UAPCZcLlgfCMhDQWv2XZNKUmCNgY8bwyhJulAoyJ8yyHZREqTdwUIoT4pm3d9S0nWqcgLYcRzMBvsYxDM0YXa4s830xq2kX8hv9o3FKrmPYhhHo435MSewDI0rnakb9adWVwf4splgotvqypO49twtG5LGIYA6fzjxY7Mao1CdpnqhR8sMaK2842rhJS7RwTDVk6ynZ4Y17DupPHBTQdBbNvbX5gaxAyvwTxzkxN3uUrYC0xgWMewFA7kVzdl/7ZWu+dNG6gK4A7MYsKeGChI2lJREcGmaNay6smWFPbLtazV0o/w6G+4zMAHO7sVrOTHAUa2/KL2vf8r+sFlzvAOP7ckfrZQ/gAHnyFFklOI5jeucHjKjDi2Pu/QT/arNrgQXBEBm4N560OQkRCxoCx3zz+tbcYVOIN7BfxubzOo8sM8rJXtQEL308Jz4ngdfrdzWjMJfB0ffk+a/0v4vSEKaR0pddp90dR0kkAOabivNHTtiqYNS6Yxrg2CfLgHDuXVT9sOkEWQo/KoObZ+acMLNUzDZCMkMCFtIvjpwv3c6t+UgHIHukRFUo8epRxLIVI8un0K8DkZupeAscxZq7GCKfJcOfO8UcGGNTzvSj/Ja9cJTsHclFgoP+xaWlRH+GhpK0S0zt/cqHUsyT6rmzPA2qfm1/uamcBkO4U0pjgtP6ftYjQtiMlo28FwjOEzErtu2qja4jsAWmFkNto3OiR6cyXJe328Nngkfyv97w6538bzAY9E753xPQ/lRMhSjn3ozsQTKmQ/LZCu6PtARXrTLLqt9gtTi7s5PbsVZH0T1R9f5ArFXxmlpYB+MmDkNPj3GldX6Mk2mIrVaYrOdddVrv9EgyZxHj9WyrFV1LObjxaBreL7hp9NjtAL53nDYCLUdoOS6UmaCvZKOYcXJPPIffGlyxTKtDd0st4dq1ep+Fm48zobiB1kihOkgDoY26klQfel50mh9lzJdxP/GARZng52LBCHEF7zwCHYn/EPkH7fgPJYFT/H8KOj//lALA6xhzUXv5qxc/QBx88hcD9LNfvnzYDAcE3crd9jbNcqEi3nSE4DE2JexbaOCqsTEMGV7hCpBxQr6UkNqnMmKsEXbQFG1GNTB5MNfQs95m6CN0Ayeahd/pB5gDpnxI6V36Qj6S7JGaHNcnr2myUVxGRM7ScjapU+dLIh7JVP5Ry39H87/Li4ud+l932D+d/yeg/2n9zymSNGd/ue54KQ6r8lZZa4HaXK9e8FqbGHIIONPmtFFbUmWsPuQUhjFtIGiGIysR3nAAbPnebyJGYQNKZiuQGuoEBso4lnktW/ZmW+H7VVYMAEkKHUWSD3nKKiw9uMTS0Adz40+uPRxOlFUQh4pVxd72HM6JocpXS/c4lO6mTqMk0OubzUxri1lL4qBGWAOR7neQdF2JJLsUGiR5UThCcrnUKm3sFnojMVSYBcOExkoWJLVaM1n1Mm1jT7Ks7iGtj95+vrJL/ukVUQIAldAwj6HdTrTuU0k7vD3tqSByohOd6EHpX2c797gAHgAA" } }, { @@ -1569,7 +1569,7 @@ snapshot[`typegraphs creation 6`] = ` "idempotent": true }, "data": { - "code": "file:scripts/three.ts;base64:H4sIAAAAAAAA/+1Y727bNhD3Zz8FoS+VN0Wx5DgYtLVY1xZrgQ4N2gwYEBgGLVGOUEnUKGq2YehN9jZ7sd2R1B8raRugS4p2JoKYOt79eDzyjscLeVakbOvKcnRvbQrt/OxM/UIb/s78+dnIm56de1N/6s+Q7nm+NxtNRw/QqlJSQcjoc9twcV9JOz0lz3ixE8n6WpLfmKRyVzDy5p+/HZImIctLFpEqj5gg8pqRFyktZRKS13qI+O7UHQPEu4vnf5wY4smriOUyiRMmgkbgBBjH4yQruJBkT+KtQy444O8cIuEPplwLWlyTmsSCZ8T6uSWdltH704xHcECtHzuE5yznbyuYJmMfEhJ6uDwFdfhA/GklPzhZQQXNSi0wDnleSlLyjF2iXR4T6ZZSVKG092NCeM4CoFAh6M6WbpJLtmbCnjgwRZbkAZmRGvs5zYDRutxwi9QTByTlhqMkL2TCc5qCcCH4dmdb78xU1gT4+sLtACC0ioXae2/TjS7hI8nXgSZDz1Yz02WcciqRrDq2UdVzSFalMgG8N3FAfKMozZcsrzJkx9+lfWXBqi2HWLAEa9HwmKW3vI0p9gQpTFBcZ0CuYBZ/oYwCcgT0z+MEVNyT92wHi/yLphWuEFHrvrotcKPzANadA7A7X7RS2hJBzyZEgXcYE8Obs1KyqL+PLIGzLuyrnuF6S5osJpNu43oQyw3ILQUD6zUHprFgRpNUmRA79mBnn3WbaDa3PZA2yGtLGV4JM52YXT9BttJy1HjJQsFkCbawdNfDPdJdv+vOrIXmB9MBnsYmZJPkEd+8Y2FAzqfOAfF1kiVgR8//oaH/WTGxM+SWmPKQpi+2IStBh3lDhf2VbCv7EcGqSiaM0rVjToEoO11omvLNM8GUDE1hJIb/zOkPv2QUQhKudnFAhwB2zSNlhV9fXFqHg28gyOFJv7K+60bYtuAluwUvo9una6ZM4vnTA30pRA/kNZwYTNwVLZMQnAM36BfsI9X4R8t0ndHQn5/biuslfCgmw6NmVjPY6wl5/KRzEXB0jGHg4Tnb9EOfDYel4ymqFbDouOrCB/wig+JYu3qhdmNl1CBQsG5c5aFtyKQfUpyWKN0V5ymjuT3piPu2h1IRHk9bKQ4OxyynN8rimKEzxltXgKX7IHXTnbjoPlp7G7Q3PLVaInDB7+jY/vPWxPxlc13eQyb4ifzPm0/nw/xvNvP9Y/73AO2L5H9fNPe72MEdkd8VoFDcJhvs7mV9ETfOc9Kww0XbC94fDct3iewGQKlgeA60N+F9ENxpBNmM0RsX95pmq4g2Eb6XEFlxIkpp9VIinSfwPLJu5klauGE0hH0T+VM1CdkGZHv1SOE+WpDv8UMDPlpYTaz/QKQ3xtzdcivdk872XoE5RMvXauMUiXxnaB9XGi+n+uu+mMpQJIUsT+W1YOyeqgCfiP8Q+WfD+O/Pzrxj/H+A9qDxHx4DIqYhI5d42l7lRSVVoFwFBJ5zKyYw6NHuox6PMbJB2MZogC8uos6pehYR6pAVqYMeGPrkvg6Uh6uHR0DeghuL6Cf9kHNgKe9zvsmfqIxSvzlof/KeJrUahTdTJfJ2NtRp9C21MsF0/17Lf5/yfx/7w/rf1D/6/0O0/03+ZypmVNXJ2lpOV+LzDgpoyt0PC1m6SoZ1sX4FbnWA13I5WD8IyNmsz4uVj2GZLon6s6isAlI5VbgyFcEL+LQGJaPfsYbSh0aZO0Bj5YLDtqAmh4hqlmH1SWe5psx0M7O9Q+r68dxXXQhQRet4Gsrtma2qt3bZYQnVxJXT1iEOahCe39SYDjK3Bt8kg6oM2wEqsyhb3kTFMYUMm4yWxT1WG1pPbp1JoRa6b8HHDotwWjdVz1IFXqUDXl9GC33Gm7QX1nej126YEgNM8I8qRcIwk/vmU9djO7Zj+8z2L/88dp8AHgAA" + "code": "file:scripts/three.ts;base64:H4sIAAAAAAAA/+1Y727bNhD3Zz8FoS+VO0Wx5CQYtLVY1xZrgQ4JmgwYEBgGLVGOEEnUKGq2YehN9jZ7sd2R1B87f4EiKdr5YFjU8e7H45E8ni7kWZGylSvLwZPRGOjk6Eg9gXafE//oeOCNj068sT/2J8j3PN+bDMaDZ6CqlFQQMvhS2p3cN0KHh+QtL9YiWVxJ8juTVK4LRk7//cchaRKyvGQRqfKICSKvGHmf0lImIfmku4jvjt0hQJyfvfvzwDAPPkYsl0mcMBE0CgcgOBwmWcGFJBsSrxxyxgF/7RAJPxhyIWhxRWoSC54R65eWdVhG14cZj2CDWj91CO9Yzj9XMEzG7lISurs8BHP4jvqbSt45WEEFzUqtMAx5XkpS8oxdoF9eEemWUlShtDdDQnjOAuBQIejalm6SS7Zgwh45MESW5AGZkBrbOc1A0LpYcovUIwc05ZKjJi9kwnOagnIh+GptW+dmKGsEcn3ltgMQWsNCfXpvs43O4CXJF4FmQ8tWI9NZnHIqka0atjHVc0hWpTIBvNM4IL4xlOYzllcZiuNzZl9aMGvLIRZMwZo2MmbqrWzjig1BDhMU5xmQSxjFnyqngB4B+/M4ARM35JqtYZJ/07TCGSJq3Te3BW5s3oF1jwHYPZ62WtoTQc8nRIF3GCMjm7NSsqi/jiyBvS7sy57jelMaTUejbuF6ELMl6M0EA+81G6bxYEaTVLkQG/bOyr7tFtEsbrshbdDXnjKyEkY6MKt+gGKl5aj+koWCyRJ8Yemmh2ukm37XnFhTLQ+uAzyNTcgyySO+PGdhQE7GzhbzU5Il4EfP/7Hh/1UxsTbslpnykKbvVyErwYbjhgvrK9lK9iOCVZVMGKNrx+wCUXa20DTly7eCKR2aQk8M/8zpd39gFEISzna6xYcAdsUj5YXf3l9Y252nEORwp19aL7setip4yW7By+jqzYIpl3j+uGev+rMXI/LqdbeL4SximIFDmLNlPzrZsJ6dTFHNQUSHPhde4IkCSmLhUohKNoYmd05L6LlUy/0rtpELp82A9UWvMhr6xye2kv0AL0p01KHqGdqNe1EsUMa6cZWHtmGTfixxWqZ055ynjOZ6o2vatC3UinBf2sodcNKY5fR6WRwzPIXxyhXg4j5I3TRHLp4b7RMbfGJkajVXkILnYE9PRU3MnzXX5RNkgg/kf97x+Eb+N5n4/j7/ewb6KvnfV839ztZwR+SPBSiUtMkGu3tZX8TN4TloxOGi7d0M98b8x1wbBkCZYGS2rDd3x06MpxFkM8ZunNwnms0j2gT6XkJkxYkopdVLiXSewPPIupknaeVG0DA2zQWQqkHIKiCryxcK98WU/IAvGvDF1GpC/h0B3zhzfcvl9EQ22xsF5hCtX6uFUyzy0vDuNxrvqPrbvp/KUCSFLA/llWDsiaoAD8R/iPyT3fjvT468ffx/BnrW+A8fAyKmISMXuNs+5kUlVaCcBwQ+5+ZMYNCj3Us9HGJkg7CN0QC/uIjap+qziFCHzEkd9MDwTG7qQJ1w9eERkM9wjEX0s/6Qc2Aq1zlf5q9VYqm/OWh/8J4lteqFb6ZK5O1oaNPge6Iywaz/Sct/D51/H9u79b+xvz//z0H/m/zPVMyoqpO1tZyuxOdtFdDUcd8uZOkqGdbF+hW4+RZeK+Vg/SAgR5O+LFY+dst0SdQfRWUVkMqpwpWpCJ7Bq7VTMvoDayh9aNR5BDSWDTgsC1qyjahG2a0+6SzXlJluZraPSF3vz33VhQBVtE6m4dye2ap6a5cdllBNnDttOWKrFOH5TY1pK3Nr8E0yqMqwHaByi/LlTVTsU8iwyOhZXGO1oPXo1pEUaqHbFryssQinbVP1LFXgVTbg9WWs0Hu8SXthfjda7YIpNcCE81GlyNjN5L771HVPe9rTF9J/rTXzOwAeAAA=" } }, { diff --git a/typegate/tests/e2e/typegraph/typegraphs/deno/complex.ts b/typegate/tests/e2e/typegraph/typegraphs/deno/complex.ts index e41fa986b6..151374c3f8 100644 --- a/typegate/tests/e2e/typegraph/typegraphs/deno/complex.ts +++ b/typegate/tests/e2e/typegraph/typegraphs/deno/complex.ts @@ -43,15 +43,14 @@ typegraph( exposeHeaders: [], maxAgeSec: 120, }, - auths: [ - Auth.basic(["testBasicAuth"]), - Auth.hmac256("testHmacAuth"), - ], }, (g) => { const deno = new DenoRuntime(); const pub = Policy.public(); + g.auth(Auth.basic(["testBasicAuth"])); + g.auth(Auth.hmac256("testHmacAuth")); + g.expose({ test: deno.func( complexType, diff --git a/typegate/tests/e2e/typegraph/typegraphs/python/complex.py b/typegate/tests/e2e/typegraph/typegraphs/python/complex.py index b410298f03..4a08aa1dbb 100644 --- a/typegate/tests/e2e/typegraph/typegraphs/python/complex.py +++ b/typegate/tests/e2e/typegraph/typegraphs/python/complex.py @@ -40,7 +40,6 @@ expose_headers=[], max_age_sec=120, ), - auths=[Auth.basic(["testBasicAuth"]), Auth.hmac256("testHmacAuth")], rate=Rate( window_sec=60, window_limit=128, @@ -53,6 +52,9 @@ def test_complex_types(g: Graph): deno = DenoRuntime() pub = Policy.public() + g.auth(Auth.basic(["testBasicAuth"])) + g.auth(Auth.hmac256("testHmacAuth")) + g.expose( test=deno.func( complexType, diff --git a/typegate/tests/policies/effects.py b/typegate/tests/policies/effects.py index 6e3e2466e0..9ddbea969c 100644 --- a/typegate/tests/policies/effects.py +++ b/typegate/tests/policies/effects.py @@ -3,10 +3,7 @@ from typegraph.runtimes.deno import DenoRuntime -@typegraph( - name="effects", - auths=[Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})], -) +@typegraph(name="effects") def tg_effects(g: Graph): deno = DenoRuntime() public = Policy.public() @@ -23,6 +20,8 @@ def tg_effects(g: Graph): name="User", ).with_policy(Policy.on(read=public, update=admin_only, delete=admin_only)) + g.auth(Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})) + g.expose( public, createUser=deno.func( diff --git a/typegate/tests/policies/policies.py b/typegate/tests/policies/policies.py index f12fee3ba0..f6ae342856 100644 --- a/typegate/tests/policies/policies.py +++ b/typegate/tests/policies/policies.py @@ -3,9 +3,7 @@ from typegraph.runtimes.deno import DenoRuntime -@typegraph( - auths=[Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})] -) +@typegraph() def policies(g: Graph): deno = DenoRuntime() @@ -21,6 +19,8 @@ def policies(g: Graph): t.struct({"a": t.integer()}), ) + 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")), diff --git a/typegate/tests/policies/policies_jwt.py b/typegate/tests/policies/policies_jwt.py index 9677a996ef..8fb1199b71 100644 --- a/typegate/tests/policies/policies_jwt.py +++ b/typegate/tests/policies/policies_jwt.py @@ -5,15 +5,15 @@ from typegraph.runtimes.deno import DenoRuntime -@typegraph( - auths=[Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})] -) +@typegraph() def policies_jwt(g: Graph): deno = DenoRuntime() some_policy = Policy.context("user.name", "some role") regex_policy = Policy.context("user.name", re.compile("[ab]{1}dmin")) + g.auth(Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})) + g.expose( sayHelloWorld=deno.func( t.struct({}), diff --git a/typegate/tests/policies/policies_jwt_format.py b/typegate/tests/policies/policies_jwt_format.py index 334262d15e..28d6ae61b7 100644 --- a/typegate/tests/policies/policies_jwt_format.py +++ b/typegate/tests/policies/policies_jwt_format.py @@ -3,13 +3,12 @@ from typegraph.runtimes.deno import DenoRuntime -@typegraph( - auths=[Auth.hmac256("native")], -) +@typegraph() def policies_jwt_format(g: Graph): deno = DenoRuntime() some_policy = Policy.context("role", "myrole") + g.auth(Auth.hmac256("native")) g.expose( sayHelloWorld=deno.func( t.struct({}), diff --git a/typegate/tests/policies/policies_jwt_injection.py b/typegate/tests/policies/policies_jwt_injection.py index 774ae36c8e..cc9b39c704 100644 --- a/typegate/tests/policies/policies_jwt_injection.py +++ b/typegate/tests/policies/policies_jwt_injection.py @@ -3,9 +3,7 @@ from typegraph.runtimes.deno import DenoRuntime -@typegraph( - auths=[Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})] -) +@typegraph() def policies_jwt_injection(g: Graph): """ This is expected to enforce the typescript generated code to return true @@ -16,6 +14,8 @@ def policies_jwt_injection(g: Graph): deno = DenoRuntime() some_policy = Policy.context("field", '"; return true; "') + g.auth(Auth.jwt("native", "jwk", {"name": "HMAC", "hash": {"name": "SHA-256"}})) + g.expose( sayHelloWorld=deno.func( t.struct({}), t.string(), code="""() => "Hello World!""" diff --git a/typegraph/core/src/conversion/params.rs b/typegraph/core/src/conversion/params.rs index 838f4a767e..f11b391357 100644 --- a/typegraph/core/src/conversion/params.rs +++ b/typegraph/core/src/conversion/params.rs @@ -18,16 +18,6 @@ impl From for Cors { } } -impl From for AuthProtocol { - fn from(value: crate::wit::core::AuthProtocol) -> Self { - match value { - crate::wit::core::AuthProtocol::Oauth2 => AuthProtocol::OAuth2, - crate::wit::core::AuthProtocol::Jwt => AuthProtocol::Jwt, - crate::wit::core::AuthProtocol::Basic => AuthProtocol::Basic, - } - } -} - impl From for Rate { fn from(value: crate::wit::core::Rate) -> Self { Rate { @@ -40,7 +30,17 @@ impl From for Rate { } } -impl crate::wit::core::Auth { +impl From for AuthProtocol { + fn from(value: crate::wit::utils::AuthProtocol) -> Self { + match value { + crate::wit::utils::AuthProtocol::Oauth2 => AuthProtocol::OAuth2, + crate::wit::utils::AuthProtocol::Jwt => AuthProtocol::Jwt, + crate::wit::utils::AuthProtocol::Basic => AuthProtocol::Basic, + } + } +} + +impl crate::wit::utils::Auth { pub fn convert(&self) -> Result { let mut auth_data = IndexMap::new(); for (k, v) in self.auth_data.iter() { diff --git a/typegraph/core/src/global_store.rs b/typegraph/core/src/global_store.rs index d5839330b3..a3494efe28 100644 --- a/typegraph/core/src/global_store.rs +++ b/typegraph/core/src/global_store.rs @@ -5,6 +5,8 @@ use crate::errors::{self, Result}; use crate::runtimes::{DenoMaterializer, Materializer, MaterializerDenoModule, Runtime}; use crate::types::{Struct, Type, TypeFun, TypeId, WrapperTypeData}; use crate::wit::core::{Policy as CorePolicy, PolicyId, RuntimeId}; +use crate::wit::utils::Auth as WitAuth; + use crate::wit::runtimes::{Effect, MaterializerDenoPredefined, MaterializerId}; use graphql_parser::parse_query; use indexmap::IndexMap; @@ -48,6 +50,7 @@ pub struct Store { typegate_runtime: RuntimeId, typegraph_runtime: RuntimeId, graphql_endpoints: Vec, + auths: Vec, } impl Store { @@ -284,7 +287,7 @@ impl Store { } } - pub fn add_graphql_endpoint(graphql: String) -> Result<()> { + pub fn add_graphql_endpoint(graphql: String) -> Result { with_store_mut(|s| { let ast = parse_query::<&str>(&graphql).map_err(|e| e.to_string())?; let endpoints = ast @@ -299,13 +302,32 @@ impl Store { .collect::>(); s.graphql_endpoints.extend(endpoints); - Ok(()) + Ok(s.graphql_endpoints.len() as u32) }) } pub fn get_graphql_endpoints() -> Vec { with_store(|s| s.graphql_endpoints.clone()) } + + pub fn add_auth(auth: WitAuth) -> Result { + with_store_mut(|s| { + let auth = auth.convert()?; + s.auths.push(auth); + Ok(s.auths.len() as u32) + }) + } + + pub fn add_raw_auth(auth: common::typegraph::Auth) -> Result { + with_store_mut(|s| { + s.auths.push(auth); + Ok(s.auths.len() as u32) + }) + } + + pub fn get_auths() -> Vec { + with_store(|s| s.auths.clone()) + } } impl TypeId { diff --git a/typegraph/core/src/lib.rs b/typegraph/core/src/lib.rs index 4ab63a3354..9b6786350d 100644 --- a/typegraph/core/src/lib.rs +++ b/typegraph/core/src/lib.rs @@ -346,7 +346,6 @@ mod tests { allow_credentials: false, max_age_sec: None, }, - auths: vec![], rate: None, } } diff --git a/typegraph/core/src/runtimes/mod.rs b/typegraph/core/src/runtimes/mod.rs index bc4da8c079..ac0bc00d1b 100644 --- a/typegraph/core/src/runtimes/mod.rs +++ b/typegraph/core/src/runtimes/mod.rs @@ -72,7 +72,7 @@ pub struct Materializer { } impl Materializer { - fn deno(data: DenoMaterializer, effect: wit::Effect) -> Self { + pub fn deno(data: DenoMaterializer, effect: wit::Effect) -> Self { Self { runtime_id: Store::get_deno_runtime(), effect, diff --git a/typegraph/core/src/typegraph.rs b/typegraph/core/src/typegraph.rs index 6e11a712b3..9a20709d74 100644 --- a/typegraph/core/src/typegraph.rs +++ b/typegraph/core/src/typegraph.rs @@ -91,11 +91,7 @@ pub fn init(params: TypegraphInitParams) -> Result<()> { }, cors: params.cors.into(), - auths: params - .auths - .iter() - .map(|auth| auth.convert()) - .collect::>>()?, + auths: vec![], prefix: params.prefix, rate: params.rate.map(|v| v.into()), secrets: vec![], @@ -123,16 +119,55 @@ pub fn init(params: TypegraphInitParams) -> Result<()> { Ok(()) } +pub fn finalize_auths(ctx: &mut TypegraphContext) -> Result> { + Store::get_auths() + .iter() + .map(|auth| match auth.protocol { + common::typegraph::AuthProtocol::OAuth2 => { + let profiler_key = "profiler"; + match auth.auth_data.get(profiler_key) { + Some(value) => match value { + serde_json::Value::Null => Ok(auth.to_owned()), + _ => { + let func_store_idx = value + .as_number() + .ok_or_else(|| "profiler has invalid type index".to_string()) + .and_then(|n| { + n.as_u64().ok_or_else(|| { + "unable to convert profiler index".to_string() + }) + })? as u32; + + let type_idx = ctx.register_type(func_store_idx.into(), None)?; + + let mut auth_processed = auth.clone(); + auth_processed + .auth_data + .insert_full(profiler_key.to_string(), type_idx.into()); + + Ok(auth_processed) + } + }, + None => Ok(auth.to_owned()), + } + } + _ => Ok(auth.to_owned()), + }) + .collect::>>() +} + pub fn finalize() -> Result { #[cfg(test)] eprintln!("Finalizing typegraph..."); - let ctx = TG.with(|tg| { + let mut ctx = TG.with(|tg| { tg.borrow_mut() .take() .ok_or_else(errors::expected_typegraph_context) })?; + let auths = finalize_auths(&mut ctx)?; + let tg = Typegraph { id: format!("https://metatype.dev/specs/{TYPEGRAPH_VERSION}.json"), types: ctx @@ -151,6 +186,7 @@ pub fn finalize() -> Result { dynamic: ctx.meta.queries.dynamic, endpoints: Store::get_graphql_endpoints(), }, + auths, ..ctx.meta }, path: None, diff --git a/typegraph/core/src/types.rs b/typegraph/core/src/types.rs index 8ccf9b023e..d465c9affe 100644 --- a/typegraph/core/src/types.rs +++ b/typegraph/core/src/types.rs @@ -45,6 +45,12 @@ impl From for CoreTypeId { } } +impl From for serde_json::Value { + fn from(id: TypeId) -> Self { + id.0.into() + } +} + pub trait TypeData { fn get_display_params_into(&self, params: &mut Vec); fn variant_name(&self) -> String; diff --git a/typegraph/core/src/utils/mod.rs b/typegraph/core/src/utils/mod.rs index a8e8933e08..cdf4b54a21 100644 --- a/typegraph/core/src/utils/mod.rs +++ b/typegraph/core/src/utils/mod.rs @@ -3,11 +3,18 @@ use std::collections::HashMap; +use common::typegraph::{Auth, AuthProtocol}; +use serde_json::json; + use crate::errors::Result; use crate::global_store::Store; +use crate::runtimes::{DenoMaterializer, Materializer}; +use crate::t::TypeBuilder; use crate::types::TypeId; use crate::wit::core::{Guest, TypeBase, TypeId as CoreTypeId, TypeStruct, TypeWithInjection}; -use crate::Lib; +use crate::wit::runtimes::MaterializerDenoFunc; +use crate::wit::utils::Auth as WitAuth; +use crate::{t, Lib}; mod apply; @@ -38,6 +45,51 @@ fn find_missing_props( Ok(missing_props) } +struct Oauth2Params<'a> { + name: &'a str, + authorize_url: &'a str, + access_url: &'a str, + scopes: &'a str, + profile_url: Option<&'a str>, + profiler: Option, +} + +impl TryFrom> for String { + type Error = crate::wit::core::Error; + fn try_from(value: Oauth2Params) -> Result { + let auth_data = json!({ + "authorize_url": serde_json::to_value(value.authorize_url).unwrap(), + "access_url": serde_json::to_value(value.access_url).unwrap(), + "scopes": serde_json::to_value(value.scopes).unwrap(), + "profile_url": serde_json::to_value(value.profile_url).unwrap(), + "profiler": value + .profiler + .map(|p| p.into()) + .unwrap_or(serde_json::Value::Null), + }); + + let ret = serde_json::to_string(&Auth { + name: value.name.to_string(), + protocol: AuthProtocol::OAuth2, + auth_data: serde_json::from_value(auth_data).unwrap(), + }) + .map_err(|e| e.to_string())?; + + Ok(ret) + } +} + +macro_rules! gen_profiler_func { + ($inp:expr, $out: expr, $profiler:expr) => {{ + let deno_mat = DenoMaterializer::Inline(MaterializerDenoFunc { + code: $profiler.to_string(), + secrets: vec![], + }); + let mat = Materializer::deno(deno_mat, crate::wit::runtimes::Effect::Read); + t::func($inp, $out, Store::register_materializer(mat)) + }}; +} + impl crate::wit::utils::Guest for crate::Lib { fn gen_applyb(supertype_id: CoreTypeId, apply: crate::wit::utils::Apply) -> Result { if apply.paths.is_empty() { @@ -119,7 +171,231 @@ impl crate::wit::utils::Guest for crate::Lib { } fn add_graphql_endpoint(graphql: String) -> Result { - Store::add_graphql_endpoint(graphql)?; - Ok(Store::get_graphql_endpoints().len() as u32) + Store::add_graphql_endpoint(graphql) + } + + fn add_auth(data: WitAuth) -> Result { + Store::add_auth(data) + } + + fn add_raw_auth(data: String) -> Result { + let raw_auth: Auth = serde_json::from_str(&data).map_err(|e| e.to_string())?; + Store::add_raw_auth(raw_auth) + } + + fn oauth2(service_name: String, scopes: String) -> Result { + let (name, scopes) = (service_name.as_ref(), scopes.as_ref()); + match name { + "digitalocean" => { + let mut account = t::struct_(); + let inp = t::struct_() + .propx("account", account.propx("uuid", t::string())?)? + .build()?; + let out = t::struct_().propx("id", t::integer())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.account.uuid})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://cloud.digitalocean.com/v1/oauth/authorize", + access_url: "https://cloud.digitalocean.com/v1/oauth/token", + // https://docs.digitalocean.com/reference/api/api-reference/#operation/account_get + scopes, + profile_url: Some("https://api.digitalocean.com/v2/account"), + profiler: Some(func), + }) + } + "discord" => { + let inp = t::struct_().propx("id", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://discord.com/api/oauth2/authorize", + access_url: "https://discord.com/api/oauth2/token", + // https://discord.com/developers/docs/resources/user + scopes, + profile_url: Some("https://discord.com/api/users/@me"), + profiler: Some(func), + }) + } + "dropbox" => { + let inp = t::struct_().propx("account_id", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.account_id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://www.dropbox.com/oauth2/authorize", + access_url: "https://api.dropboxapi.com/oauth2/token", + // https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account + scopes, + profile_url: Some("https://api.dropboxapi.com/2/users/get_current_account"), + profiler: Some(func), + }) + } + "facebook" => { + let inp = t::struct_().propx("id", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://www.facebook.com/v16.0/dialog/oauth", + access_url: "https://graph.facebook.com/v16.0/oauth/access_token", + // https://developers.facebook.com/docs/graph-api/overview#me + // https://developers.facebook.com/docs/graph-api/reference/user/ + scopes, + profile_url: Some("https://graph.facebook.com/me"), + profiler: Some(func), + }) + } + "github" => { + let inp = t::struct_().propx("id", t::integer())?.build()?; + let out = t::struct_().propx("id", t::integer())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://github.com/login/oauth/authorize", + access_url: "https://github.com/login/oauth/access_token", + // https://docs.github.com/en/rest/reference/users?apiVersion=2022-11-28#get-the-authenticated-user + scopes, + profile_url: Some("https://api.github.com/user"), + profiler: Some(func), + }) + } + "gitlab" => { + let inp = t::struct_().propx("id", t::integer())?.build()?; + let out = t::struct_().propx("id", t::integer())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://gitlab.com/oauth/authorize", + access_url: "https://gitlab.com/oauth/token", + // https://docs.gitlab.com/ee/api/users.html#list-current-user + scopes, + profile_url: Some("https://gitlab.com/api/v3/user"), + profiler: Some(func), + }) + } + "google" => { + let inp = t::struct_().propx("localId", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.localId})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://accounts.google.com/o/oauth2/v2/auth", + access_url: "https://oauth2.googleapis.com/token", + // https://cloud.google.com/identity-platform/docs/reference/rest/v1/UserInfo + scopes, + profile_url: Some("https://openidconnect.googleapis.com/v1/userinfo"), + profiler: Some(func), + }) + } + "instagram" => { + let inp = t::struct_().propx("id", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://api.instagram.com/oauth/authorize", + access_url: "https://api.instagram.com/oauth/access_token", + // https://developers.facebook.com/docs/instagram-basic-display-api/reference/me + // https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#reading + scopes, + profile_url: Some("https://graph.instagram.com/me"), + profiler: Some(func), + }) + } + "linkedin" => { + let inp = t::struct_().propx("id", t::integer())?.build()?; + let out = t::struct_().propx("id", t::integer())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://www.linkedin.com/oauth/v2/authorization", + access_url: "https://www.linkedin.com/oauth/v2/accessToken", + // https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api#retrieve-current-members-profile + scopes, + profile_url: Some("https://api.linkedin.com/v2/me"), + profiler: Some(func), + }) + } + "microsoft" => { + let inp = t::struct_().propx("id", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + access_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + // https://learn.microsoft.com/en-us//javascript/api/@microsoft/teams-js/app.userinfo?view=msteams-client-js-latest + scopes, + profile_url: Some("https://graph.microsoft.com/oidc/userinfo"), + profiler: Some(func), + }) + } + "reddit" => { + let inp = t::struct_() + .propx("id", t::eitherx!(t::integer(), t::string()))? + .build()?; + let out = t::struct_() + .propx("id", t::eitherx!(t::integer(), t::string()))? + .build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://www.reddit.com/api/v1/authorize", + access_url: "https://www.reddit.com/api/v1/access_token", + // https://www.reddit.com/dev/api/#GET_api_v1_me + scopes, + profile_url: Some("https://oauth.reddit.com/api/v1/me"), + profiler: Some(func), + }) + } + "slack" => { + let inp = t::struct_().propx("user_id", t::string())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.user_id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://slack.com/oauth/v2/authorize", + access_url: "https://slack.com/api/oauth.v2.access", + // https://api.slack.com/methods/auth.test + scopes, + profile_url: Some("https://slack.com/api/auth.test"), + profiler: Some(func), + }) + } + "stackexchange" => { + let inp = t::struct_().propx("account_id", t::integer())?.build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: `${p.account_id}`})")?; + Ok(Oauth2Params { + name: "stackexchange", + authorize_url: "https://stackoverflow.com/oauth", + access_url: "https://stackoverflow.com/oauth/access_token/json", + // https://api.stackexchange.com/docs/me + scopes, + profile_url: Some("https://api.stackexchange.com/2.3/me"), + profiler: Some(func), + }) + } + "twitter" => { + let mut data = t::struct_(); + let inp = t::struct_() + .propx("data", data.propx("id", t::string())?)? + .build()?; + let out = t::struct_().propx("id", t::string())?.build()?; + let func = gen_profiler_func!(inp, out, "(p) => ({id: p.data.id})")?; + Ok(Oauth2Params { + name, + authorize_url: "https://twitter.com/i/oauth2/authorize", + access_url: "https://api.twitter.com/2/oauth2/token", + // https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me + scopes, + profile_url: Some("https://api.twitter.com/2/users/me"), + profiler: Some(func), + }) + } + _ => Err(format!("service named {:?} not supported", name)), + }? + .try_into() } } diff --git a/typegraph/core/wit/typegraph.wit b/typegraph/core/wit/typegraph.wit index 21b0f9c550..4ea3d457e6 100644 --- a/typegraph/core/wit/typegraph.wit +++ b/typegraph/core/wit/typegraph.wit @@ -16,19 +16,6 @@ interface core { max-age-sec: option } - variant auth-protocol { - oauth2, - jwt, - basic, - } - - record auth { - name: string, - protocol: auth-protocol, - // string => json string - auth-data: list>, - } - record rate { window-limit: u32, window-sec: u32, @@ -44,7 +31,6 @@ interface core { // TypeMeta prefix: option, cors: cors, - auths: list, rate: option, } @@ -494,6 +480,23 @@ interface utils { gen-applyb: func(supertype-id: type-id, data: apply) -> result add-graphql-endpoint: func(graphql: string) -> result + + variant auth-protocol { + oauth2, + jwt, + basic, + } + + record auth { + name: string, + protocol: auth-protocol, + // string => json string + auth-data: list>, + } + + add-auth: func(data: auth) -> result + add-raw-auth: func(data: string) -> result + oauth2: func(service-name: string, scopes: string) -> result } world typegraph { diff --git a/typegraph/deno/src/params.ts b/typegraph/deno/src/params.ts index abb668bae7..74e1dcb000 100644 --- a/typegraph/deno/src/params.ts +++ b/typegraph/deno/src/params.ts @@ -1,7 +1,8 @@ // Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0. // SPDX-License-Identifier: MPL-2.0 -import { Auth as Auth_ } from "./wit.ts"; +import { RawAuth } from "./typegraph.ts"; +import { Auth as Auth_, wit_utils } from "./wit.ts"; export class Auth { static jwt(name: string, format: string, algorithm?: any): Auth_ { @@ -37,4 +38,60 @@ export class Auth { authData, }; } + + static oauth2Digitalocean(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("digitalocean", scopes)); + } + + static oauth2Discord(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("discord", scopes)); + } + + static oauth2Dropbox(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("dropbox", scopes)); + } + + static oauth2Facebook(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("facebook", scopes)); + } + + static oauth2Github(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("github", scopes)); + } + + static oauth2Gitlab(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("gitlab", scopes)); + } + + static oauth2Google(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("google", scopes)); + } + + static oauth2Instagram(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("instagram", scopes)); + } + + static oauth2Linkedin(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("linkedin", scopes)); + } + + static oauth2Microsoft(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("microsoft", scopes)); + } + + static oauth2Reddit(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("reddit", scopes)); + } + + static oauth2Slack(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("slack", scopes)); + } + + static oauth2Stackexchange(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("stackexchange", scopes)); + } + + static oauth2Twitter(scopes: string): RawAuth { + return new RawAuth(wit_utils.oauth2("twitter", scopes)); + } } diff --git a/typegraph/deno/src/typegraph.ts b/typegraph/deno/src/typegraph.ts index 80ae0105ea..06eb03c225 100644 --- a/typegraph/deno/src/typegraph.ts +++ b/typegraph/deno/src/typegraph.ts @@ -23,7 +23,6 @@ interface TypegraphArgs { prefix?: string; secrets?: Array; cors?: Cors; - auths?: Array; rate?: Rate; } @@ -31,6 +30,7 @@ interface TypegraphBuilderArgs { expose: (exports: Exports, defaultPolicy?: Policy) => void; inherit: () => InheritDef; rest: (graphql: string) => number; + auth: (value: Auth | RawAuth) => number; } export class InheritDef { @@ -63,6 +63,10 @@ export class InheritDef { type TypegraphBuilder = (g: TypegraphBuilderArgs) => void; +export class RawAuth { + constructor(readonly jsonStr: string) {} +} + export function typegraph( name: string, builder: TypegraphBuilder, @@ -85,7 +89,6 @@ export function typegraph( const { name, dynamic, - auths, cors, prefix, rate, @@ -118,7 +121,6 @@ export function typegraph( exposeHeaders: [], maxAgeSec: undefined, } as Cors, - auths: auths ?? [], rate, }; @@ -137,6 +139,12 @@ export function typegraph( rest: (graphql: string) => { return wit_utils.addGraphqlEndpoint(graphql); }, + auth: (value: Auth | RawAuth) => { + if (value instanceof RawAuth) { + return wit_utils.addRawAuth(value.jsonStr); + } + return wit_utils.addAuth(value); + }, }; builder(g); diff --git a/typegraph/deno/src/wit.ts b/typegraph/deno/src/wit.ts index f8eb18b522..ce0fb4412b 100644 --- a/typegraph/deno/src/wit.ts +++ b/typegraph/deno/src/wit.ts @@ -12,12 +12,11 @@ export const runtimes = js.runtimes as typeof MetatypeTypegraphRuntimes; export const aws = js.aws as typeof MetatypeTypegraphAws; export const wit_utils = js.utils as typeof MetatypeTypegraphUtils; +export type { Cors, Rate } from "./gen/interfaces/metatype-typegraph-core.d.ts"; export type { Auth, AuthProtocol, AuthProtocolBasic, AuthProtocolJwt, AuthProtocolOauth2, - Cors, - Rate, -} from "./gen/interfaces/metatype-typegraph-core.d.ts"; +} from "./gen/interfaces/metatype-typegraph-utils.d.ts"; diff --git a/typegraph/python/typegraph/graph/params.py b/typegraph/python/typegraph/graph/params.py index 00db1abb19..44e4cf8573 100644 --- a/typegraph/python/typegraph/graph/params.py +++ b/typegraph/python/typegraph/graph/params.py @@ -3,9 +3,14 @@ from dataclasses import dataclass import json -from typing import List, Optional -from typegraph.gen.exports import core -from box import Box +from typing import List, Optional, TYPE_CHECKING +from typegraph.gen.exports import utils +from typegraph.wit import store, wit_utils + +from typegraph.gen.types import Err + +if TYPE_CHECKING: + from typegraph import t class Rate: @@ -58,7 +63,7 @@ def __init__( class Auth: - def jwt(name: str, format: str, algorithm: None) -> "core.Auth": + def jwt(name: str, format: str, algorithm: None) -> "utils.Auth": """ [Documentation](http://localhost:3000/docs/guides/authentication#jwt-authentication) """ @@ -70,14 +75,14 @@ def jwt(name: str, format: str, algorithm: None) -> "core.Auth": ("algorithm", json.dumps(algorithm)), ] - return core.Auth(name, core.AuthProtocolJwt(), auth_data) + return utils.Auth(name, utils.AuthProtocolJwt(), auth_data) - def hmac256(name: str) -> "core.Auth": + def hmac256(name: str) -> "utils.Auth": return Auth.jwt(name, "raw", {"name": "HMAC", "hash": {"name": "SHA-256"}}) - def basic(users: List[str]) -> "core.Auth": + def basic(users: List[str]) -> "utils.Auth": auth_data = [("users", json.dumps(users))] - return core.Auth("basic", core.AuthProtocolBasic(), auth_data) + return utils.Auth("basic", utils.AuthProtocolBasic(), auth_data) @classmethod def oauth2( @@ -87,22 +92,108 @@ def oauth2( access_url: str, scopes: str, profile_url: Optional[str] = None, - profiler: Optional[str] = None, - ) -> "core.Auth": - return core.Auth( + profiler: Optional["t.func"] = None, + ) -> "utils.Auth": + return utils.Auth( name, - core.AuthProtocolOauth2(), + utils.AuthProtocolOauth2(), [ ("authorize_url", json.dumps(authorize_url)), ("access_url", json.dumps(access_url)), ("scopes", json.dumps(scopes)), ("profile_url", json.dumps(profile_url)), - ("profiler", json.dumps(profiler)), + ("profiler", json.dumps(None if profiler is None else profiler.id)), ], ) + def oauth2_digitalocean(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "digitalocean", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_discord(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "discord", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_dropbox(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "dropbox", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_facebook(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "facebook", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_github(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "github", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_gitlab(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "gitlab", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_google(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "google", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_instagram(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "instagram", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_linkedin(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "linkedin", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_microsoft(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "microsoft", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_reddit(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "reddit", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_slack(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "slack", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_stackexchange(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "stackexchange", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) + + def oauth2_twitter(scopes: str) -> "utils.Auth": + res = wit_utils.oauth2(store, "twitter", scopes) + if isinstance(res, Err): + raise Exception(res.value) + return RawAuth(res.value) -oauth2 = dict() + +@dataclass +class RawAuth: + json_str: str @dataclass @@ -110,130 +201,4 @@ class OAuth2Params: authorize_url: str access_url: str profile_url: Optional[str] - profiler: Optional[str] - - -_oauth2_params = { - "digitalocean": OAuth2Params( - authorize_url="https://cloud.digitalocean.com/v1/oauth/authorize", - access_url="https://cloud.digitalocean.com/v1/oauth/token", - # https://docs.digitalocean.com/reference/api/api-reference/#operation/account_get - profile_url="https://api.digitalocean.com/v2/account", - profiler="(p) => ({id: p.account.uuid})", - ), - "discord": OAuth2Params( - authorize_url="https://discord.com/api/oauth2/authorize", - access_url="https://discord.com/api/oauth2/token", - # https://discord.com/developers/docs/resources/user - profile_url="https://discord.com/api/users/@me", - profiler="(p) => ({id: p.id})", - ), - "dropbox": OAuth2Params( - authorize_url="https://www.dropbox.com/oauth2/authorize", - access_url="https://api.dropboxapi.com/oauth2/token", - # https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account - profile_url="https://api.dropboxapi.com/2/users/get_current_account", - profiler="(p) => ({id: p.account_id})", - ), - "facebook": OAuth2Params( - authorize_url="https://www.facebook.com/v16.0/dialog/oauth", - access_url="https://graph.facebook.com/v16.0/oauth/access_token", - # https://developers.facebook.com/docs/graph-api/overview#me - # https://developers.facebook.com/docs/graph-api/reference/user/ - profile_url="https://graph.facebook.com/me", - profiler="(p) => ({id: p.id})", - ), - "github": OAuth2Params( - authorize_url="https://github.com/login/oauth/authorize", - access_url="https://github.com/login/oauth/access_token", - # https://docs.github.com/en/rest/reference/users?apiVersion=2022-11-28#get-the-authenticated-user - profile_url="https://api.github.com/user", - profiler="(p) => ({id: p.id})", - ), - "gitlab": OAuth2Params( - authorize_url="https://gitlab.com/oauth/authorize", - access_url="https://gitlab.com/oauth/token", - # https://docs.gitlab.com/ee/api/users.html#list-current-user - profile_url="https://gitlab.com/api/v3/user", - profiler="(p) => ({id: p.id})", - ), - "google": OAuth2Params( - authorize_url="https://accounts.google.com/o/oauth2/v2/auth", - access_url="https://oauth2.googleapis.com/token", - # https://cloud.google.com/identity-platform/docs/reference/rest/v1/UserInfo - profile_url="https://openidconnect.googleapis.com/v1/userinfo", - profiler="(p) => ({id: p.localId})", - ), - "instagram": OAuth2Params( - authorize_url="https://api.instagram.com/oauth/authorize", - access_url="https://api.instagram.com/oauth/access_token", - # https://developers.facebook.com/docs/instagram-basic-display-api/reference/me - # https://developers.facebook.com/docs/instagram-basic-display-api/reference/user#reading - profile_url="https://graph.instagram.com/me", - profiler="(p) => ({id: p.id})", - ), - "linkedin": OAuth2Params( - authorize_url="https://www.linkedin.com/oauth/v2/authorization", - access_url="https://www.linkedin.com/oauth/v2/accessToken", - # https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api#retrieve-current-members-profile - profile_url="https://api.linkedin.com/v2/me", - profiler="(p) => ({id: p.id})", - ), - "microsoft": OAuth2Params( - authorize_url="https://login.microsoftonline.com/common/oauth2/v2.0/authorize", - access_url="https://login.microsoftonline.com/common/oauth2/v2.0/token", - # https://learn.microsoft.com/en-us//javascript/api/@microsoft/teams-js/app.userinfo?view=msteams-client-js-latest - profile_url="https://graph.microsoft.com/oidc/userinfo", - profiler="(p) => ({id: p.id})", - ), - "reddit": OAuth2Params( - authorize_url="https://www.reddit.com/api/v1/authorize", - access_url="https://www.reddit.com/api/v1/access_token", - # https://www.reddit.com/dev/api/#GET_api_v1_me - profile_url="https://oauth.reddit.com/api/v1/me", - profiler="(p) => ({id: p.id})", - ), - "slack": OAuth2Params( - authorize_url="https://slack.com/oauth/v2/authorize", - access_url="https://slack.com/api/oauth.v2.access", - # https://api.slack.com/methods/auth.test - profile_url="https://slack.com/api/auth.test", - profiler="(p) => ({id: p.user_id})", - ), - "stackexchange": OAuth2Params( - authorize_url="https://stackoverflow.com/oauth", - access_url="https://stackoverflow.com/oauth/access_token/json", - # https://api.stackexchange.com/docs/me - profile_url="https://api.stackexchange.com/2.3/me", - profiler="(p) => ({id: `${p.account_id}`})", - ), - "twitter": OAuth2Params( - authorize_url="https://twitter.com/i/oauth2/authorize", - access_url="https://api.twitter.com/2/oauth2/token", - # https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me - profile_url="https://api.twitter.com/2/users/me", - profiler="(p) => ({id: p.data.id})", - ), -} - - -def __gen(auth_name: str, details: OAuth2Params): - def auth(scopes: str, name=None): - return Auth.oauth2( - name if name is not None else auth_name, - details.authorize_url, - details.access_url, - scopes, - details.profile_url, - details.profiler, - ) - - return auth - - -oauth2 = Box( - { - auth_name: __gen(auth_name, params) - for auth_name, params in _oauth2_params.items() - } -) + profiler: Optional[int] diff --git a/typegraph/python/typegraph/graph/typegraph.py b/typegraph/python/typegraph/graph/typegraph.py index 0af79ac046..60048a1c56 100644 --- a/typegraph/python/typegraph/graph/typegraph.py +++ b/typegraph/python/typegraph/graph/typegraph.py @@ -7,15 +7,15 @@ from typing import TYPE_CHECKING, Callable, List, Optional, Union from typegraph.gen.exports.core import ( - Auth, Rate, TypegraphInitParams, ) from typegraph.gen.exports.core import ( Cors as CoreCors, ) + from typegraph.gen.types import Err -from typegraph.graph.params import Cors +from typegraph.graph.params import Auth, Cors, RawAuth from typegraph.policy import Policy, PolicyPerEffect, PolicySpec, get_policy_chain from typegraph.wit import core, store, wit_utils @@ -32,7 +32,6 @@ class Typegraph: dynamic: Optional[bool] path: str _context: List["Typegraph"] = [] - auths: Optional[List[Auth]] rate: Optional[Rate] cors: Optional[CoreCors] prefix: Optional[str] @@ -42,7 +41,6 @@ def __init__( name: str, dynamic: Optional[bool] = None, *, - auths: Optional[List[Auth]] = None, rate: Optional[Rate] = None, cors: Optional[Cors] = None, prefix: Optional[str] = None, @@ -51,7 +49,6 @@ def __init__( self.dynamic = dynamic self.path = str(Path(inspect.stack()[2].filename).resolve().parent) - self.auths = auths or [] self.rate = rate cors = cors or Cors() @@ -108,12 +105,21 @@ def rest(self, graphql: str) -> int: raise Exception(res.value) return res.value + def auth(self, value: Union[Auth, RawAuth]): + res = ( + wit_utils.add_raw_auth(store, value.json_str) + if isinstance(value, RawAuth) + else wit_utils.add_auth(store, value) + ) + if isinstance(res, Err): + raise Exception(res.value) + return res.value + def typegraph( name: Optional[str] = None, *, dynamic: Optional[bool] = None, - auths: Optional[List[Auth]] = None, rate: Optional[Rate] = None, cors: Optional[Cors] = None, prefix: Optional[str] = None, @@ -129,7 +135,6 @@ def decorator(builder: Callable[[Graph], None]) -> Typegraph: tg = Typegraph( name=actual_name, dynamic=dynamic, - auths=auths, rate=rate, cors=cors, prefix=prefix, @@ -143,7 +148,6 @@ def decorator(builder: Callable[[Graph], None]) -> Typegraph: name=tg.name, dynamic=tg.dynamic, path=tg.path, - auths=tg.auths, rate=tg.rate, cors=tg.cors, prefix=tg.prefix, diff --git a/website/docs/guides/authentication/basic.py b/website/docs/guides/authentication/basic.py index 3c11d073d0..81232acac0 100644 --- a/website/docs/guides/authentication/basic.py +++ b/website/docs/guides/authentication/basic.py @@ -7,10 +7,6 @@ @typegraph( - auths=[ - # highlight-next-line - Auth.basic(["admin"]), - ], # skip:next-line cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), ) @@ -20,6 +16,9 @@ def basic_authentication(g: Graph): ctx = t.struct({"username": t.string().optional().from_context("username")}) + # highlight-next-line + g.auth(Auth.basic(["admin"])) + g.expose( public, get_context=deno.identity(ctx), diff --git a/website/docs/guides/authentication/jwt.py b/website/docs/guides/authentication/jwt.py index 937737d252..9759e8777b 100644 --- a/website/docs/guides/authentication/jwt.py +++ b/website/docs/guides/authentication/jwt.py @@ -7,10 +7,6 @@ @typegraph( - auths=[ - # highlight-next-line - Auth.hmac256("custom"), - ], # skip:next-line cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), ) @@ -21,6 +17,8 @@ def jwt_authentication(g: Graph): ctx = t.struct( {"your_own_content": t.string().optional().from_context("your_own_content")} ) + # highlight-next-line + g.auth(Auth.hmac256("custom")) g.expose( get_context=deno.identity(ctx), diff --git a/website/docs/guides/authentication/oauth2.py b/website/docs/guides/authentication/oauth2.py index 5109a6a5f1..fbd9e040e6 100644 --- a/website/docs/guides/authentication/oauth2.py +++ b/website/docs/guides/authentication/oauth2.py @@ -7,16 +7,6 @@ @typegraph( - auths=[ - # highlight-start - Auth.oauth2( - "github", - "https://github.com/login/oauth/authorize", - "https://github.com/login/oauth/access_token", - "openid profile email", - ), - # highlight-end - ], # skip:next-line cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), ) @@ -26,6 +16,17 @@ def oauth2_authentication(g: Graph): ctx = t.struct({"exp": t.integer().optional().from_context("exp")}) + # highlight-start + g.auth( + Auth.oauth2( + "github", + "https://github.com/login/oauth/authorize", + "https://github.com/login/oauth/access_token", + "openid profile email", + ) + ) + # highlight-end + g.expose( public, get_context=deno.identity(ctx), diff --git a/website/docs/reference/typegraph/python/typegraph/graph/params.md b/website/docs/reference/typegraph/python/typegraph/graph/params.md index 94a68aae16..649e34470a 100644 --- a/website/docs/reference/typegraph/python/typegraph/graph/params.md +++ b/website/docs/reference/typegraph/python/typegraph/graph/params.md @@ -12,7 +12,7 @@ class Auth() #### jwt ```python -def jwt(name: str, format: str, algorithm: None) -> "core.Auth" +def jwt(name: str, format: str, algorithm: None) -> "utils.Auth" ``` [Documentation](http://localhost:3000/docs/guides/authentication#jwt-authentication) diff --git a/website/docs/tutorials/authentication-and-security/authentication.py b/website/docs/tutorials/authentication-and-security/authentication.py index b0d1bc564b..53157fa161 100644 --- a/website/docs/tutorials/authentication-and-security/authentication.py +++ b/website/docs/tutorials/authentication-and-security/authentication.py @@ -7,16 +7,10 @@ @typegraph( - auths=[ - # highlight-start - # expects a secret in metatype.yml - # `TG_[typegraph]_BASIC_[username]` - # highlight-next-line - Auth.basic(["admin"]), - # highlight-end - ], - # skip:next-line + # skip:start cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), + # skip:end + # .. ) def authentication(g: Graph): deno = DenoRuntime() @@ -24,6 +18,13 @@ def authentication(g: Graph): ctx = t.struct({"username": t.string().optional().from_context("username")}) + # highlight-start + # expects a secret in metatype.yml + # `TG_[typegraph]_BASIC_[username]` + # highlight-next-line + g.auth(Auth.basic(["admin"])) + # highlight-end + g.expose( get_context=deno.identity(ctx), default_policy=[public], diff --git a/website/docs/tutorials/policies-and-materializers/policies.py b/website/docs/tutorials/policies-and-materializers/policies.py index 3ee0b60d6e..03b82e4a00 100644 --- a/website/docs/tutorials/policies-and-materializers/policies.py +++ b/website/docs/tutorials/policies-and-materializers/policies.py @@ -8,9 +8,6 @@ @typegraph( - auths=[ - Auth.basic(["admin", "user"]), - ], cors=Cors( allow_origin=["https://metatype.dev", "http://localhost:3000"], ), @@ -29,6 +26,8 @@ def policies(g: Graph): "(args, { context }) => context.username ? context.username === 'user' : null", ) + g.auth(Auth.basic(["admin", "user"])) + g.expose( public=random.gen(t.string()).with_policy(public), admin_only=random.gen(t.string()).with_policy(admin_only), diff --git a/website/src/pages/index.py b/website/src/pages/index.py index 2d2e3f71e1..79e9597e3b 100644 --- a/website/src/pages/index.py +++ b/website/src/pages/index.py @@ -2,16 +2,13 @@ import re from typegraph import Graph, Policy, t, typegraph -from typegraph.graph.params import Cors, Rate, oauth2 +from typegraph.graph.params import Auth, Cors, Rate from typegraph.providers import PrismaRuntime from typegraph.runtimes import HttpRuntime # skip:end @typegraph( - # skip:next-line - # out of the box authenfication support - auths=[oauth2.github("openid email")], # skip:start rate=Rate(window_limit=2000, window_sec=60, query_limit=200), cors=Cors(allow_origin=["https://metatype.dev", "http://localhost:3000"]), @@ -50,6 +47,10 @@ def homepage(g: Graph): } ) + # skip:next-line + # out of the box authenfication support + g.auth(Auth.oauth2_github("openid email")) + # expose part of the graph for queries g.expose( public,