72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
// The tenant wall of the effect actions (lib/effects.ts). The kernel in an action resolves the
|
|
// tenant scope and checks the permission here, inside a query, with the same tenancy hooks over
|
|
// ctx.db that every other procedure uses. Both are internal: only this deployment's own actions
|
|
// call them, and they derive the actor and the organization pin from the caller's verified
|
|
// identity, never from their arguments.
|
|
|
|
import { type Actor, type AnyScope, notFound, toProblem, unauthenticated } from "@conusai-kit/core"
|
|
import { ConvexError, type Value, v } from "convex/values"
|
|
import { internalQuery } from "./_generated/server"
|
|
import { callContextOf, tenancyOver } from "./lib/runtime"
|
|
|
|
const level = v.union(v.literal("org"), v.literal("workspace"), v.literal("project"))
|
|
|
|
async function verifiedActor(
|
|
auth: Parameters<typeof callContextOf>[0],
|
|
): Promise<{ actor: Actor; pinnedOrgId?: string }> {
|
|
const { actor, pinnedOrgId } = await callContextOf(auth)
|
|
if (!actor) throw unauthenticated()
|
|
return { actor, ...(pinnedOrgId === undefined ? {} : { pinnedOrgId }) }
|
|
}
|
|
|
|
const problem = (error: unknown): never => {
|
|
throw new ConvexError(toProblem(error) as unknown as Value)
|
|
}
|
|
|
|
/** The scope ids of a tenant-level call, or the kernel's problem (a stranger's notFound). */
|
|
export const resolve = internalQuery({
|
|
args: { level, input: v.any() },
|
|
handler: async (ctx, args) => {
|
|
try {
|
|
const { actor, pinnedOrgId } = await verifiedActor(ctx.auth)
|
|
const scope = (await tenancyOver(ctx.db).resolveScope(args.level, args.input, actor, pinnedOrgId)) as AnyScope & {
|
|
orgId: string
|
|
workspaceId?: string
|
|
projectId?: string
|
|
}
|
|
if (pinnedOrgId !== undefined && scope.orgId !== pinnedOrgId) throw notFound("no scope in this organization")
|
|
return {
|
|
orgId: scope.orgId,
|
|
...(scope.workspaceId === undefined ? {} : { workspaceId: scope.workspaceId }),
|
|
...(scope.projectId === undefined ? {} : { projectId: scope.projectId }),
|
|
}
|
|
} catch (error) {
|
|
return problem(error)
|
|
}
|
|
},
|
|
})
|
|
|
|
/** Whether the verified actor holds `permission` in the scope, for this procedure. */
|
|
export const authorize = internalQuery({
|
|
args: {
|
|
level,
|
|
scope: v.object({ orgId: v.string(), workspaceId: v.optional(v.string()), projectId: v.optional(v.string()) }),
|
|
permission: v.string(),
|
|
procedure: v.object({
|
|
name: v.string(),
|
|
kind: v.union(v.literal("read"), v.literal("write"), v.literal("effect")),
|
|
}),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
try {
|
|
const { actor, pinnedOrgId } = await verifiedActor(ctx.auth)
|
|
if (pinnedOrgId !== undefined && args.scope.orgId !== pinnedOrgId) return false
|
|
const scope = { level: args.level, actor, ...args.scope } as AnyScope
|
|
return await tenancyOver(ctx.db).authorize(scope, args.permission, args.procedure)
|
|
} catch (error) {
|
|
return problem(error)
|
|
}
|
|
},
|
|
})
|