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

Supporting session-dependent queries like Postgres' SET across queries of a request #5128

Open
matthewmueller opened this issue Jan 15, 2021 · 77 comments
Assignees
Labels
domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. kind/feature A request for a new feature. topic: connection pool topic: database-provider/supabase topic: enterprise topic: rls

Comments

@matthewmueller
Copy link
Contributor

matthewmueller commented Jan 15, 2021

Problem

In Postgres you can set a user for a connection on the database:

await prisma.$executeRaw(`SET current_user_id = ${currentUser.id}`)

This SET can then be used in combination with row-level security (RLS) to issue queries like this:

select * from messages

that only give you back messages from that user. This technique is used by Postgraphile and Postgrest and really takes advantage of what Postgres offers.

Without access to the connection pool, there's no way to guarantee you'll get the same connection each query. Since SET values are bound to the query, subsequent queries may be missing the SET or may even override another request's SET.

I'm not sure what the best approach is. Tying a connection to the lifecycle of a request has its own performance implications.

A point of reference potentially worth investigating. Go's standard SQL library uses a connection pool under the hood that's transparent to developers. Do they run into this problem too? If so, do they or how do they deal with it?

Originally from: #4303 (comment)

@matthewmueller matthewmueller added kind/feature A request for a new feature. topic: enterprise domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. labels Jan 15, 2021
@matthewmueller matthewmueller changed the title Supporting Postgres' SET Supporting Postgres' SET across queries of a request Jan 15, 2021
@webbkyr
Copy link

webbkyr commented Jan 15, 2021

Thanks for opening a separate issue. Would love to see this supported.

@crubier
Copy link

crubier commented Jan 19, 2021

Maybe an approach with middlewares allowing to tweak the SQL request could be useful?

@KrisCoulson
Copy link

I want to add a plus on to this. I have been using supabase lately and it would be really great to take advantage of RLS. I haven't really found a great way Of handling things like multi-tenancy using prisma/supabase and think that this could really unlock a new realm of possibilities

@yuval-hazaz
Copy link

Although I understand the current challenges to link a connection to the request, I would love to see some way to support RLS.

I am not familiar with the internals of Prisma engine, but I am throwing an idea and would love to hear your thoughts.
Instead of looking for a solution to link a connection to a request, or looking for something that is based on a long lived transaction - maybe you can think of a solution specifically for RLS.
Maybe the transaction API can get another parameter with a command to be executed with every action in the transaction - it can be generic or very specific for the "SET" command.

generic example - get a command to be executed before everyother command in the transaction on the same connection

const [userList, updateUser] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
  prisma.post.count(),
],
  prisma.$executeRaw(`SET current_user_id = ${currentUser.id}`)
)

RLS specific example - get the name and value of the parameter to be set on the connection before executing each of the commands in the transaction

const [userList, updateUser] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
  prisma.post.count(),
],
"current_user_id",
currentUser.id)

@matthewmueller
Copy link
Contributor Author

matthewmueller commented Apr 26, 2021

Thanks for the suggestion @yuval-hazaz! I like your suggestion of offering some options in the transaction API that could do this. That could also be a good place for transaction settings (read committed, read uncommitted, etc.)

The fundamental problem is that you need to SET within the same connection as the write, otherwise when you go to write, you may pull out a different connection from the pool that doesn't have that setting. So they need to be within the same prisma call, can't be separate calls like your first example.

A couple things that we'll also need to check:

  1. If you create a policy that excludes certain fields from the current user, does that pass through the query engine correctly? That's on us to investigate, but we may need to do some work there.
  2. Typescript's types will also assume full access unless we're able to generate different clients for different users. I wonder if it makes sense to generate separate Typescript clients for each user. What do you think?
  3. What's the lifecycle of a setting and how are the settings reset? If it's when you terminate the connection, the pool could potentially have pre-set connections. This is fine if you override the setting each time, but if you use a connection without overriding the setting, you may be authenticated as the wrong person.

@yuval-hazaz
Copy link

Thank you @matthewmueller.
Those are good questions.
I think that Prisma can ignore the policies - Prisma can always reflect the full schema - but the data will not be available.

@matthewmueller
Copy link
Contributor Author

matthewmueller commented Apr 26, 2021

@yuval-hazaz did you also try the following?

const [ignore, userList, updateUser] = await prisma.$transaction([
  prisma.$executeRaw(`SET current_user_id = ${currentUser.id}`),
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
  prisma.post.count(),
])

Looks like you may also need to SET LOCAL to avoid the 3rd issue.

@yuval-hazaz
Copy link

I didn't, since you wrote "Without access to the connection pool, there's no way to guarantee you'll get the same connection each query" - so, even if it will work on my dev machine, I cannot guarantee it will work on prod

@janpio
Copy link
Member

janpio commented Apr 26, 2021

In context of a transaction, started via prisma.$transaction, this statement might not hold true. We just realized that and will now have to test that. Right now we think it should be true, any feedback from you would be helpful.

@yuval-hazaz
Copy link

ho... wow.. that is interesting... IIUC if that will work, it is all we need.
I will test that and let you know, but I think any test on dev env. will not prove anything (unless it will fail...)

@remioo
Copy link

remioo commented May 19, 2021

Interested in this issue as well: I want to implement RLS in postgres.
If I understand correctly, the proposed workaround would be to wrap every request in a transaction with a SET before it.
replacing

const userList = await prisma.post.findMany({ where: { title: { contains: 'prisma' } } })

with

const [ignore, userList] = await prisma.$transaction([
  prisma.$executeRaw(`SET current_user_id = ${currentUser.id}`),
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
])

