T
tRPC

Validating Permissions

Validating Permissions

RRhys12/23/2022
Hi! A common operation that I'm doing in tRPC is validating that a person has permissions to perform the action they're trying to do, i.e:
const serverCreateUpdateRouter = router({
create: protectedProcedureWithUserServers
.input(server_create_input)
.mutation(({ ctx, input }) => {
assertCanEditServer(ctx, input.id);
return ctx.prisma.server.create({ data: input });
}),
update: protectedProcedureWithUserServers
.input(server_update_input)
.mutation(({ ctx, input }) => {
assertCanEditServer(ctx, input.id);
return ctx.prisma.server.update({ where: { id: input.id }, data: input });
}),
});
const serverCreateUpdateRouter = router({
create: protectedProcedureWithUserServers
.input(server_create_input)
.mutation(({ ctx, input }) => {
assertCanEditServer(ctx, input.id);
return ctx.prisma.server.create({ data: input });
}),
update: protectedProcedureWithUserServers
.input(server_update_input)
.mutation(({ ctx, input }) => {
assertCanEditServer(ctx, input.id);
return ctx.prisma.server.update({ where: { id: input.id }, data: input });
}),
});
Where assertCanEditServer is the permissions check. In this instance, I'm taking the ID of the server they're editing and comparing it against a list of server permissions to validate they can edit that server. I'd like to find a better way of doing this instead of just having to repeat a bunch of code with this assertCanEditServer function Having to put that inside of each router is a bit ugly, but the information to ensure the caller can edit isn't available inside of Context so that's the only place that I can think to put it. Is this the best approach to this where I just make an assert function at throw that inside of the procedure or is there some better way of doing this? Thanks
AKAlex / KATT 🐱12/23/2022
I've actually done a bit of work on this https://twitter.com/alexdotjs/status/1588205093576675328?t=dd8KODcSmAxA2tElBpfAwg&s=19 But haven't OSS'd it yet But some stuff to give you places to look or get ideas - use multiple input parsers (e.g. a base procedure that does auth checks and see that the user is in a specific orgsnizarik if it's a multitenant SaaS thing) - use metadata to decorate permissions - have auth resolvers based on the entity Also, in your prisma queries, you can limit your where statement instead of doing an auth check
RRhys12/24/2022
Thanks for the reply! I’ll experiment with those methods and see what works best Limiting it in the Prisma query is an interesting approach although what I’m using to check is fetching from the Discord API and storing that in memory so I can’t use that approach
AKAlex / KATT 🐱12/24/2022
i guess most procs will be around a "server" then so you can do some base proc like this
const serverProc = t.procedure
.input(z.object({ serverId: z.string() }))
.use(async opts => {
const permissions = null; /* .... get permissions for opts.ctx.id in ops.input.serverId */

return opts.next({ ctx: { permsissions } });
})
const serverProc = t.procedure
.input(z.object({ serverId: z.string() }))
.use(async opts => {
const permissions = null; /* .... get permissions for opts.ctx.id in ops.input.serverId */

return opts.next({ ctx: { permsissions } });
})
or maybe something like
// base proc
const channelProc = t.procedure
.input(z.object({ channelId: z.string() }))
.use(async opts => {
if (!opts.meta.permissions?.length) throw new Error('server error - gotta setup permissions for ' + opts.path)
if (opts.meta.permissions.some(perm => !opts.ctx.permissions.includes(perm)) throw new TRPCError({ code: "BAD_REQUEST" })

return opts.next({ ctx: { permsissions } });
})

// actual proc in the router
const sendMessage = channelProc
.input( z.object({ text: z.string() } )
.meta({ permissions: ['send-message'] })
.mutation(() => {
// .....
})
// base proc
const channelProc = t.procedure
.input(z.object({ channelId: z.string() }))
.use(async opts => {
if (!opts.meta.permissions?.length) throw new Error('server error - gotta setup permissions for ' + opts.path)
if (opts.meta.permissions.some(perm => !opts.ctx.permissions.includes(perm)) throw new TRPCError({ code: "BAD_REQUEST" })

return opts.next({ ctx: { permsissions } });
})

// actual proc in the router
const sendMessage = channelProc
.input( z.object({ text: z.string() } )
.meta({ permissions: ['send-message'] })
.mutation(() => {
// .....
})
RRhys1/18/2023
I ended up implementing this in a functional way
export async function protectedFetch<T>(input: ProtectedFetchInput<T>) {
const { fetch, ctx, not_found_message } = input;
const data = await findOrThrowNotFound(fetch, not_found_message);
const server_id = input.getServerId(data);
assertCanEditServers(ctx, server_id);
return data;
}

// 2. Scenario 2, we already have the data and can assertCanEditServer
export async function protectedOperation<T>(input: {
operation: () => Promise<T>;
// eslint-disable-next-line no-unused-vars
server_id: string | string[];
ctx: Context;
}) {
const { operation, server_id, ctx } = input;
assertCanEditServers(ctx, server_id);
return operation();
}
export async function protectedFetch<T>(input: ProtectedFetchInput<T>) {
const { fetch, ctx, not_found_message } = input;
const data = await findOrThrowNotFound(fetch, not_found_message);
const server_id = input.getServerId(data);
assertCanEditServers(ctx, server_id);
return data;
}

// 2. Scenario 2, we already have the data and can assertCanEditServer
export async function protectedOperation<T>(input: {
operation: () => Promise<T>;
// eslint-disable-next-line no-unused-vars
server_id: string | string[];
ctx: Context;
}) {
const { operation, server_id, ctx } = input;
assertCanEditServers(ctx, server_id);
return operation();
}
create: protectedProcedureWithUserServers.input(z_server_create).mutation(({ ctx, input }) => {
return protectedOperation({
ctx,
server_id: input.id,
operation: () => ctx.prisma.server.create({ data: input }),
});
})
create: protectedProcedureWithUserServers.input(z_server_create).mutation(({ ctx, input }) => {
return protectedOperation({
ctx,
server_id: input.id,
operation: () => ctx.prisma.server.create({ data: input }),
});
})
Still figuring out if this is a way I like or not but so far it seems good - I can take it a step further and make the auth check a functional element as well to have different permission checks Sample file from the branch I'm working on where this is heavily used https://github.com/AnswerOverflow/AnswerOverflow/blob/API/packages/api/src/router/channel/channel_settings.ts Completely necroposting on this to share how I'm handling permissions now to help others out in the future Here's two variants
create: publicProcedure.input(z_channel_create).mutation(({ ctx, input }) => {
return protectedMutation({
operation: () => ctx.prisma.channel.create({ data: input }),
permissions: [() => canEditServer(ctx, input.server_id), () => isCtxCallerDiscordBot(ctx)],
});
}),

byId: publicProcedure.input(z.string()).query(async ({ ctx, input }) => {
return protectedFetchWithPublicData({
fetch: () => ctx.prisma.channel.findUnique({ where: { id: input } }),
permissions: (data) => canEditServer(ctx, data.server_id),
not_found_message: "Channel does not exist",
public_data_formatter: (data) => z_channel_public.parse(data),
});
}),
create: publicProcedure.input(z_channel_create).mutation(({ ctx, input }) => {
return protectedMutation({
operation: () => ctx.prisma.channel.create({ data: input }),
permissions: [() => canEditServer(ctx, input.server_id), () => isCtxCallerDiscordBot(ctx)],
});
}),

byId: publicProcedure.input(z.string()).query(async ({ ctx, input }) => {
return protectedFetchWithPublicData({
fetch: () => ctx.prisma.channel.findUnique({ where: { id: input } }),
permissions: (data) => canEditServer(ctx, data.server_id),
not_found_message: "Channel does not exist",
public_data_formatter: (data) => z_channel_public.parse(data),
});
}),
It makes adding / removing permissions from procedures super easy - along with that I'm now just using middlewares for data prep and putting the relevant permissions inside of the procedure itself so that way it's readable as to what permissions are required for the route Once I get this merged into my main I'll put some links here of code snippets, really happy with this approach for it - still could use some expanding on but for now sticking with this
Nnlucas1/19/2023
That work can definitely be pushed up to a middleware and Meta on the procedure used to define what protection level/roles each procedure has It can be as simple as
protectedProcedure = publicProcesure.use(AuthMiddleware)

router({
create: protectedProcedure
.input().output()
.meta({ canEdit: true })
.mutation(/* now only the operation needs to be here and nothing needed importing but the middleware */)
})
protectedProcedure = publicProcesure.use(AuthMiddleware)

router({
create: protectedProcedure
.input().output()
.meta({ canEdit: true })
.mutation(/* now only the operation needs to be here and nothing needed importing but the middleware */)
})
Ah which is actually what KATT already suggested now I see it. Definitely do what works for you at the end of the day 🙂
RRhys1/20/2023
I might be missing something with this but I think with this approach I'm not able to pass in values from the input into it To give a few examples For validating permissions when editing a server, I have to check if the user can edit the server id that was passed in from the input For validating permissions when editing channel settings, i first have to fetch the channel from the database and then check against the server id on that channel if they can edit From my understanding I can't access input values from anywhere except for the mutation/query which results in putting the validation inside of this If I could do something like this with dynamically setting meta properties
.meta({ server_id: input.server_id })
.meta({ server_id: input.server_id })
or
.meta({ server_id: ctx.prisma.channels(input.channel_id).server_id) canEdit: true})
.meta({ server_id: ctx.prisma.channels(input.channel_id).server_id) canEdit: true})
then I'd be able to move this into the meta which would be nice (cc @Nick Lucas forgot to do that as a reply sorry, no rush on responding)
Nnlucas1/20/2023
Makes sense, middlewares do have access to the parsed/validated input I believe, but then your challenge becomes having a middleware which can understand the input type, by convention or configuration. You might end up with something like this to invert control up to the middleware
protectedProcedure = publicProcesure.use(AuthMiddleware)

interface Meta {
accessValidator: (stuff) => boolean
}

router({
create: protectedProcedure
.input().output()
.meta({ accessValidator: serverIdAccessValidator })
.mutation(/* now only the operation needs to be here and nothing needed importing but the middleware */)
})
protectedProcedure = publicProcesure.use(AuthMiddleware)

interface Meta {
accessValidator: (stuff) => boolean
}

router({
create: protectedProcedure
.input().output()
.meta({ accessValidator: serverIdAccessValidator })
.mutation(/* now only the operation needs to be here and nothing needed importing but the middleware */)
})
Since I haven't built this for my own needs obviously I revert to your expertise, and inverting control to middlewares is definitely my opinion

Looking for more? Join the community!

T
tRPC

Validating Permissions

Join Server
Recommended Posts
Any typescript chads able know if it's possible to map a type to another type w genericsNot 100% sure if this is appropriate to ask here but I figured there's a lot of good TS developers ouseQuery enabled not working???Even setting the `enabled` to false --> `trpc.order.get({id: "123"}, {enabled: false})` still make`QueryClientProvider` not included in `withTRPC`?Trying to use `import { useIsMutating } from "react-query"` but it's throwing `Error: No QueryClientHandle React error boundarySeems like Im doing something wrong as I can't handle the error boundary from trpc query. I've queryAny tips for Typescript slowness?We successfully migrated from 9.x to 10 but are still seeing slow VS Code perf (10+ seconds to get aChange status of useMutation from 'success' back to 'idle'Hiya, I have a mutation for creating stuff that can run a few different ways on a page, and I want tIs it possible to call one procedure in another?I have multiple routers in my trpc server. For example userRouter (e.g. procedure: getUser) and postHandling Query Errors at Root of App (v9)I want to show an error toast on my `NextJS` frontend every time there is an error from `useQuery` itRPC Client webpack errorAs soon as I add the client part to my legacy app i get an error and Can't figure out what is wrong.cookies, headers and authenticationin express I can do something like `res.cookie("name", "value")` for example. alternatively I can dtrpc hook pattern. This works, but I’m not convinced…I want to call my route when a button is clicked and have access to isLoading, onError etc… I have iCatch TRPCError, ZoddError on the front-endi am throwing a TRPCError in a mutation. i dont understand how to catch this error in the OnError meimplicitly has type 'any' because it does not have a type annotation and is referenced directlyRunning into this error. I feel like it's some sort of infinite recursion issue, but not sure where Why do some examples create PrismaClient in context, and others don’tIs there a specific reason to do this or not to do this? I can imagine with the new Prisma extensionAppRouter type any?I am trying to use trpc in a Turbo repo and when I export the AppRouter on the server side it is thez.map() as an inputHi, I have ```ts z.map(z.string(), z.string()) ``` as an input, but when I try to pass the map as correct way to call TRPC inside a functioni want a user to be able to sign after they have a clicked a button but i get a hooks error. can anyuse tRPC for RPC calls instead of gRPCHi, I know that tRPC and gRPC are different things - despite the similar name. Nevertheless I woutRPC caching vs Vercel dynamic edge cachingWhat is the difference, or improvements, on Vercel's dynamic edge caching? tRPC server caching worksIgnore Errors from batch callsI have some of my routers throw helpful errors for the client to use in the error message object, bu