Jules
Jules•3mo ago

How to add RLS to queries/mutations within an action?

Right now, we make use of customQuery/wrapDatabaseReader, customMutation/wrapDatabaseWriter, and Rules from convex-helpers to add row-level security to our database. We have defined authenticatedQuery and authenticatedMutation wrapper functions using this, and enforce the usage of them through ESLint rules. Ideally, we would also like to apply the same Rules object to any ctx.runQuery/ctx.runMutation call within the handler of our custom authenticatedAction action. Is there any way to wrap the runQuery and runMutation methods in ActionCtx with a Rules object so we can patch them in our custom action?
5 Replies
Convex Bot
Convex Bot•3mo ago
Thanks for posting in <#1088161997662724167>. Reminder: If you have a Convex Pro account, use the Convex Dashboard to file support tickets. - Provide context: What are you trying to achieve, what is the end-user interaction, what are you seeing? (full error message, command output, etc.) - Use search.convex.dev to search Docs, Stack, and Discord all at once. - Additionally, you can post your questions in the Convex Community's <#1228095053885476985> channel to receive a response from AI. - Avoid tagging staff unless specifically instructed. Thank you!
ampp
ampp•3mo ago
Sometimes i cant keep track of all the magic that happens within the wrappers and such. I think you just automatically accomplish this by always accessing customized internal functions. Using ctx.runMutation etc does not bypass your authenticatedMutation. Because i use ents i can RLS at a whole different level, so i cant speak to that here. Anyway i had to deconstruct the session context code from convex-helpers for some modifications i cant even remember why in this moment lol. But it should be what you want. There's threads somewhere around with me asking for help. One of many crazy endeavors 😅 This is how you would do custom functions inside the ctx of a action that could have more logic. Maybe its helpful? This is more complex because of the args passing so it could be simpler.
// in functions.ts
export const sessionAction = customAction(rawAction, {
args: { sessionId: v.optional(v.union(v.string(), v.null())) },
input: async (ctx, args) => {
return {
ctx: {
...ctx,
...runSessionFunctions(ctx, args.sessionId),
},
args,
}
},
})
// in functions.ts
export const sessionAction = customAction(rawAction, {
args: { sessionId: v.optional(v.union(v.string(), v.null())) },
input: async (ctx, args) => {
return {
ctx: {
...ctx,
...runSessionFunctions(ctx, args.sessionId),
},
args,
}
},
})
export type SessionId = string | null | undefined
// Validator for session IDs.
export const vSessionId = v.optional(
v.union(v.string(), v.null())
) as unknown as
| Validator<(SessionId & { __SessionId: true }) | null>
| undefined
export const SessionIdArg = { sessionId: vSessionId }

type SessionFunction<
T extends 'query' | 'mutation' | 'action',
Args = any,
> = FunctionReference<
T,
'public' | 'internal',
{ sessionId: SessionId } & Args,
any
>

type SessionArgsArray<
Fn extends SessionFunction<'query' | 'mutation' | 'action', any>,
> = keyof FunctionArgs<Fn> extends 'sessionId'
? [args?: EmptyObject]
: [args: BetterOmit<FunctionArgs<Fn>, 'sessionId'>]

export interface RunSessionFunctions {
runSessionQuery<Query extends SessionFunction<'query'>>(
query: Query,
...args: SessionArgsArray<Query>
): Promise<FunctionReturnType<Query>>

/**
* Run the Convex mutation with the given name and arguments.
*
* Consider using an {@link internalMutation} to prevent users from calling
* the mutation directly.
*
* @param mutation - A {@link FunctionReference} for the mutation to run.
* @param args - The arguments to the mutation function.
* @returns A promise of the mutation's result.
*/
runSessionMutation<Mutation extends SessionFunction<'mutation'>>(
mutation: Mutation,
...args: SessionArgsArray<Mutation>
): Promise<FunctionReturnType<Mutation>>

runSessionAction<Action extends SessionFunction<'action'>>(
action: Action,
...args: SessionArgsArray<Action>
): Promise<FunctionReturnType<Action>>
}
export type SessionId = string | null | undefined
// Validator for session IDs.
export const vSessionId = v.optional(
v.union(v.string(), v.null())
) as unknown as
| Validator<(SessionId & { __SessionId: true }) | null>
| undefined
export const SessionIdArg = { sessionId: vSessionId }