Is there any way to force every request from the prisma client to go through this transaction + SET?
e.g. a rlsPrismaClient that alway does the SET before a request

const userList = await rlsPrisma.post.findMany({ where: { title: { contains: 'prisma' } } })

I could have a class that exposes every find/create/delete... for each prisma object and wraps them in a transaction, but that would not be very generic... Maybe there is a prisma mechanism that would help?

Edit: In the meantime I will use a middleware to force a where clause on the current_id, but a RLS solution to provide a check at the database layer would be best.

@janpio
Copy link
Member

janpio commented May 19, 2021

You might be able to force the transaction and query in a middleware as well. You do not have to execute next() in a middleware, so you could take any query and internally create a $transaction() call with the necessary queries. (Might be problematic how to differentiate between the original one and the already modified one... would need to try if the middleware can handle that somehow. I think query get a param if they are run in an explicit transaction already). (Happy to chat about this on slack.prisma.io if you take a shot at building this @remioo)

@remioo
Copy link

remioo commented May 26, 2021

I tried the middleware solution and it seems to do the job.
I did not do advanced tests on that.

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return next(params);

    // Generate model class name from model params (PascalCase to camelCase)
    const modelName =
      params.model.charAt(0).toLowerCase() + params.model.slice(1);
    const [, results] = await prisma.$transaction([
      prisma.$executeRaw(
        `SET current_user_id = ${currentUser.id}`,
      ),
      // Call function
      // Assumptions: 
      // - prisma model class is params.model in camelCase
      // - prisma function name is params.action
      prisma[modelName][params.action](params.args),
    ]);
    return results;
  };
    prisma.$use(rlsMiddleware);

I took two assumptions that are hope are true:

  • prisma model class is params.model in camelCase
  • prisma function name is params.action

The main issue I see with this is does not work with transactions.

One workaround would be to be able to pass a context to the middleware, as described here: #6882
I could then add a setUserDone: true or something similar and test on that instead of runInTransaction at the start of the middleware.

@mayerlench
Copy link

I tried the middleware solution and it seems to do the job.
I did not do advanced tests on that.

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return next(params);

    // Generate model class name from model params (PascalCase to camelCase)
    const modelName =
      params.model.charAt(0).toLowerCase() + params.model.slice(1);
    const [, results] = await prisma.$transaction([
      prisma.$executeRaw(
        `SET current_user_id = ${currentUser.id}`,
      ),
      // Call function
      // Assumptions: 
      // - prisma model class is params.model in camelCase
      // - prisma function name is params.action
      prisma[modelName][params.action](params.args),
    ]);
    return results;
  };
    prisma.$use(rlsMiddleware);

I took two assumptions that are hope are true:

  • prisma model class is params.model in camelCase
  • prisma function name is params.action

The main issue I see with this is does not work with transactions.

One workaround would be to be able to pass a context to the middleware, as described here: #6882
I could then add a setUserDone: true or something similar and test on that instead of runInTransaction at the start of the middleware.

Getting it to work with transactions might just be workable by either unraveling the transaction and then creating a new one (if thats possible).

My other concern is, i dont want to have to pass in a param of currentUser on every request. AFAIK, middlewares are stateless, it doesnt have context

@ntgussoni
Copy link

I tried the middleware solution and it seems to do the job.
I did not do advanced tests on that.

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return next(params);

    // Generate model class name from model params (PascalCase to camelCase)
    const modelName =
      params.model.charAt(0).toLowerCase() + params.model.slice(1);
    const [, results] = await prisma.$transaction([
      prisma.$executeRaw(
        `SET current_user_id = ${currentUser.id}`,
      ),
      // Call function
      // Assumptions: 
      // - prisma model class is params.model in camelCase
      // - prisma function name is params.action
      prisma[modelName][params.action](params.args),
    ]);
    return results;
  };
    prisma.$use(rlsMiddleware);

I took two assumptions that are hope are true:

  • prisma model class is params.model in camelCase
  • prisma function name is params.action

The main issue I see with this is does not work with transactions.

One workaround would be to be able to pass a context to the middleware, as described here: #6882
I could then add a setUserDone: true or something similar and test on that instead of runInTransaction at the start of the middleware.

My main concern here is that there's no way to get the currentUser unless you are adding it to all queries, which is terrible.
I currently have 2 libraries that depend on this and I cannot release them as I need to pass request-related context to the middleware.

@janpio I've been tracking all these issues for months now, is there anything in the roadmap to allow us to have contextual data in prisma middlewares?

@janpio
Copy link
Member

janpio commented Jul 30, 2021

No, not at this time. You can see the public roadmap at http://pris.ly/roadmap

@ntgussoni
Copy link

No, not at this time. You can see the public roadmap at http://pris.ly/roadmap

Thank you! Have a nice weekend

@leesus
Copy link

leesus commented Sep 8, 2021

@remioo I've been using something similar to your snippet to use RLS via supabase for a few months, but have run into a situation where I'm leveraging $queryRaw more and more often - have you found a way of calling $queryRaw within a transaction (i.e. where client['model'] is not available)? I can't seem to find a way without getting errors.

@remioo
Copy link

remioo commented Sep 9, 2021

Indeed I have tweaked the middleware to handle $queryRaw and $executeRaw
It now looks like that:

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return await next(params);

    let results;
    if (
      params.model &&
      typeof params.model === 'string' &&
      params.model.length > 0
    ) {
      // Generate model class name from model params (PascalCase to camelCase)
      const modelName =
        params.model.charAt(0).toLowerCase() + params.model.slice(1);
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient[modelName][params.action](params.args),
      ]);
    } else if (params.action === 'executeRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$executeRaw(params.args),
      ]);
    } else if (params.action === 'queryRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$queryRaw(params.args.query),
      ]);
    }
    return results;
  };

Let me know if that works for you.

@eduhenke
Copy link

eduhenke commented Sep 16, 2021

I tried the middleware solution and it seems to do the job.
I did not do advanced tests on that.

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return next(params);

    // Generate model class name from model params (PascalCase to camelCase)
    const modelName =
      params.model.charAt(0).toLowerCase() + params.model.slice(1);
    const [, results] = await prisma.$transaction([
      prisma.$executeRaw(
        `SET current_user_id = ${currentUser.id}`,
      ),
      // Call function
      // Assumptions: 
      // - prisma model class is params.model in camelCase
      // - prisma function name is params.action
      prisma[modelName][params.action](params.args),
    ]);
    return results;
  };
    prisma.$use(rlsMiddleware);

I took two assumptions that are hope are true:

  • prisma model class is params.model in camelCase
  • prisma function name is params.action

The main issue I see with this is does not work with transactions.
One workaround would be to be able to pass a context to the middleware, as described here: #6882
I could then add a setUserDone: true or something similar and test on that instead of runInTransaction at the start of the middleware.

My main concern here is that there's no way to get the currentUser unless you are adding it to all queries, which is terrible.
I currently have 2 libraries that depend on this and I cannot release them as I need to pass request-related context to the middleware.

@janpio I've been tracking all these issues for months now, is there anything in the roadmap to allow us to have contextual data in prisma middlewares?

Hello, we are currently using a mix of a prisma RLS middleware, and using a CLS-based library to perform Postgres queries, that change the RLS variables depending on the content of the request(but you could use any CLS library, and not restrict to requests).

We are using fastify, so we are using https://github.com/fastify/fastify-request-context, to have a context shared between our fastify server, and the prisma server(but there is also this: https://www.npmjs.com/package/express-request-context, or you could use something more low-level like https://www.npmjs.com/package/cls-hooked, or even the node-based AsyncLocalStorage):

import Fastify from 'fastify'
import { fastifyRequestContextPlugin } from 'fastify-request-context';

const app = Fastify();
app.register(fastifyRequestContextPlugin, {});

Then, when the request gets parsed, we have a function that is responsible to feed data in that "request context"(we are using graphql, so it is the server context creation function, but it could also be an express middleware that does this):

import { requestContext } from 'fastify-request-context';

async function createContext(req) {
  // ...
  const account = await getAccount(req);
  const organization = await getOrganization(req);
  const authContext: AuthContext = { account, organization };
  requestContext.set('authContext', authContext);
  // ...
}

Then we have a prisma middleware that gets that information and applies to a local variable in Postgres(like the above mentioned solution):

export const createRlsMiddleware = (prisma: PrismaClient) =>
  async (
    params: Prisma.MiddlewareParams,
    next: (params: Prisma.MiddlewareParams) => Promise<any>
  ) => {
    if (params.runInTransaction) return next(params);

    // TODO: validate
    if (params.model == null) return next(params);

    // Generate model class name from model params (PascalCase to camelCase)
    const modelName =
      params.model.charAt(0).toLowerCase() + params.model.slice(1);

    const { organization } = requestContext.get('authContext') as AuthContext | null ?? {};

    // warning: set local does not work completely as you expect
    // https://www.postgresql.org/message-id/flat/56842412.5000005@joeconway.com
    const organizationPolicy = prisma.$executeRaw(`SET LOCAL app.current_organization = '${organization?.id ?? ''}'`);

    // @ts-ignore
    const prismaAction = prisma[modelName][params.action](params.args);

    const results = await prisma.$transaction([organizationPolicy, prismaAction]);
    return results.pop();
  };

Hope this helps someone :)

@dimaip
Copy link

dimaip commented Sep 17, 2021

Does anyone know of anything similar we could use with Mongodb?

@hyusetiawan
Copy link

would love to get a first-class support for this on prisma, or maybe a sanctioned middleware like what @eduhenke's solution?

@eduhenke
Copy link

For people looking for a short-term solution for using RLS with Prisma, I ended up creating one Prisma client per user based on the user ID in the request.

It's a clean, easy, and safe (no risk of the queries using the wrong parameters) solution but is only acceptable if the number of concurrent users is low. With this approach, you will be creating a lot of Prisma clients which comes with overhead and a lot of connections to the database. That might be mitigated by using a proxy like pgproxy.

I recommend:

Example:

const prismaClients: { [userId: number]: PrismaClient } = {};

async function getPrismaClientByUserId(userId) {
    if (!prismaClients[userId]) {
        prismaClients[userId] = new PrismaClient();
    
        await prismaClients[userId].$executeRawUnsafe(
            `SET app.current_user_id = ${userId}` // Make sure to sanitize the user ID
        );
    }

    return prismaClients[userId];
}

const prisma = await getPrismaClientByUserId('user ID');

