From 266bae426fcd177a9b6a9d9b5fd202d5f88dba21 Mon Sep 17 00:00:00 2001 From: Luis Sousa Date: Thu, 16 Jan 2025 13:55:54 -0300 Subject: [PATCH 1/3] feat: create server only access control for loaders and actions --- blocks/action.ts | 13 +++++++++---- blocks/loader.ts | 10 ++++++++-- blocks/utils.tsx | 32 +++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/blocks/action.ts b/blocks/action.ts index 88dbc5ed8..124f79539 100644 --- a/blocks/action.ts +++ b/blocks/action.ts @@ -1,14 +1,19 @@ // deno-lint-ignore-file no-explicit-any -import { applyProps, type FnProps } from "../blocks/utils.tsx"; +import { + applyProps, + type FnProps, + GateKeeperAccess, +} from "../blocks/utils.tsx"; import JsonViewer from "../components/JsonViewer.tsx"; import type { Block, BlockModule, InstanceOf } from "../engine/block.ts"; +import { gateKeeper } from "./utils.tsx"; export type Action = InstanceOf; -export type ActionModule< +export interface ActionModule< TProps = any, TResp = any, -> = BlockModule>; +> extends BlockModule>, GateKeeperAccess {} const actionBlock: Block = { type: "actions", @@ -17,7 +22,7 @@ const actionBlock: Block = { >( mod: ActionModule, ) => [ - applyProps(mod), + applyProps(gateKeeper(mod)), ], defaultPreview: (result) => { return { diff --git a/blocks/loader.ts b/blocks/loader.ts index 0edb2491d..a02a94f03 100644 --- a/blocks/loader.ts +++ b/blocks/loader.ts @@ -19,6 +19,8 @@ import { applyProps, type FnContext, type FnProps, + gateKeeper, + type GateKeeperAccess, type RequestState, type SingleFlightKeyFunc, } from "./utils.tsx"; @@ -28,7 +30,7 @@ export type Loader = InstanceOf; export interface LoaderModule< TProps = any, TState = any, -> extends BlockModule> { +> extends BlockModule>, GateKeeperAccess { /** * Specifies caching behavior for the loader and its dependencies. * @@ -335,7 +337,11 @@ const loaderBlock: Block = { adapt: (mod: LoaderModule) => [ wrapCaughtErrors, (props: TProps, ctx: HttpContext<{ global: any } & RequestState>) => - applyProps(wrapLoader(mod, ctx.resolveChain, ctx.context.state.release))( + applyProps( + gateKeeper( + wrapLoader(mod, ctx.resolveChain, ctx.context.state.release), + ), + )( props, ctx, ), diff --git a/blocks/utils.tsx b/blocks/utils.tsx index 89e8c2ade..cddbc5c02 100644 --- a/blocks/utils.tsx +++ b/blocks/utils.tsx @@ -5,7 +5,7 @@ import type { StatusCode as Status } from "@std/http/status"; import type { JSX } from "preact"; import type { AppManifest, ImportMap } from "../blocks/app.ts"; -import { isInvokeCtx } from "../blocks/loader.ts"; +import { isInvokeCtx, LoaderModule } from "../blocks/loader.ts"; import type { InvocationFunc } from "../clients/withManifest.ts"; import { withSection } from "../components/section.tsx"; import type { @@ -27,6 +27,11 @@ import type { InvocationProxy } from "../utils/invoke.types.ts"; import { type Device, deviceOf, isBot as isUABot } from "../utils/userAgent.ts"; import type { HttpContext } from "./handler.ts"; import type { Vary } from "../utils/vary.ts"; +import { ActionModule } from "./action.ts"; + +export interface GateKeeperAccess { + visibility?: "server" | "public"; +} export type SingleFlightKeyFunc = ( args: TConfig, @@ -138,6 +143,7 @@ export const fnContextFromHttpContext = ( }, }; }; + /** * Applies the given props to the target block function. * @@ -248,3 +254,27 @@ export const buildImportMap = (manifest: AppManifest): ImportMap => { ); return buildImportMapWith(manifest, builder); }; + +export const gateKeeper = ( + { + default: handler, + visibility = "public", + ...rest + }: BlockModule & GateKeeperAccess, +) => { + return { + ...rest, + default: async ( + props: Parameters[0], + req: Request, + ctx: FnContext, + ): Promise> => { + if (visibility === "server" && !ctx.isInvoke) { + return new Response(null, { + status: 403, + }); + } + return await handler(props, req, ctx); + }, + }; +}; From 9dcf96b164012f3cdbc79713560df56811090efe Mon Sep 17 00:00:00 2001 From: Luis Sousa Date: Thu, 16 Jan 2025 15:20:55 -0300 Subject: [PATCH 2/3] rename gateKeeper acces key and values --- blocks/action.ts | 2 +- blocks/utils.tsx | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/blocks/action.ts b/blocks/action.ts index 124f79539..d40e806a0 100644 --- a/blocks/action.ts +++ b/blocks/action.ts @@ -2,7 +2,7 @@ import { applyProps, type FnProps, - GateKeeperAccess, + type GateKeeperAccess, } from "../blocks/utils.tsx"; import JsonViewer from "../components/JsonViewer.tsx"; import type { Block, BlockModule, InstanceOf } from "../engine/block.ts"; diff --git a/blocks/utils.tsx b/blocks/utils.tsx index cddbc5c02..f7189ba9a 100644 --- a/blocks/utils.tsx +++ b/blocks/utils.tsx @@ -27,10 +27,9 @@ import type { InvocationProxy } from "../utils/invoke.types.ts"; import { type Device, deviceOf, isBot as isUABot } from "../utils/userAgent.ts"; import type { HttpContext } from "./handler.ts"; import type { Vary } from "../utils/vary.ts"; -import { ActionModule } from "./action.ts"; export interface GateKeeperAccess { - visibility?: "server" | "public"; + defaultVisibility?: "private" | "public"; } export type SingleFlightKeyFunc = ( @@ -258,7 +257,7 @@ export const buildImportMap = (manifest: AppManifest): ImportMap => { export const gateKeeper = ( { default: handler, - visibility = "public", + defaultVisibility = "public", ...rest }: BlockModule & GateKeeperAccess, ) => { @@ -269,7 +268,7 @@ export const gateKeeper = ( req: Request, ctx: FnContext, ): Promise> => { - if (visibility === "server" && !ctx.isInvoke) { + if (defaultVisibility === "private" && !ctx.isInvoke) { return new Response(null, { status: 403, }); From 74908d82ed46f5504d5af96a8e9f823fcd50b33f Mon Sep 17 00:00:00 2001 From: Luis Sousa Date: Thu, 16 Jan 2025 19:25:15 -0300 Subject: [PATCH 3/3] feat: add block visibility override --- blocks/action.ts | 4 +++- blocks/loader.ts | 7 +++--- blocks/utils.tsx | 43 +++++++++++++++++-------------------- deco.ts | 10 ++++++++- engine/block.ts | 2 +- engine/manifest/manifest.ts | 2 ++ runtime/fresh/plugin.tsx | 4 +++- runtime/mod.ts | 3 +++ 8 files changed, 44 insertions(+), 31 deletions(-) diff --git a/blocks/action.ts b/blocks/action.ts index d40e806a0..40baf2ae4 100644 --- a/blocks/action.ts +++ b/blocks/action.ts @@ -21,8 +21,10 @@ const actionBlock: Block = { TProps = any, >( mod: ActionModule, + key: string, ) => [ - applyProps(gateKeeper(mod)), + gateKeeper(mod.defaultVisibility, key), + applyProps(mod), ], defaultPreview: (result) => { return { diff --git a/blocks/loader.ts b/blocks/loader.ts index a02a94f03..45f36631e 100644 --- a/blocks/loader.ts +++ b/blocks/loader.ts @@ -334,13 +334,12 @@ const wrapLoader = ( const loaderBlock: Block = { type: "loaders", introspect: { includeReturn: true }, - adapt: (mod: LoaderModule) => [ + adapt: (mod: LoaderModule, key: string) => [ + gateKeeper(mod.defaultVisibility, key), wrapCaughtErrors, (props: TProps, ctx: HttpContext<{ global: any } & RequestState>) => applyProps( - gateKeeper( - wrapLoader(mod, ctx.resolveChain, ctx.context.state.release), - ), + wrapLoader(mod, ctx.resolveChain, ctx.context.state.release), )( props, ctx, diff --git a/blocks/utils.tsx b/blocks/utils.tsx index f7189ba9a..8a9e81c89 100644 --- a/blocks/utils.tsx +++ b/blocks/utils.tsx @@ -5,7 +5,7 @@ import type { StatusCode as Status } from "@std/http/status"; import type { JSX } from "preact"; import type { AppManifest, ImportMap } from "../blocks/app.ts"; -import { isInvokeCtx, LoaderModule } from "../blocks/loader.ts"; +import { isInvokeCtx } from "../blocks/loader.ts"; import type { InvocationFunc } from "../clients/withManifest.ts"; import { withSection } from "../components/section.tsx"; import type { @@ -27,6 +27,7 @@ import type { InvocationProxy } from "../utils/invoke.types.ts"; import { type Device, deviceOf, isBot as isUABot } from "../utils/userAgent.ts"; import type { HttpContext } from "./handler.ts"; import type { Vary } from "../utils/vary.ts"; +import { Context } from "../mod.ts"; export interface GateKeeperAccess { defaultVisibility?: "private" | "public"; @@ -254,26 +255,22 @@ export const buildImportMap = (manifest: AppManifest): ImportMap => { return buildImportMapWith(manifest, builder); }; -export const gateKeeper = ( - { - default: handler, - defaultVisibility = "public", - ...rest - }: BlockModule & GateKeeperAccess, -) => { - return { - ...rest, - default: async ( - props: Parameters[0], - req: Request, - ctx: FnContext, - ): Promise> => { - if (defaultVisibility === "private" && !ctx.isInvoke) { - return new Response(null, { - status: 403, - }); - } - return await handler(props, req, ctx); - }, +export const gateKeeper = + (defaultVisibility: GateKeeperAccess["defaultVisibility"], key: string) => + < + TContext extends ResolverMiddlewareContext = ResolverMiddlewareContext< + any + >, + >(_props: unknown, ctx: TContext) => { + const currentContext = Context.active(); + + const visibility = currentContext.visibilityOverrides?.[key] ?? + defaultVisibility ?? "public"; + + if (visibility === "private" && !isInvokeCtx(ctx)) { + return new Response(null, { + status: 403, + }); + } + return ctx.next!(); }; -}; diff --git a/deco.ts b/deco.ts index 3bb1c89a1..6612b5242 100644 --- a/deco.ts +++ b/deco.ts @@ -6,6 +6,8 @@ import type { ReleaseResolver } from "./engine/core/mod.ts"; import type { DecofileProvider } from "./engine/decofile/provider.ts"; import type { AppManifest } from "./types.ts"; import { randId } from "./utils/rand.ts"; +import { BlockKeys } from "./mod.ts"; +import { GateKeeperAccess } from "./blocks/utils.tsx"; export interface DecoRuntimeState< TAppManifest extends AppManifest = AppManifest, @@ -55,6 +57,12 @@ export interface DecoContext { runtime?: Promise>; instance: InstanceInfo; request?: RequestContext; + + visibilityOverrides?: Record< + BlockKeys extends undefined ? string + : BlockKeys, + GateKeeperAccess["defaultVisibility"] + >; } export interface RequestContextBinder { @@ -107,7 +115,7 @@ export const Context = { // Function to retrieve the active context active: (): DecoContext => { // Retrieve the context associated with the async ID - return asyncLocalStorage.getStore() ?? defaultContext; + return asyncLocalStorage.getStore() as DecoContext ?? defaultContext; }, bind: ( ctx: DecoContext, diff --git a/engine/block.ts b/engine/block.ts index 922e170d8..7c6f02a48 100644 --- a/engine/block.ts +++ b/engine/block.ts @@ -117,7 +117,7 @@ export type InstanceOf< export type BlockTypes = keyof Omit< TManifest, - "config" | "baseUrl" + "config" | "baseUrl" | "name" >; export type BlockKeys = { diff --git a/engine/manifest/manifest.ts b/engine/manifest/manifest.ts index 235f85cd5..93c722c65 100644 --- a/engine/manifest/manifest.ts +++ b/engine/manifest/manifest.ts @@ -409,6 +409,7 @@ export const newContext = < instanceId: string | undefined = undefined, site: string | undefined = undefined, namespace: string = "site", + visibilityOverrides?: DecoContext["visibilityOverrides"], ): Promise> => { const currentContext = Context.active(); const ctx: DecoContext = { @@ -419,6 +420,7 @@ export const newContext = < id: instanceId ?? randId(), startedAt: new Date(), }, + visibilityOverrides, }; return fulfillContext(ctx, m, currentImportMap, release); diff --git a/runtime/fresh/plugin.tsx b/runtime/fresh/plugin.tsx index 646bf6269..bd3f80ec4 100644 --- a/runtime/fresh/plugin.tsx +++ b/runtime/fresh/plugin.tsx @@ -1,6 +1,6 @@ // TODO make fresh plugin use @deco/deco from JSR. so that we can use the same code for both -import type { AppManifest, SiteInfo } from "@deco/deco"; +import type { AppManifest, DecoContext, SiteInfo } from "@deco/deco"; import { Deco, type PageData, type PageParams } from "@deco/deco"; import { framework as htmxFramework } from "@deco/deco/htmx"; import type { ComponentType } from "preact"; @@ -51,6 +51,7 @@ export interface InitOptions { site?: SiteInfo; deco?: Deco; middlewares?: PluginMiddleware[]; + visibilityOverrides?: DecoContext["visibilityOverrides"]; } export type Options = @@ -89,6 +90,7 @@ export default function decoPlugin( site: opt?.site?.name, namespace: opt?.site?.namespace, bindings: { framework: opt?.htmx ? htmxFramework : framework }, + visibilityOverrides: opt.visibilityOverrides, }); const catchAll: PluginRoute = { diff --git a/runtime/mod.ts b/runtime/mod.ts index 94069f765..7e7e6dff8 100644 --- a/runtime/mod.ts +++ b/runtime/mod.ts @@ -57,6 +57,8 @@ export interface DecoOptions { manifest?: TAppManifest; decofile?: DecofileProvider; bindings?: Bindings; + + visibilityOverrides?: DecoContext["visibilityOverrides"]; } const NOOP_CALL = () => {}; @@ -86,6 +88,7 @@ export class Deco { crypto.randomUUID(), site, opts?.namespace, + opts?.visibilityOverrides, ) ); Context.setDefault(decoContext);