Skip to content

Commit

Permalink
feat(gate,sdk): update auth interface, better oauth2 (#447)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-0acf4 authored Oct 18, 2023
1 parent 97c70cc commit 3c5b117
Show file tree
Hide file tree
Showing 40 changed files with 882 additions and 352 deletions.
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion examples/demo/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions typegate/src/engine/typecheck/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<string, unknown>,
): 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}`;
}
Expand Down
14 changes: 8 additions & 6 deletions typegate/src/runtimes/python_wasi/python_wasi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)),
];
}

Expand All @@ -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 });
}
}
15 changes: 13 additions & 2 deletions typegate/src/services/auth/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<Protocol> {
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":
Expand Down
109 changes: 79 additions & 30 deletions typegate/src/services/auth/protocols/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Protocol> {
const clientId = secretManager.secretOrFail(`${auth.name}_CLIENT_ID`);
const clientSecret = secretManager.secretOrFail(
Expand All @@ -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<OAuth2ClientConfig, "redirectUri">,
private profileUrl: string | null,
private denoRuntime: DenoRuntime,
private profiler: Materializer | null,
private authProfiler: AuthProfiler | null,
) {
super(typegraphName);
}
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 3c5b117

Please sign in to comment.