@jawadst If the connection limit of each PrismaClient is set to 5(as in each prisma.model.action() you perform, you'll select one of those 5 connections to perform this query), and you only set the variable once(SET app.current_user_id after creating the PrismaClient), the other 4 connections that may sometimes be selected won't have that variable set, correct?

I've done your suggestion and when I increase that limit to anything higher than 1, I sometimes get the error from Postgres that the variable was not set. So I always set the connection limit to 1. Do you also have that problem, or you've found a workaround for that?

@jawadst
Copy link

jawadst commented Jul 18, 2022

@eduhenke I do have connection limit set to 1 in my code as that was enough for my use case. It's possible more than 1 does not work indeed with the code that I posted, I have not tested it. I updated my comment to make that clear.

There might be a way to run a query when a new connection is open in Prisma.

@jtmarmon
Copy link

jtmarmon commented Sep 6, 2022

This is also relevant to #14749. Prisma text search uses the postgres default language, which could be set per connection if supported (there's a workaround which I posted on that issue)

@jmarbutt
Copy link

jmarbutt commented Oct 5, 2022

Does anyone have a working workaround?

@andyjy
Copy link
Contributor

andyjy commented Oct 5, 2022

Does anyone have a working workaround?

Yes - I am using a similar approach to the one very thoroughly documented above by @wladiston. If you read all the comments people have posted above you should find success.

TL;DR -- wrap all your relevant Prisma calls inside transactions, and use prisma.$executeRawUnsafe(`SET LOCAL ..`) to set Postgres variables:

  • Transactions plus SET LOCAL instead of SET to set the Postgres variable(s) only for the current transaction, to avoid leakage between different users.
  • prisma.$executeRawUnsafe instead of prisma.$executeRaw since SET/SET LOCAL can't be used inside Postgres prepared statements that prisma.$executeRaw creates to safely pass parameters.

If you want to do this in many places throughout your app, most of the work is in creating a way to use the above approach in a reusable way, e.g. creating a Prisma middleware (various example code in comments above).

The middleware approach seems to be working for others but struck me as quite a major hack to the internals of how Prisma works and tricky to make work for all cases, so I'm using an alternative approach using a wrapper function around all my Prisma calls (and considering exploring whether a Proxy object around the Prisma client might work better - not tried yet).

(If you meant a workaround for a full implementation of row-level security, that is also doable but is a broader topic beyond the (current) scope of Prisma that requires a bunch of Postgres-specific work using CREATE POLICY ... and ALTER TABLE xxx ENABLE ROW LEVEL SECURITY in your migrations to create and apply Postgres policies that apply rules referencing the variables passed via SET LOCAL. Again, examples in wladiston's post above.)

@joe-giunti-kiefa
Copy link

joe-giunti-kiefa commented Oct 6, 2022

@andyjy I took a swing at the Proxy object based implementation since I also found that the approach using middleware is very hacky and actually alters how prisma is handling the request internally. In fact, the middleware approach simply does not work for queryRaw.

export const prisma = new Proxy(
  getClient(() => {
    const client = new PrismaClient({
      log:
        env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
      datasources: {
        db: {
          url: process.env.DATABASE_URL_APP,
        },
      },
    });
    return client;
  }),
  {
    get(prismaProxy, p, receiver) {
      const org = storage.getStore()?.orgId;
      //catch calls directly on the prisma client such as $queryRaw and $executeRaw, but ignore internal methods
      if (
        typeof p == "string" &&
        p.length > 0 &&
        (p.charAt(0) == "$" || p.charAt(0) != "_") &&
        !p.endsWith("Internal")
      ) {
        // @ts-expect-error
        return new Proxy(prismaProxy[p], {
          apply(func, thisArg, argArray) {
            return prismaProxy
              .$transaction([
                prismaProxy.$executeRawUnsafe(
                  `SET LOCAL request.orgId = '${org}'`
                ),
                Reflect.apply(func, thisArg, argArray) as PrismaPromise<any>,
              ])
              .then((value) => {
                const [, result] = value;
                return result;
              });
          },
          get(modelProxy, pp, receiver) {
            //handle the case when they are accessing a model, such as prisma.model.findMany({})
            if (
              typeof p == "string" &&
              p.length > 0 &&
              p.charAt(0) != "_" &&
              !p.endsWith("Internal")
            ) {
              return new Proxy(modelProxy[pp], {
               //function call on model, such as model.findMany()
                apply(func, thisArg, argArray) {
                  return prismaProxy
                    .$transaction([
                      prismaProxy.$executeRawUnsafe(
                        `SET LOCAL request.orgId = '${org}'`
                      ),
                      Reflect.apply(
                        func,
                        thisArg,
                        argArray
                      ) as PrismaPromise<any>,
                    ])
                    .then((value) => {
                      const [, result] = value;
                      return result;
                    });
                },
              });
            } else {
              return Reflect.get(modelProxy, pp, receiver);
            }
          },
        });
      }
      return Reflect.get(prismaProxy, p, receiver);
    },
  }
);

@AllanOricil
Copy link

@andyjy I took a swing at the Proxy object based implementation since I also found that the approach using middleware is very hacky and actually alters how prisma is handling the request internally. In fact, the middleware approach simply does not work for queryRaw.

export const prisma = new Proxy(
  getClient(() => {
    const client = new PrismaClient({
      log:
        env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
      datasources: {
        db: {
          url: process.env.DATABASE_URL_APP,
        },
      },
    });
    return client;
  }),
  {
    get(prismaProxy, p, receiver) {
      const org = storage.getStore()?.orgId;
      //catch calls directly on the prisma client such as $queryRaw and $executeRaw, but ignore internal methods
      if (
        typeof p == "string" &&
        p.length > 0 &&
        (p.charAt(0) == "$" || p.charAt(0) != "_") &&
        !p.endsWith("Internal")
      ) {
        // @ts-expect-error
        return new Proxy(prismaProxy[p], {
          apply(func, thisArg, argArray) {
            return prismaProxy
              .$transaction([
                prismaProxy.$executeRawUnsafe(
                  `SET LOCAL request.orgId = '${org}'`
                ),
                Reflect.apply(func, thisArg, argArray) as PrismaPromise<any>,
              ])
              .then((value) => {
                const [, result] = value;
                return result;
              });
          },
          get(modelProxy, pp, receiver) {
            //handle the case when they are accessing a model, such as prisma.model.findMany({})
            if (
              typeof p == "string" &&
              p.length > 0 &&
              p.charAt(0) != "_" &&
              !p.endsWith("Internal")
            ) {
              return new Proxy(modelProxy[pp], {
               //function call on model, such as model.findMany()
                apply(func, thisArg, argArray) {
                  return prismaProxy
                    .$transaction([
                      prismaProxy.$executeRawUnsafe(
                        `SET LOCAL request.orgId = '${org}'`
                      ),
                      Reflect.apply(
                        func,
                        thisArg,
                        argArray
                      ) as PrismaPromise<any>,
                    ])
                    .then((value) => {
                      const [, result] = value;
                      return result;
                    });
                },
              });
            } else {
              return Reflect.get(modelProxy, pp, receiver);
            }
          },
        });
      }
      return Reflect.get(prismaProxy, p, receiver);
    },
  }
);

@joe-giunti-kiefa really interesting what you did. Thanks for sharing.

@IbrahimFathy19
Copy link

@remioo

Indeed I have tweaked the middleware to handle $queryRaw and $executeRaw It now looks like that:

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return await next(params);

    let results;
    if (
      params.model &&
      typeof params.model === 'string' &&
      params.model.length > 0
    ) {
      // Generate model class name from model params (PascalCase to camelCase)
      const modelName =
        params.model.charAt(0).toLowerCase() + params.model.slice(1);
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient[modelName][params.action](params.args),
      ]);
    } else if (params.action === 'executeRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$executeRaw(params.args),
      ]);
    } else if (params.action === 'queryRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$queryRaw(params.args.query),
      ]);
    }
    return results;
  };

