Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thoughts on protecting some methods behind authentication and permissions #1

Closed
TheBestMoshe opened this issue May 11, 2022 · 12 comments
Assignees
Labels
enhancement New feature or request

Comments

@TheBestMoshe
Copy link

Problem

Currently, this generator can't be used whenever there needs to be permission per procedure. For example, only some users are supposed to have access to a procedure. Or a procedure requires the user to contain a specific scope in their JWT.

Suggested solution

Add a way to pass information into the generator (maybe directives in the Prisma file?)

Alternatives

Manually write all tRPC procedures by hand.

@omar-dulaimi
Copy link
Owner

Hey @TheBestMoshe

I like your suggestion very much.

How do you feel about an option that let's you pass a custom middleware. This middleware will be applied to all routers:

export const postsRouter = createRouter()
  .middleware(async ({ ctx, next }) => {
    if (!ctx.user?.isAdmin) {
      throw new TRPCError({ code: "UNAUTHORIZED" });
    }
    return next();
  })
  .mutation("findUniquePost", {
    input: PostFindUniqueSchema,
    async resolve({ ctx, input }) {
      const findUniquePost = await ctx.prisma.post.findUnique(input);
      return findUniquePost;
    },
  })

So that way, you can have as many custom cases you need, all in one place.

Would that solve your problem?

Please let me know if you have any other implementation ideas in mind.

@omar-dulaimi
Copy link
Owner

How about a middleware per router, not a global one?

@omar-dulaimi omar-dulaimi added the enhancement New feature or request label May 11, 2022
@omar-dulaimi omar-dulaimi self-assigned this May 11, 2022
@omar-dulaimi
Copy link
Owner

The directive way inside the Prisma schema could have worked if we're spelling out all possible procedures in the Prisma schema itself, but unfortunately, we only spell models and enums. It was only possible in GraphQL schemas.

@omar-dulaimi
Copy link
Owner

I guess the most sane thing to do here is a middleware that handles auth inside of it based on proc name. Very soon, I'm looking to build a separate middleware library that makes proc auth a breeze.

@omar-dulaimi omar-dulaimi pinned this issue May 12, 2022
@TheBestMoshe
Copy link
Author

@omar-dulaimi I agree that a middleware might be the best approach. Especially if the Prisma tRPC Generator can generate helper functions that can include the typescript definitions of the possible inputs and outputs.

I think a middleware approach would be more flexible than declaring it in the Prisma file. This would enable arbitrary rules that do not need to be built into the library.

I guess the most sane thing to do here is a middleware that handles auth inside of it based on proc name.

This sounds like a great approach!

@omar-dulaimi
Copy link
Owner

@TheBestMoshe Thank you for the feedback.

I have implemented the middleware solution and it's out now in prisma-trpc-generator@0.0.0-rc.8.
Glad to have it implemented. Please keep your ideas coming!

I appreciate it.

@omar-dulaimi omar-dulaimi unpinned this issue May 21, 2022
@omar-dulaimi
Copy link
Owner

omar-dulaimi commented May 21, 2022

Hello @TheBestMoshe

Announcing tRPC Shield
It's based on GraphQL Shield if you have used it before, it allows you to abstract away your permissions into a single file!

take this example:

const permissions = shield({
  query: {
    fruits: and(isAuthenticated, or(isAdmin, isEditor)),
    customers: and(isAuthenticated, isAdmin),
  },
  mutation: {
    addFruitToBasket: isAuthenticated,
  },
})

You can define rules as you like, then mix and match then to get exactly what you need with logical operators.

To make things even smoother for tRPC users who also use Prisma, I built a new generator that takes care of emitting the right shield for your Prisma schema:

import { shield, allow } from 'trpc-shield';

export const permissions = shield({
  query: {
    aggregatePost: allow,
    aggregateUser: allow,
    findFirstPost: allow,
    findFirstUser: allow,
    findManyPost: allow,
    findManyUser: allow,
    findUniquePost: allow,
    findUniqueUser: allow,
    groupByPost: allow,
    groupByUser: allow,
  },
  mutation: {
    createOnePost: allow,
    createOneUser: allow,
    deleteManyPost: allow,
    deleteManyUser: allow,
    deleteOnePost: allow,
    deleteOneUser: allow,
    updateManyPost: allow,
    updateManyUser: allow,
    updateOnePost: allow,
    updateOneUser: allow,
    upsertOnePost: allow,
    upsertOneUser: allow,
  },
});

You can fined them here:
tRPC Shield: https://github.com/omar-dulaimi/trpc-shield
Prisma tRPC Shield Generator: https://github.com/omar-dulaimi/prisma-trpc-shield-generator

@MartinMuzatko
Copy link

@omar-dulaimi can you please document this feature? I'd like to know how I can pass a custom middleware or configure the permissions. thanks :)

@omar-dulaimi
Copy link
Owner

Hey @MartinMuzatko

To pass a custom middleware, you only need to pass the withMiddleware flag in your Prisma schema like so:

generator trpc {
  provider       = "prisma-trpc-generator"
  withMiddleware = true
}

Same thing if you want to get a shield generated:

generator trpc {
  provider       = "prisma-trpc-generator"
  withShield     = true
}

You could also generate both of them if you add both flags above.

To learn more about tRPC Shield and how to configure it's permissions, you should go here: https://github.com/omar-dulaimi/trpc-shield

It explains all possible options in that project README :)

@MartinMuzatko
Copy link

I am not sure I understand correctly. The point of Shield and custom middleware is that I can customize them. If they are generated, how can I update them, without having the generator overwriting them again?
Where is the entrypoint for my custom middleware? Do I need to write to a file and give the path to the generator config in Prisma?

Surely, if I just test this out, I'll see how things work out. But that is something I would like to know before I even install the module to my pc, when looking for new things that fit into my stack :)

I'll report back once I know more.

@omar-dulaimi
Copy link
Owner

@MartinMuzatko

The purpose of all Prisma generators is to always be in-sync with the Prisma schema and client. So, yeah it will overwrite every time a generate happens. Best thing you can do is imagine them as snippets generators. You could use them as is or modify them to your liking, in another directory where it will be source control tracked.

The middleware lies inside the helpers directory, in the createProtectedRoute to be exact.

@scottyeck
Copy link

I am not sure I understand correctly. The point of Shield and custom middleware is that I can customize them. If they are generated, how can I update them, without having the generator overwriting them again?
Where is the entrypoint for my custom middleware? Do I need to write to a file and give the path to the generator config in Prisma?

FWIW I'm having this same issue. Feels like this could be resolved more generically by allowing the caller to configure a their own custom createRouter function - that way they could implement middleware, or a shield, or... really whatever they want. The limitation of having to add a post-generate step to copy existing code into helpers/createRouter.ts feels like unnecessary burden on the caller.

I need this functionality and can configure it in a fork I'm maintaining. Happy to PR the change into the upstream if there's appetite for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants