Skip to content

Commit

Permalink
Merge pull request #884 from deco-cx/feat/loader-acess-control
Browse files Browse the repository at this point in the history
[FEAT]: create server only access control for loaders and actions
  • Loading branch information
IncognitaDev authored Jan 16, 2025
2 parents a3758ba + 74908d8 commit 9628ebd
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 9 deletions.
13 changes: 10 additions & 3 deletions blocks/action.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
// deno-lint-ignore-file no-explicit-any
import { applyProps, type FnProps } from "../blocks/utils.tsx";
import {
applyProps,
type FnProps,
type 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<typeof actionBlock, "#/root/actions">;

export type ActionModule<
export interface ActionModule<
TProps = any,
TResp = any,
> = BlockModule<FnProps<TProps, TResp>>;
> extends BlockModule<FnProps<TProps, TResp>>, GateKeeperAccess {}

const actionBlock: Block<ActionModule> = {
type: "actions",
adapt: <
TProps = any,
>(
mod: ActionModule<TProps>,
key: string,
) => [
gateKeeper(mod.defaultVisibility, key),
applyProps(mod),
],
defaultPreview: (result) => {
Expand Down
11 changes: 8 additions & 3 deletions blocks/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
applyProps,
type FnContext,
type FnProps,
gateKeeper,
type GateKeeperAccess,
type RequestState,
type SingleFlightKeyFunc,
} from "./utils.tsx";
Expand All @@ -28,7 +30,7 @@ export type Loader = InstanceOf<typeof loaderBlock, "#/root/loaders">;
export interface LoaderModule<
TProps = any,
TState = any,
> extends BlockModule<FnProps<TProps>> {
> extends BlockModule<FnProps<TProps>>, GateKeeperAccess {
/**
* Specifies caching behavior for the loader and its dependencies.
*
Expand Down Expand Up @@ -332,10 +334,13 @@ const wrapLoader = (
const loaderBlock: Block<LoaderModule> = {
type: "loaders",
introspect: { includeReturn: true },
adapt: <TProps = any>(mod: LoaderModule<TProps>) => [
adapt: <TProps = any>(mod: LoaderModule<TProps>, key: string) => [
gateKeeper(mod.defaultVisibility, key),
wrapCaughtErrors,
(props: TProps, ctx: HttpContext<{ global: any } & RequestState>) =>
applyProps(wrapLoader(mod, ctx.resolveChain, ctx.context.state.release))(
applyProps(
wrapLoader(mod, ctx.resolveChain, ctx.context.state.release),
)(
props,
ctx,
),
Expand Down
26 changes: 26 additions & 0 deletions blocks/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Context } from "../mod.ts";

export interface GateKeeperAccess {
defaultVisibility?: "private" | "public";
}

export type SingleFlightKeyFunc<TConfig = any, TCtx = any> = (
args: TConfig,
Expand Down Expand Up @@ -138,6 +143,7 @@ export const fnContextFromHttpContext = <TState = {}>(
},
};
};

/**
* Applies the given props to the target block function.
*
Expand Down Expand Up @@ -248,3 +254,23 @@ export const buildImportMap = (manifest: AppManifest): ImportMap => {
);
return buildImportMapWith(manifest, builder);
};

export const gateKeeper =
(defaultVisibility: GateKeeperAccess["defaultVisibility"], key: string) =>
<
TContext extends ResolverMiddlewareContext<any> = 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!();
};
10 changes: 9 additions & 1 deletion deco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -55,6 +57,12 @@ export interface DecoContext<TAppManifest extends AppManifest = AppManifest> {
runtime?: Promise<DecoRuntimeState<TAppManifest>>;
instance: InstanceInfo;
request?: RequestContext;

visibilityOverrides?: Record<
BlockKeys<TAppManifest> extends undefined ? string
: BlockKeys<TAppManifest>,
GateKeeperAccess["defaultVisibility"]
>;
}

export interface RequestContextBinder {
Expand Down Expand Up @@ -107,7 +115,7 @@ export const Context = {
// Function to retrieve the active context
active: <T extends AppManifest = AppManifest>(): DecoContext<T> => {
// Retrieve the context associated with the async ID
return asyncLocalStorage.getStore() ?? defaultContext;
return asyncLocalStorage.getStore() as DecoContext<T> ?? defaultContext;
},
bind: <R, TArgs extends unknown[]>(
ctx: DecoContext,
Expand Down
2 changes: 1 addition & 1 deletion engine/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export type InstanceOf<
export type BlockTypes<TManifest extends AppManifest = AppManifest> =
keyof Omit<
TManifest,
"config" | "baseUrl"
"config" | "baseUrl" | "name"
>;

export type BlockKeys<TManifest extends AppManifest = AppManifest> = {
Expand Down
2 changes: 2 additions & 0 deletions engine/manifest/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ export const newContext = <
instanceId: string | undefined = undefined,
site: string | undefined = undefined,
namespace: string = "site",
visibilityOverrides?: DecoContext<T>["visibilityOverrides"],
): Promise<DecoContext<T>> => {
const currentContext = Context.active<T>();
const ctx: DecoContext<T> = {
Expand All @@ -419,6 +420,7 @@ export const newContext = <
id: instanceId ?? randId(),
startedAt: new Date(),
},
visibilityOverrides,
};

return fulfillContext(ctx, m, currentImportMap, release);
Expand Down
4 changes: 3 additions & 1 deletion runtime/fresh/plugin.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -51,6 +51,7 @@ export interface InitOptions<TManifest extends AppManifest = AppManifest> {
site?: SiteInfo;
deco?: Deco<TManifest>;
middlewares?: PluginMiddleware[];
visibilityOverrides?: DecoContext<TManifest>["visibilityOverrides"];
}

export type Options<TManifest extends AppManifest = AppManifest> =
Expand Down Expand Up @@ -89,6 +90,7 @@ export default function decoPlugin<TManifest extends AppManifest = AppManifest>(
site: opt?.site?.name,
namespace: opt?.site?.namespace,
bindings: { framework: opt?.htmx ? htmxFramework : framework },
visibilityOverrides: opt.visibilityOverrides,
});

const catchAll: PluginRoute = {
Expand Down
3 changes: 3 additions & 0 deletions runtime/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export interface DecoOptions<TAppManifest extends AppManifest = AppManifest> {
manifest?: TAppManifest;
decofile?: DecofileProvider;
bindings?: Bindings<TAppManifest>;

visibilityOverrides?: DecoContext<TAppManifest>["visibilityOverrides"];
}

const NOOP_CALL = () => {};
Expand Down Expand Up @@ -86,6 +88,7 @@ export class Deco<TAppManifest extends AppManifest = AppManifest> {
crypto.randomUUID(),
site,
opts?.namespace,
opts?.visibilityOverrides,
)
);
Context.setDefault(decoContext);
Expand Down

0 comments on commit 9628ebd

Please sign in to comment.