isitayush
isitayush2y ago

typesafe permissions

Hi, So I wanted to infer all the procedures from my router recursively & assign a permission (string[]) to each one them. I wrote the following,
type GetProceduresRecusivelyAndAssignPermissions<T extends AnyRouter> = {
[K in keyof T]: T[K] extends AnyProcedure
? { permissions: string[] }
: T[K] extends AnyRouter
? GetProceduresRecusivelyAndAssignPermissions<T[K]>
: never;
};

const permissions: GetProceduresRecusivelyAndAssignPermissions<
typeof appRouter
> = {
example: { getData: { permissions: ["canGetData"] } },
};
type GetProceduresRecusivelyAndAssignPermissions<T extends AnyRouter> = {
[K in keyof T]: T[K] extends AnyProcedure
? { permissions: string[] }
: T[K] extends AnyRouter
? GetProceduresRecusivelyAndAssignPermissions<T[K]>
: never;
};

const permissions: GetProceduresRecusivelyAndAssignPermissions<
typeof appRouter
> = {
example: { getData: { permissions: ["canGetData"] } },
};
It works however, I get a red squiggly (error) under the example in my permissions object with the following error message.
Type '{ getData: { permissions: string[]; }; }' is missing the following properties from type 'GetProceduresRecusivelyAndAssignPermissions<CreateRouterInner<RootConfig<{ ctx: ....
Type '{ getData: { permissions: string[]; }; }' is missing the following properties from type 'GetProceduresRecusivelyAndAssignPermissions<CreateRouterInner<RootConfig<{ ctx: ....
In my above type, I am trying to check if the key is a procedure, then simply returning the permissions. Otherwise checking if the key is a router then repeating it recursively. I'm unable to figure out what I'm doing wrong. Can someone help fix this. : )
14 Replies
Nick
Nick2y ago
Would it not be cleaner to use a Middleware and Procedure Meta to set this up? That’s the more first class way to manage access programmatically in tRPC The trouble with iterating over the routers is you may touch APIs which are considered internal and may change without notice, plus as you’ve found you run into things which aren’t really documented and you’ll be on your own figuring it out
isitayush
isitayush2y ago
Apologizes for this late response. You're right. It'd be nicer if I could achieve this with Meta. However, I am not really sure how that would work. I tried creating a solution using Meta but couldn't really come up with something that would fit my case. Diving in some internal code for trpc (the GetInferenceHelpers to be more precise). I did find my self a working solution.
type MapRouterToPermissions<T extends AnyRouter> = {
[K in keyof T["_def"]["record"]]: T["_def"]["record"][K] extends infer P
? P extends AnyRouter
? MapRouterToPermissions<P>
: P extends AnyProcedure
? string[]
: never
: never;
};
type MapRouterToPermissions<T extends AnyRouter> = {
[K in keyof T["_def"]["record"]]: T["_def"]["record"][K] extends infer P
? P extends AnyRouter
? MapRouterToPermissions<P>
: P extends AnyProcedure
? string[]
: never
: never;
};
I'm not sure though what ["_def"]["record"] really hold here. If I'm not wrong Nick, The meta can be accessed in the middleware before a procedure is fired right?
Nick
Nick2y ago
Yes ^ You just have to codify your permissions in a declarative way. My app has entitlements, and roles which are associated with multiple entitlements. Every route gets an entitlement. Every user gets a role. Middleware checks a route’s entitlement and if the user has it via their role. Quite a simple model
isitayush
isitayush2y ago
Understood! Thank You. I'll try this & if it works I'll update it here & let you know.
Alex / KATT 🐱
i do a factory function instead for my proc creation
export const scopedProcedure = (scope: PermissionScope) =>
authedProcedure.meta({
scope,
});
export const scopedProcedure = (scope: PermissionScope) =>
authedProcedure.meta({
scope,
});
then a middleware in my authedProcedure do check that the user has the defined scope example
const postRouter = router({
edit: scopedProcedure('post.edit').mutation(() => '...')
})
const postRouter = router({
edit: scopedProcedure('post.edit').mutation(() => '...')
})
isitayush
isitayush2y ago
thanks alex! both this & nick's solution works great.
Unknown User
Unknown User17mo ago
Message Not Public
Sign In & Join Server To View
Alex / KATT 🐱
Alex / KATT 🐱17mo ago
we haven't OSS'ed our thing at work [yet] but we do a sort of mapper between our user roles and prisma "where"-queries that are passed through AsyncLocalStorage
Unknown User
Unknown User17mo ago
Message Not Public
Sign In & Join Server To View
Alex / KATT 🐱
Alex / KATT 🐱17mo ago
Base procedure with an input e.g. organizationId Do auth check where you fetch the user, check if the user is a member of the supplied org id, and get their role
Alex / KATT 🐱
Alex / KATT 🐱17mo ago
Define Procedures | tRPC
Procedures in tRPC are very flexible primitives to create backend functions; they use a builder pattern which means you can create reusable base procedures for different parts of your backend application.
Unknown User
Unknown User17mo ago
Message Not Public
Sign In & Join Server To View
Alex / KATT 🐱
Alex / KATT 🐱17mo ago
If you want that granularity, maybe you wna use graphql or some other abstraction for your services like nestjs trpc doesn't "care" about your business logic and we haven't made any abstractions to make this sort of granularity easy
Unknown User
Unknown User17mo ago
Message Not Public
Sign In & Join Server To View