Let me know if that works for you.

I'm not sure if $queryRaw and $executeRaw work, They throw this error

Error: `$queryRaw` is a tag function, please use it like the following:

const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`

And I believe this is related to issue#5083
any idea?

@IbrahimFathy19
Copy link

@andyjy I took a swing at the Proxy object based implementation since I also found that the approach using middleware is very hacky and actually alters how prisma is handling the request internally. In fact, the middleware approach simply does not work for queryRaw.

export const prisma = new Proxy(
  getClient(() => {
    const client = new PrismaClient({
      log:
        env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
      datasources: {
        db: {
          url: process.env.DATABASE_URL_APP,
        },
      },
    });
    return client;
  }),
  {
    get(prismaProxy, p, receiver) {
      const org = storage.getStore()?.orgId;
      //catch calls directly on the prisma client such as $queryRaw and $executeRaw, but ignore internal methods
      if (
        typeof p == "string" &&
        p.length > 0 &&
        (p.charAt(0) == "$" || p.charAt(0) != "_") &&
        !p.endsWith("Internal")
      ) {
        // @ts-expect-error
        return new Proxy(prismaProxy[p], {
          apply(func, thisArg, argArray) {
            return prismaProxy
              .$transaction([
                prismaProxy.$executeRawUnsafe(
                  `SET LOCAL request.orgId = '${org}'`
                ),
                Reflect.apply(func, thisArg, argArray) as PrismaPromise<any>,
              ])
              .then((value) => {
                const [, result] = value;
                return result;
              });
          },
          get(modelProxy, pp, receiver) {
            //handle the case when they are accessing a model, such as prisma.model.findMany({})
            if (
              typeof p == "string" &&
              p.length > 0 &&
              p.charAt(0) != "_" &&
              !p.endsWith("Internal")
            ) {
              return new Proxy(modelProxy[pp], {
               //function call on model, such as model.findMany()
                apply(func, thisArg, argArray) {
                  return prismaProxy
                    .$transaction([
                      prismaProxy.$executeRawUnsafe(
                        `SET LOCAL request.orgId = '${org}'`
                      ),
                      Reflect.apply(
                        func,
                        thisArg,
                        argArray
                      ) as PrismaPromise<any>,
                    ])
                    .then((value) => {
                      const [, result] = value;
                      return result;
                    });
                },
              });
            } else {
              return Reflect.get(modelProxy, pp, receiver);
            }
          },
        });
      }
      return Reflect.get(prismaProxy, p, receiver);
    },
  }
);

This a great work, did you manage to fix if the caller already uses a transaction? how to destructure all of them into a single transaction? @joe-giunti-kiefa

@remioo
Copy link

remioo commented Oct 11, 2022

@remioo

Indeed I have tweaked the middleware to handle $queryRaw and $executeRaw It now looks like that:

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return await next(params);

    let results;
    if (
      params.model &&
      typeof params.model === 'string' &&
      params.model.length > 0
    ) {
      // Generate model class name from model params (PascalCase to camelCase)
      const modelName =
        params.model.charAt(0).toLowerCase() + params.model.slice(1);
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient[modelName][params.action](params.args),
      ]);
    } else if (params.action === 'executeRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$executeRaw(params.args),
      ]);
    } else if (params.action === 'queryRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$queryRaw(params.args.query),
      ]);
    }
    return results;
  };

Let me know if that works for you.

I'm not sure if $queryRaw and $executeRaw work, They throw this error

Error: `$queryRaw` is a tag function, please use it like the following:

const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`

And I believe this is related to issue#5083 any idea?

In the recent prisma version we had to replace $executeRaw and $queryRaw by $executeRawUnsafe and $queryRawUnsafe