type SessionFunction<
T extends 'query' | 'mutation' | 'action',
Args = any,
> = FunctionReference<
T,
'public' | 'internal',
{ sessionId: SessionId } & Args,
any
>

type SessionArgsArray<
Fn extends SessionFunction<'query' | 'mutation' | 'action', any>,
> = keyof FunctionArgs<Fn> extends 'sessionId'
? [args?: EmptyObject]
: [args: BetterOmit<FunctionArgs<Fn>, 'sessionId'>]

export interface RunSessionFunctions {
runSessionQuery<Query extends SessionFunction<'query'>>(
query: Query,
...args: SessionArgsArray<Query>
): Promise<FunctionReturnType<Query>>

/**
* Run the Convex mutation with the given name and arguments.
*
* Consider using an {@link internalMutation} to prevent users from calling
* the mutation directly.
*
* @param mutation - A {@link FunctionReference} for the mutation to run.
* @param args - The arguments to the mutation function.
* @returns A promise of the mutation's result.
*/
runSessionMutation<Mutation extends SessionFunction<'mutation'>>(
mutation: Mutation,
...args: SessionArgsArray<Mutation>
): Promise<FunctionReturnType<Mutation>>

runSessionAction<Action extends SessionFunction<'action'>>(
action: Action,
...args: SessionArgsArray<Action>
): Promise<FunctionReturnType<Action>>
}
export function runSessionFunctions<DataModel extends GenericDataModel>(
ctx: GenericActionCtx<DataModel>,
sessionId: SessionId
): RunSessionFunctions {
return {
runSessionQuery(fn, ...args) {
const argsWithSession = { ...(args[0] ?? {}), sessionId } as FunctionArgs<
typeof fn
>
return ctx.runQuery(fn, argsWithSession)
},
runSessionMutation(fn, ...args) {
const argsWithSession = { ...(args[0] ?? {}), sessionId } as FunctionArgs<
typeof fn
>
return ctx.runMutation(fn, argsWithSession)
},
runSessionAction(fn, ...args) {
const argsWithSession = { ...(args[0] ?? {}), sessionId } as FunctionArgs<
typeof fn
>
return ctx.runAction(fn, argsWithSession)
},
}
}
export function runSessionFunctions<DataModel extends GenericDataModel>(
ctx: GenericActionCtx<DataModel>,
sessionId: SessionId
): RunSessionFunctions {
return {
runSessionQuery(fn, ...args) {
const argsWithSession = { ...(args[0] ?? {}), sessionId } as FunctionArgs<
typeof fn
>
return ctx.runQuery(fn, argsWithSession)
},
runSessionMutation(fn, ...args) {
const argsWithSession = { ...(args[0] ?? {}), sessionId } as FunctionArgs<
typeof fn
>
return ctx.runMutation(fn, argsWithSession)
},
runSessionAction(fn, ...args) {
const argsWithSession = { ...(args[0] ?? {}), sessionId } as FunctionArgs<
typeof fn
>
return ctx.runAction(fn, argsWithSession)
},
}
}
this ultimately gives me the ability to call ctx.runSessionMutation() within a sessionAction()
Jules
JulesOP•2mo ago
Thank you so much for the elaborate answer @ampp!
I think you just automatically accomplish this by always accessing customized internal functions. Using ctx.runMutation etc does not bypass your authenticatedMutation.
That's a great point, I totally overlooked that! Thanks for clarifying that. The example of how you handle passing a session ID to any convex function called within your sessionAction is insightful as well. Right now, we're using Convex Auth for our authentication, which e.g. handles refreshing session tokens. Could you elaborate on the reason for you to keep track of a sessionId?
ampp
ampp•2mo ago
Its all so we can target the individual browser window session with updates at the most granular level and know how many windows they have open within one browser. We can target [member across all browser logins] [all open windows in browser] [specific browser window] The only downside is we lose a lot of convex cross query caching as each query could have "unique" args, so we do allow it to be optional. The goal is to give all the functionality in why someone would use multiple browsers to meta game us to be allowed.
Jules
JulesOP•2mo ago
Right, cool!

Did you find this page helpful?