Skip to content

Commit 445bb5d

Browse files
authored
perf: use predefined function for context check policies (#959)
<!-- Pull requests are squashed and merged using: - their title as the commit message - their description as the commit body Having a good title and description is important for the users to get readable changelog. --> <!-- 1. Explain WHAT the change is about --> Improve performance by running the functions in the main thread when it can be done securely: - Use predefined function for context check policies - Use context check policy for `admin_only` <!-- 2. Explain WHY the change cannot be made simpler --> <!-- 3. Explain HOW users should update their code --> #### Migration notes --- - [ ] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Enhanced predefined function handling with optional parameters. - Introduced a more flexible context checking mechanism. - Simplified policy definition for admin access. - **Improvements** - Updated runtime function registration process. - Improved type safety for predefined function validation. - Streamlined error handling for function materialization. - **Changes** - Removed deprecated error handling functions. - Modified internal representations of predefined functions. - Updated function signatures for predefined Deno functions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 0f699ee commit 445bb5d

File tree

15 files changed

+164
-107
lines changed

15 files changed

+164
-107
lines changed

src/common/src/typegraph/runtimes/deno.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0.
22
// SPDX-License-Identifier: MPL-2.0
33

4+
use anyhow::{anyhow, Context, Result};
45
use indexmap::IndexMap;
56
use serde::{Deserialize, Serialize};
67
use serde_json::Value;
@@ -11,6 +12,47 @@ pub struct FunctionMatData {
1112
pub script: String,
1213
}
1314

15+
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
16+
#[serde(tag = "type", content = "value")]
17+
#[serde(rename_all = "snake_case")]
18+
pub enum ContextCheckX {
19+
NotNull,
20+
Value(String),
21+
Pattern(String),
22+
}
23+
24+
#[derive(PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Debug)]
25+
#[serde(rename_all = "snake_case")]
26+
#[serde(tag = "name", content = "param")]
27+
pub enum PredefinedFunctionMatData {
28+
Identity,
29+
True,
30+
False,
31+
Allow,
32+
Deny,
33+
Pass,
34+
InternalPolicy,
35+
ContextCheck { key: String, value: ContextCheckX },
36+
}
37+
38+
#[derive(Serialize)]
39+
struct PredefinedFunctionMatDataRaw {
40+
name: String,
41+
param: Option<Value>,
42+
}
43+
44+
impl PredefinedFunctionMatData {
45+
pub fn from_raw(name: String, param: Option<String>) -> Result<Self> {
46+
let param = param
47+
.map(|p| serde_json::from_str(&p))
48+
.transpose()
49+
.context("invalid predefined function materializer parameter")?;
50+
let value = serde_json::to_value(&PredefinedFunctionMatDataRaw { name, param })?;
51+
serde_json::from_value(value)
52+
.map_err(|e| anyhow!("invalid predefined function materializer: {e:?}"))
53+
}
54+
}
55+
1456
#[derive(Serialize, Deserialize, Clone, Debug)]
1557
#[serde(rename_all = "camelCase")]
1658
pub struct ModuleMatData {

src/typegate/src/runtimes/deno/deno.ts

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,42 @@ import { WorkerManager } from "./worker_manager.ts";
3030

3131
const logger = getLogger(import.meta);
3232

33-
const predefinedFuncs: Record<string, Resolver<Record<string, unknown>>> = {
34-
identity: ({ _, ...args }) => args,
35-
true: () => true,
36-
false: () => false,
37-
allow: () => "ALLOW" as PolicyResolverOutput,
38-
deny: () => "DENY" as PolicyResolverOutput,
39-
pass: () => "PASS" as PolicyResolverOutput,
40-
internal_policy: ({ _: { context } }) =>
33+
const predefinedFuncs: Record<
34+
string,
35+
(param: any) => Resolver<Record<string, unknown>>
36+
> = {
37+
identity: () => ({ _, ...args }) => args,
38+
true: () => () => true,
39+
false: () => () => false,
40+
allow: () => () => "ALLOW" as PolicyResolverOutput,
41+
deny: () => () => "DENY" as PolicyResolverOutput,
42+
pass: () => () => "PASS" as PolicyResolverOutput,
43+
internal_policy: () => ({ _: { context } }) =>
4144
context.provider === "internal" ? "ALLOW" : "PASS" as PolicyResolverOutput,
45+
context_check: ({ key, value }) => {
46+
let check: (value: any) => boolean;
47+
switch (value.type) {
48+
case "not_null":
49+
check = (v) => v != null;
50+
break;
51+
case "value":
52+
check = (v) => v === value.value;
53+
break;
54+
case "pattern":
55+
check = (v) => v != null && new RegExp(value.value).test(v);
56+
break;
57+
default:
58+
throw new Error("unreachable");
59+
}
60+
const path = key.split(".");
61+
return ({ _: { context } }) => {
62+
let value: any = context;
63+
for (const segment of path) {
64+
value = value?.[segment];
65+
}
66+
return check(value) ? "PASS" : "DENY" as PolicyResolverOutput;
67+
};
68+
},
4269
};
4370

4471
export class DenoRuntime extends Runtime {
@@ -229,7 +256,7 @@ export class DenoRuntime extends Runtime {
229256
if (!func) {
230257
throw new Error(`predefined function ${mat.data.name} not found`);
231258
}
232-
return func;
259+
return func(mat.data.param);
233260
}
234261

235262
if (mat.name === "static") {

src/typegate/src/typegraphs/typegate.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -807,15 +807,21 @@
807807
"data": {}
808808
},
809809
{
810-
"name": "function",
810+
"name": "predefined_function",
811811
"runtime": 0,
812812
"effect": {
813813
"effect": "read",
814814
"idempotent": true
815815
},
816816
"data": {
817-
"script": "var _my_lambda = (_args, { context }) => context.username === 'admin' ? 'ALLOW' : 'DENY' ",
818-
"secrets": []
817+
"name": "context_check",
818+
"param": {
819+
"key": "username",
820+
"value": {
821+
"type": "value",
822+
"value": "admin"
823+
}
824+
}
819825
}
820826
},
821827
{
@@ -942,7 +948,7 @@
942948
],
943949
"policies": [
944950
{
945-
"name": "admin_only",
951+
"name": "__ctx_username_admin",
946952
"materializer": 1
947953
}
948954
],

src/typegate/src/typegraphs/typegate.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0.
22
# SPDX-License-Identifier: MPL-2.0
33

4-
from typegraph import Graph, fx, t, typegraph
4+
from typegraph import Graph, fx, t, typegraph, Policy
55
from typegraph.gen.exports.runtimes import TypegateOperation
66
from typegraph.gen.types import Err
77
from typegraph.graph.params import Auth, Cors, Rate
88
from typegraph.runtimes.base import Materializer
9-
from typegraph.runtimes.deno import DenoRuntime
109
from typegraph.wit import runtimes, store
1110

1211
### Prisma query (Json protocol):
@@ -95,11 +94,7 @@
9594
),
9695
)
9796
def typegate(g: Graph):
98-
deno = DenoRuntime()
99-
admin_only = deno.policy(
100-
"admin_only",
101-
code="(_args, { context }) => context.username === 'admin' ? 'ALLOW' : 'DENY' ",
102-
)
97+
admin_only = Policy.context("username", "admin")
10398

10499
g.auth(Auth.basic(["admin"]))
105100

src/typegraph/core/src/conversion/runtimes.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,7 @@ impl MaterializerConverter for DenoMaterializer {
128128
("import_function".to_string(), data)
129129
}
130130
Predefined(predef) => {
131-
let data = serde_json::from_value(json!({
132-
"name": predef.name,
133-
}))
134-
.unwrap();
131+
let data = serde_json::from_value(serde_json::to_value(predef).unwrap()).unwrap();
135132
("predefined_function".to_string(), data)
136133
}
137134
};

src/typegraph/core/src/errors.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,6 @@ pub fn object_not_found(kind: &str, id: u32) -> TgError {
143143
// .into()
144144
// }
145145

146-
pub fn unknown_predefined_function(name: &str, runtime: &str) -> TgError {
147-
format!("unknown predefined function {name} for runtime {runtime}").into()
148-
}
149-
150146
pub fn duplicate_policy_name(name: &str) -> TgError {
151147
format!("duplicate policy name '{name}'").into()
152148
}

src/typegraph/core/src/global_store.rs

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::wit::utils::Auth as WitAuth;
1414

1515
#[allow(unused)]
1616
use crate::wit::runtimes::{Effect, MaterializerDenoPredefined, MaterializerId};
17+
use common::typegraph::runtimes::deno::PredefinedFunctionMatData;
1718
use graphql_parser::parse_query;
1819
use indexmap::IndexMap;
1920
use std::rc::Rc;
@@ -55,7 +56,7 @@ pub struct Store {
5556
pub policies: Vec<Policy>,
5657

5758
deno_runtime: RuntimeId,
58-
predefined_deno_functions: HashMap<String, MaterializerId>,
59+
predefined_deno_functions: HashMap<PredefinedFunctionMatData, MaterializerId>,
5960
deno_modules: IndexMap<String, MaterializerId>,
6061

6162
public_policy_id: PolicyId,
@@ -88,9 +89,7 @@ impl Store {
8889
runtime_id: deno_runtime,
8990
effect: Effect::Read,
9091
data: MaterializerData::Deno(Rc::new(DenoMaterializer::Predefined(
91-
crate::wit::runtimes::MaterializerDenoPredefined {
92-
name: "pass".to_string(),
93-
},
92+
PredefinedFunctionMatData::Pass,
9493
))),
9594
}],
9695

@@ -104,8 +103,6 @@ impl Store {
104103
}
105104
}
106105

107-
const PREDEFINED_DENO_FUNCTIONS: &[&str] = &["identity", "true"];
108-
109106
thread_local! {
110107
pub static STORE: RefCell<Store> = RefCell::new(Store::new());
111108
pub static SDK_VERSION: String = "0.5.0-rc.9".to_owned();
@@ -350,25 +347,24 @@ impl Store {
350347
with_store(|s| s.public_policy_id)
351348
}
352349

353-
pub fn get_predefined_deno_function(name: String) -> Result<MaterializerId> {
354-
if let Some(mat) = with_store(|s| s.predefined_deno_functions.get(&name).cloned()) {
355-
Ok(mat)
356-
} else if !PREDEFINED_DENO_FUNCTIONS.iter().any(|n| n == &name) {
357-
Err(errors::unknown_predefined_function(&name, "deno"))
350+
pub fn get_predefined_deno_function(
351+
name: String,
352+
param: Option<String>,
353+
) -> Result<MaterializerId> {
354+
let mat = PredefinedFunctionMatData::from_raw(name, param)?;
355+
if let Some(mat_id) = with_store(|s| s.predefined_deno_functions.get(&mat).cloned()) {
356+
Ok(mat_id)
358357
} else {
359358
let runtime_id = Store::get_deno_runtime();
360-
let mat = Store::register_materializer(Materializer {
359+
let mat_id = Store::register_materializer(Materializer {
361360
runtime_id,
362361
effect: Effect::Read,
363-
data: Rc::new(DenoMaterializer::Predefined(MaterializerDenoPredefined {
364-
name: name.clone(),
365-
}))
366-
.into(),
362+
data: Rc::new(DenoMaterializer::Predefined(mat.clone())).into(),
367363
});
368364
with_store_mut(|s| {
369-
s.predefined_deno_functions.insert(name, mat);
365+
s.predefined_deno_functions.insert(mat, mat_id);
370366
});
371-
Ok(mat)
367+
Ok(mat_id)
372368
}
373369
}
374370

src/typegraph/core/src/lib.rs

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ mod test_utils;
1919

2020
use std::collections::HashSet;
2121

22+
use common::typegraph::runtimes::deno::{ContextCheckX, PredefinedFunctionMatData};
2223
use common::typegraph::Injection;
2324
use errors::{Result, TgError};
2425
use global_store::Store;
25-
use indoc::formatdoc;
2626
use params::apply;
2727
use regex::Regex;
2828
use runtimes::{DenoMaterializer, Materializer};
@@ -38,7 +38,7 @@ use wit::core::{
3838
TypeEither, TypeFile, TypeFloat, TypeFunc, TypeId as CoreTypeId, TypeInteger, TypeList,
3939
TypeOptional, TypeString, TypeStruct, TypeUnion, TypegraphInitParams,
4040
};
41-
use wit::runtimes::{Guest, MaterializerDenoFunc};
41+
use wit::runtimes::{Guest as _, MaterializerDenoPredefined};
4242

4343
pub mod wit {
4444
wit_bindgen::generate!({
@@ -52,6 +52,17 @@ pub mod wit {
5252

5353
pub struct Lib {}
5454

55+
impl From<ContextCheck> for ContextCheckX {
56+
fn from(check: ContextCheck) -> Self {
57+
use ContextCheck as CC;
58+
match check {
59+
CC::NotNull => ContextCheckX::NotNull,
60+
CC::Value(v) => ContextCheckX::Value(v),
61+
CC::Pattern(p) => ContextCheckX::Pattern(p),
62+
}
63+
}
64+
}
65+
5566
impl wit::core::Guest for Lib {
5667
fn init_typegraph(params: TypegraphInitParams) -> Result<()> {
5768
typegraph::init(params)
@@ -185,9 +196,7 @@ impl wit::core::Guest for Lib {
185196
}
186197

187198
fn get_internal_policy() -> Result<(PolicyId, String)> {
188-
let deno_mat = DenoMaterializer::Predefined(wit::runtimes::MaterializerDenoPredefined {
189-
name: "internal_policy".to_string(),
190-
});
199+
let deno_mat = DenoMaterializer::Predefined(PredefinedFunctionMatData::InternalPolicy);
191200
let mat = Materializer::deno(deno_mat, crate::wit::runtimes::Effect::Read);
192201
let policy_id = Store::register_policy(
193202
Policy {
@@ -213,42 +222,19 @@ impl wit::core::Guest for Lib {
213222
.replace_all(&name, "_")
214223
.to_string();
215224

216-
let check = match check {
217-
ContextCheck::NotNull => "value != null ? 'PASS' : 'DENY'".to_string(),
218-
ContextCheck::Value(val) => {
219-
format!(
220-
"value === {} ? 'PASS' : 'DENY'",
221-
serde_json::to_string(&val).unwrap()
222-
)
223-
}
224-
ContextCheck::Pattern(pattern) => {
225-
format!(
226-
"new RegExp({}).test(value) ? 'PASS' : 'DENY' ",
227-
serde_json::to_string(&pattern).unwrap()
228-
)
229-
}
230-
};
225+
let check: ContextCheckX = check.into();
226+
let check = serde_json::json!({
227+
"key": key,
228+
"value": check
229+
});
231230

232-
let key = serde_json::to_string(&key).unwrap();
233-
234-
let code = formatdoc! {r#"
235-
(_, {{ context }}) => {{
236-
const chunks = {key}.split(".");
237-
let value = context;
238-
for (const chunk of chunks) {{
239-
value = value?.[chunk];
240-
}}
241-
return {check};
242-
}}
243-
"# };
244-
245-
let mat_id = Lib::register_deno_func(
246-
MaterializerDenoFunc {
247-
code,
248-
secrets: vec![],
249-
},
250-
wit::runtimes::Effect::Read,
251-
)?;
231+
let mat_id = Lib::get_predefined_deno_func(MaterializerDenoPredefined {
232+
name: "context_check".to_string(),
233+
param: Some(
234+
serde_json::to_string(&check)
235+
.map_err(|e| format!("Error while serializing context check: {e:?}"))?,
236+
),
237+
})?;
252238

253239
Lib::register_policy(Policy {
254240
name: name.clone(),

src/typegraph/core/src/runtimes/deno.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright Metatype OÜ, licensed under the Mozilla Public License Version 2.0.
22
// SPDX-License-Identifier: MPL-2.0
33

4+
use common::typegraph::runtimes::deno::PredefinedFunctionMatData;
5+
46
use crate::wit::runtimes as wit;
57

68
#[derive(Debug)]
@@ -25,7 +27,7 @@ pub struct MaterializerDenoStatic {
2527
pub enum DenoMaterializer {
2628
Static(MaterializerDenoStatic),
2729
Inline(wit::MaterializerDenoFunc),
28-
Predefined(wit::MaterializerDenoPredefined),
30+
Predefined(PredefinedFunctionMatData),
2931
Module(MaterializerDenoModule),
3032
Import(MaterializerDenoImport),
3133
}

0 commit comments

Comments
 (0)