@joe-giunti-kiefa
Copy link

@remioo does this code actually return data for you? From what we found with this approach is that all the fields of the data are undefined when returned to the client, despite being able to see the correct data in results in the middlware. What version of prisma are you using?

@remioo

Indeed I have tweaked the middleware to handle $queryRaw and $executeRaw It now looks like that:

  rlsMiddleware = async (params, next) => {
    if (params.runInTransaction) return await next(params);

    let results;
    if (
      params.model &&
      typeof params.model === 'string' &&
      params.model.length > 0
    ) {
      // Generate model class name from model params (PascalCase to camelCase)
      const modelName =
        params.model.charAt(0).toLowerCase() + params.model.slice(1);
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient[modelName][params.action](params.args),
      ]);
    } else if (params.action === 'executeRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$executeRaw(params.args),
      ]);
    } else if (params.action === 'queryRaw') {
      [, results] = await this.prismaRWClient.$transaction([
        this.prismaRWClient.$executeRaw(
          `SET app.current_user_id  = ${currentUser.id}`,
        ),
        // Call function
        this.prismaRWClient.$queryRaw(params.args.query),
      ]);
    }
    return results;
  };

Let me know if that works for you.

I'm not sure if $queryRaw and $executeRaw work, They throw this error

Error: `$queryRaw` is a tag function, please use it like the following:

