MichalLytek / typegraphql-prisma

Prisma generator to emit TypeGraphQL types and CRUD resolvers from your Prisma schema
https://prisma.typegraphql.com
MIT License
891 stars 113 forks source link

Prisma middlewares not applied on relation resolvers? #342

Closed srosato closed 2 years ago

srosato commented 2 years ago

Describe the Bug I seem to be having trouble using prisma-field-encryption middleware on nested resolvers. Nested fields do not get decrypted. Only top level ones.

To Reproduce Add prisma-field-encryption middleware. Create quick schema with @encrypted annotation (see my code below)

Expected Behavior I expect nested fields to be decrypted (therefore passing through the middleware correctly)

Environment (please complete the following information):

Additional Context

I have this prisma client (as singleton):

// prisma.ts
export const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
});

prisma.$use(fieldEncryptionMiddleware());

Passed as context to this graphql-yoga server:

import { prisma } from './prisma';
import { createSchema } from './schema';

export async function graphqlServer(req: NextApiRequest, res: NextApiResponse) {
  restrictAdminApiToAuthorizedUsers();

  const user = await authorizeUser(req, res, { prisma });

  if (!user) {
    return;
  }

  if (!server || isTest()) {
    server = createServer<ServerContext, AuthContext>({
      schema: await createSchema(),
      maskedErrors: {
        isDev: isDev() || isTest(),
      },
      context: { prisma, user, options: { bypassAuth: shouldBypassAuth(req) } },
    });
  }

  await server.handle(req, res);
}

I have this schema (simplified for this submission purpose)

model User {
  id String @id @default(uuid())
  pin String @unique /// @encrypted
  pinHash String? @unique /// @encryption:hash(pin)
}

I have this query:

query getUser($userId: String!) {
  user(where: { emailAuthId: $userId }) {
    id
    pin
    jobPositions {
      user {
        id
        pin
      }
    }
  }
}

I get this response:

{
  "data": {
    "user": {
      "id": "eid-1",
      "pin": "9999",
      "jobPositions": [
        {
          "user": {
            "id": "eid-1",
            "pin": "v1.aesgcm256.f1b66677.rgpgqzVgqOgqD2Ij.8OOcNuS6llne2wzL-fISNsRHka4="
          }
        }
      ],
    }
  }
}

Notice the nested field not being properly decrypted, but the top level is. Not sure what I'm doing wrong here. I tried using prisma directly:

import { prisma } from './prisma';

const main = async () => {
   const user = await prisma.user.findFirst({
    select: {
      id: true,
      pin: true,
      jobPositions: {
        include: {
          user: true
        }
      }
    },
    where: {
      id: 'eid-1'
    }
  });

  console.log(user?.id, user?.pin, user?.jobPositions[0].user.pin); // correctly outputs [eid-1, 9999, 9999]
};

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

And was able to see the nested field being decrypted successfully

MichalLytek commented 2 years ago

Can you try subsequent prisma client calls instead of nested include? That's what typgraphql-prisma resolvers are doing.

srosato commented 2 years ago

Do you mean something like this?

import { prisma } from './prisma'

const main = async () => {
  const user = await prisma.user.findFirst({
    where: {
      id: 'eid-1'
    }
  });

  console.log(user?.id, user?.pin); //outputs pin 9999

  const jobPosition = await prisma.jobPosition.findFirst({
    select: {
      user: true
    },
    where: {
      user: {
        id: {
          equals: 'eid-1'
        }
      }
    }
  })

  console.log(jobPosition?.user.pin);  //does NOT output pin 9999 (outputs encrypted version)
};

main().catch((e) => {
  console.error(e);
  process.exit(1);
});

Then in this case if I use select to get the user, it is not encrypted. Probably has nothing to do with typegraphql then :thinking:

MichalLytek commented 2 years ago

Yes 😉 So you need to adjust your middleware or report bug in Prisma repo

srosato commented 2 years ago

For reference, I created a PR on prisma-field-encryption, as I was able to reproduce using the repo itself.

https://github.com/47ng/prisma-field-encryption/pull/33

srosato commented 2 years ago

@MichalLytek Sry to bother again. Can you give me some insights as to how graphql queries are being translated behind when using relations?

Following my merged PR on prisma-field-encryption, I thought I would fix my issue by making sure the select portion of the query would be taken into consideration. And with raw prisma, I can now do:

const jobPosition = await prisma.jobPosition.findFirst({
  select: {
    user: {
      select: {
        pin: true,
      }
    }
  },
  where: {
    user: {
      id: {
        equals: 'eid-1'
      }
    }
  }
})

console.log(jobPosition?.user.pin); //correctly outputs uncrypted pin

but if I do:

query {
  jobPositions {
    user {
      pin // still unencrypted
    }
  }
}

Any insights would help. I tried navigating the code but I am not sure to what my graphql queries essentially translate to behind so I can properly fix the issue in prisma-field-encryption. Thanks!

srosato commented 2 years ago

I found a workaround for anybody stumbling in the same issue of unencrypting a nested value with prisma-field-encryption. Probably not ideal, but here it is:

@Resolver((of) => User)
export class UserResolver {
  /*
   * This is a workaround, as I am not sure how typegraphql-prisma handles queries. Nested resolvers that
   * need the pin found themselves with an encrypted value instead of an unencrypted one. This works around that,
   * although not performant.
   */
  @FieldResolver((type) => String)
  async pin(@Root() user: User, @Ctx() {prisma}: Context): Promise<string> {
    const foundUser = await prisma.user.findUnique({ where: { id: user.id } });

    return foundUser?.pin || user.pin
  }

Instead of relying on nested queries, I make sure to do another one no matter where I request for the encrypted field in my graphql queries.