const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`

And I believe this is related to issue#5083 any idea?

In the recent prisma version we had to replace $executeRaw and $queryRaw by $executeRawUnsafe and $queryRawUnsafe

@eduhenke
Copy link

@andyjy I took a swing at the Proxy object based implementation since I also found that the approach using middleware is very hacky and actually alters how prisma is handling the request internally. In fact, the middleware approach simply does not work for queryRaw.

export const prisma = new Proxy(
  getClient(() => {
    const client = new PrismaClient({
      log:
        env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
      datasources: {
        db: {
          url: process.env.DATABASE_URL_APP,
        },
      },
    });
    return client;
  }),
  {
    get(prismaProxy, p, receiver) {
      const org = storage.getStore()?.orgId;
      //catch calls directly on the prisma client such as $queryRaw and $executeRaw, but ignore internal methods
      if (
        typeof p == "string" &&
        p.length > 0 &&
        (p.charAt(0) == "$" || p.charAt(0) != "_") &&
        !p.endsWith("Internal")
      ) {
        // @ts-expect-error
        return new Proxy(prismaProxy[p], {
          apply(func, thisArg, argArray) {
            return prismaProxy
              .$transaction([
                prismaProxy.$executeRawUnsafe(
                  `SET LOCAL request.orgId = '${org}'`
                ),
                Reflect.apply(func, thisArg, argArray) as PrismaPromise<any>,
              ])
              .then((value) => {
                const [, result] = value;
                return result;
              });
          },
          get(modelProxy, pp, receiver) {
            //handle the case when they are accessing a model, such as prisma.model.findMany({})
            if (
              typeof p == "string" &&
              p.length > 0 &&
              p.charAt(0) != "_" &&
              !p.endsWith("Internal")
            ) {
              return new Proxy(modelProxy[pp], {
               //function call on model, such as model.findMany()
                apply(func, thisArg, argArray) {
                  return prismaProxy
                    .$transaction([
                      prismaProxy.$executeRawUnsafe(
                        `SET LOCAL request.orgId = '${org}'`
                      ),
                      Reflect.apply(
                        func,
                        thisArg,
                        argArray
                      ) as PrismaPromise<any>,
                    ])
                    .then((value) => {
                      const [, result] = value;
                      return result;
                    });
                },
              });
            } else {
              return Reflect.get(modelProxy, pp, receiver);
            }
          },
        });
      }
      return Reflect.get(prismaProxy, p, receiver);
    },
  }
);

That's great, was any one able to make it work with the fluent API? We need to be able to use it with the fluent API, as it solves the N+1 problem, regarding query optimization: https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance

@joe-giunti-kiefa
Copy link

@andyjy I took a swing at the Proxy object based implementation since I also found that the approach using middleware is very hacky and actually alters how prisma is handling the request internally. In fact, the middleware approach simply does not work for queryRaw.

export const prisma = new Proxy(
  getClient(() => {
    const client = new PrismaClient({
      log:
        env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
      datasources: {
        db: {
          url: process.env.DATABASE_URL_APP,
        },
      },
    });
    return client;
  }),
  {
    get(prismaProxy, p, receiver) {
      const org = storage.getStore()?.orgId;
      //catch calls directly on the prisma client such as $queryRaw and $executeRaw, but ignore internal methods
      if (
        typeof p == "string" &&
        p.length > 0 &&
        (p.charAt(0) == "$" || p.charAt(0) != "_") &&
        !p.endsWith("Internal")
      ) {
        // @ts-expect-error
        return new Proxy(prismaProxy[p], {
          apply(func, thisArg, argArray) {
            return prismaProxy
              .$transaction([
                prismaProxy.$executeRawUnsafe(
                  `SET LOCAL request.orgId = '${org}'`
                ),
                Reflect.apply(func, thisArg, argArray) as PrismaPromise<any>,
              ])
              .then((value) => {
                const [, result] = value;
                return result;
              });
          },
          get(modelProxy, pp, receiver) {
            //handle the case when they are accessing a model, such as prisma.model.findMany({})
            if (
              typeof p == "string" &&
              p.length > 0 &&
              p.charAt(0) != "_" &&
              !p.endsWith("Internal")
            ) {
              return new Proxy(modelProxy[pp], {
               //function call on model, such as model.findMany()
                apply(func, thisArg, argArray) {
                  return prismaProxy
                    .$transaction([
                      prismaProxy.$executeRawUnsafe(
                        `SET LOCAL request.orgId = '${org}'`
                      ),
                      Reflect.apply(
                        func,
                        thisArg,
                        argArray
                      ) as PrismaPromise<any>,
                    ])
                    .then((value) => {
                      const [, result] = value;
                      return result;
                    });
                },
              });
            } else {
              return Reflect.get(modelProxy, pp, receiver);
            }
          },
        });
      }
      return Reflect.get(prismaProxy, p, receiver);
    },
  }
);

This a great work, did you manage to fix if the caller already uses a transaction? how to destructure all of them into a single transaction? @joe-giunti-kiefa

@IbrahimFathy19 Unfortunately I have not figured out a way to account for the caller already having a transaction.

@janpio janpio changed the title Supporting Postgres' SET across queries of a request Supporting Postgres' SET across queries of a request Nov 5, 2022
@ozum
Copy link

ozum commented Jan 11, 2023

Thanks, everyone. @wladiston, I used your guide for Supabase.

I also stumbled upon a new feature at the preview stage called "Prisma Client Extensions". Prisma docs mention in the docs, that it can be used for RLS.

There is also a blog post about a custom Prisma client for RLS, developed before Prisma Client Extensions. The writer explains nicely what issues he had with several methods.

Prisma points to examples published by @sbking. There is an RLS example developed as a Prisma Client Extension.

As a result, I create an example for row level security. I'm new to Supabase and Prisma. Can the example below be used in the long run? (Memory consumption or leak, performance problem, etc.)

Notes & Thoughts:

  • Prisma Client Extension is called for every request. Does it create a new prisma client for each request?

TLDR; Example

function rlsClient(jwtClaim = "{}"): (client: any) => PrismaClient<any, any, any, Types.Extensions.Args> {
  return Prisma.defineExtension((prisma) =>
    // @ts-ignore (Excessive stack depth comparing types...)
    prisma.$extends({
      query: {
        $allModels: {
          async $allOperations({ args, query }) {
            const [, result] = await prisma.$transaction([
              prisma.$executeRawUnsafe(`SELECT set_config('request.jwt.claim', '${jwtClaim}', TRUE)`),
              query(args),
            ]);
            return result;
          },
        },
      },
    })
  );
}

Supabase & Prisma & Nuxt RLS Example

My RLS implementation is using "businessId" of the user. This data is added to the auth.user_metadata column in the database (For example during user sign up).

server/middleware/1.user.ts
Nuxt server middleware to add the user and JWT token to the context.

import type { H3Event } from "h3";
import { serverSupabaseUser } from "#supabase/server";

// Server middlewares are executed in reverse order. 02 -> 01 -> 00 etc...

/**
 * Adds logged in user into the context.
 *
 * @example
 * const user = event.context._user
 */
export default eventHandler(async (event: H3Event) => {
  await serverSupabaseUser(event);
});

server/middleware/0.prisma.ts
Nuxt server middleware to add JWT data into PostgreSQL settings for each request and return extended prisma client for RLS. JWT data was added to the context by previous middleware.

import { Prisma, PrismaClient } from "@prisma/client";
import type { Types } from "@prisma/client/runtime/index";
import type { H3Event } from "h3";

// Server middlewares are executed in reverse order. 02 -> 01 -> 00 etc...

/** Singleton prisma client. */
let prisma: PrismaClient;

/**
 * Prisma type declaration for event context.
 * Add `prisma` attribute to the `event.context` type to
 * `const prisma = event.context.prisma;`
 */
declare module "h3" {
  interface H3EventContext {
    prisma: PrismaClient;
  }
}

/**
 * Decodes given JWT without verifying.
 *
 * @param token is the token.
 * @returns object data decoded from JWT as a string.
 */
function decodeJwt(token: string): string | undefined {
  return token ? Buffer.from(token.split(".")[1], "base64").toString() : undefined;
}

/**
 * Nuxt middleware function which adds extended prisma RLS client into context.
 * Middleware handlers will run on every request before any other server route.
 */
export default eventHandler((event: H3Event) => {
  if (!prisma) prisma = new PrismaClient();
  const token = event.context._user ? event.context._token : undefined;
  event.context.prisma = prisma.$extends(rlsClient(decodeJwt(token))) as any as PrismaClient;
});

/**
 * Creates a Prisma Client Extension with RLS which injects Supabase JWT data
 * into PostgreSQL local config.
 *
 * Queries are wrapped in a transaction, because PostgreSQL connection pool
 * may give different connections for the same session. Transactions overcome
 * this problem.
 *
 * @param jwtClaim JWT object as a string.
 * @returns Prisma Client Extension with RLS
 * @see https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions
 * @see https://github.com/sbking/prisma-client-extensions/blob/main/examples/row-level-security/script.ts
 * @see https://github.com/prisma/prisma/issues/5128#issuecomment-1059814093
 */
function rlsClient(jwtClaim = "{}"): (client: any) => PrismaClient<any, any, any, Types.Extensions.Args> {
  return Prisma.defineExtension((prisma) =>
    // @ts-ignore (Excessive stack depth comparing types...)
    prisma.$extends({
      query: {
        $allModels: {
          async $allOperations({ args, query }) {
            const [, result] = await prisma.$transaction([
              prisma.$executeRawUnsafe(`SELECT set_config('request.jwt.claim', '${jwtClaim}', TRUE)`),
              query(args),
            ]);
            return result;
          },
        },
      },
    })
  );
}

server/api/business.get.ts
Nuxt server-side API. (BTW Nuxt uses Nitro)

export default eventHandler(async (event) => {
  const prisma = event.context.prisma;
  const business = await prisma.business.findMany();
  return business;
});

Business Table SQL

-- Record user "businessId" as 'bid' in the user.raw_user_meta_data JSONB field
-- 1) Fetch 'bid' using supabase client from user.raw_user_meta_data. (Usually supabase client)
-- 2) Fetch 'bid' from app.bid (Usually for Prisma or 3rd party ORM)

CREATE TABLE "Business" (
  "id" UUID DEFAULT gen_random_uuid() PRIMARY KEY
)

ALTER TABLE "Business" ENABLE ROW LEVEL SECURITY;

CREATE POLICY "BusinessAccess" ON "Business"
  FOR ALL
  USING (id = (COALESCE(auth.jwt(), '{}')->'user_metadata'->>'bid')::uuid);

@jiashengguo
Copy link
Contributor

jiashengguo commented Mar 13, 2023

The Prisma extension library ZenStack we are building uses a declarative way in the schema to handle access control by code generation instead of using RLS. So actually, you can use it for whatever database, not limited to Postgres. To achieve what Postgres can do in the original problem, you could simply add an allow policy rule in the schema as below:

model Post {
    id Int @id() @default(autoincrement())
    title String
    owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
    ownerId String

    // Only the owner has full access. 
    // auth() returns current user
    @@allow('all', auth() == author)
}

And then create a wrapper of Prisma client using withPresets provided by ZenStack which adds a transparent proxy on Prisma guarded by the access policy defined in the schema above

// the standard Prisma client
const prisma = new PrismaClient();

app.get('/posts', (req, res) => {
  const db = withPresets(prisma, { user: getSessionUser(req) });
  res.json(await db.post.findMany());
})

The above API only returns the data filtered by ownerId = current_user_id.

Of course, you could have more flexible fine-grained control over it using Access Policy of ZenStack, below is a tutorial post for a more complicated use case:

How to build a collaborative SaaS product using Next.js and ZenStack's access control policy

We would really appreciate it if you could share your opinions by commenting or joining our Discord to help us make ZenStack the right thing to solve your problems.

@moofoo
Copy link

moofoo commented May 12, 2023

Here's a multi-tenant NestJS implementation that uses Prisma Client Extensions and AsyncLocalStorage (with nestjs-cls):

Link to dev.to article

A complete example app can be found here

@anton-johansson
Copy link

Jumping in here as we have basically the exact same issue for our multi-tenancy solution. I want to create one connection pool per MSSQL server rather than one pool for each database. This is because one connection pool per databases causes a lot of overhead and consumes a lot of memory, and it's generally just cleaner to have one shared pool. This also lets me more easily utilize the metrics (preview) feature.

This means I need to run USE [<name of database>] before each query, similar to what you have already been talking about with SET.

I could use use client extensions to wrap each query in a transaction and call the USE command first, like this:

    // Extend Prisma so that we can switch database on the fly. This allows us to use one single connection pool for each SQL server.
    // In order to avoid leaking connections, we must do these two calls within a transaction.
    // This is far from optimal.
    const extendedClient = client.$extends({
        query: {
            async $allOperations({query, operation, args}) {
                const [, result] = await client.$transaction([
                    client.$executeRawUnsafe(`USE [${databaseName}]`),
                    query(args),
                ]);

                return result;
            },
        },
    });

However, I don't really want a transaction for each query. It will cause an additional performance overhead that I simply do not want.

I would like to suggest a new function. One that behaves similar to $transaction, except that it does not actually create an SQL transaction. It simply reserves a connection from the connection pool during it's execution. Maybe $withinSameConnection or $reserveConnection? It would look like this:

    // Extend Prisma so that we can switch database on the fly. This allows us to use one single connection pool for each SQL server.
    const extendedClient = client.$extends({
        query: {
            async $allOperations({query, operation, args}) {
                const [, result] = await client.$withinSameConnection([
                    client.$executeRawUnsafe(`USE [${databaseName}]`),
                    query(args),
                ]);

                return result;
            },
        },
    });

I'm not familiar with the source code of Prisma. But I feel like this shouldn't be too much work (if you are OK with the suggested solution). Here is where it starts:

$transaction(input: any, options?: any) {

I can't really find out where it picks a connection from the pool. I would've guessed somewhere here:

export function waitForBatch<T extends PromiseLike<unknown>[]>(

But it doesn't seem to do that either. Maybe someone can point me in the right direction?

@janpio janpio self-assigned this Jun 13, 2024
@anton-johansson
Copy link

One more thing I came to think about regarding my suggested solution above. Since my intention is to run this as an extension on all calls and I still want the option to run certain things in a transaction, $withinSameConnection (or whatever it would be called) would need to have knowledge about whether or not we're in a transaction. If we are, it would simply yield the connection we are already working with, of course.

For example:

await client.$transaction(async transaction => {
    // the following statement would simply yield the same connection that the transaction is in
    await transaction.$withinSameConnection([
        transaction.something(...),
        transaction.somethingElse(...),
    ]);
});

This might be obvious, but I just thought I'd share.

@apolanc apolanc self-assigned this Jul 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
domain/client Issue in the "Client" domain: Prisma Client, Prisma Studio etc. kind/feature A request for a new feature. topic: connection pool topic: database-provider/supabase topic: enterprise topic: rls
Projects
None yet
Development

No branches or pull requests