npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@joindeed/prisma-auth

v1.2.9

Published

Declarative Prisma Authorization layer

Downloads

273

Readme

@joindeed/prisma-auth

What?: A declarative authorisation middleware that operates on Prisma model level (and not on GraphQL resolver level).

Why?: Because imperatively crafting authorization rules is dangerous, especially in GraphQL+Prisma world, where we tend to automatically expose the whole schema (with tools like Pal.js, nexus-prisma).

See this thread for tweet-sized introduction:

Theory

Every modern GraphQL platform (like Hasura or MongoDB Realm) offer resource authorisation at the schema level. Prisma however lacks such luxury, being merely an ORM. But if we are building a GraphQL API on top of Prisma with a tool that automatically exposes all relations (like nexus-prisma, typegraphql-prisma or Pal.js), we're left to handle resource authorisation on our own.

And suddenly securing all relations inside a resolver becomes a huge P.I.T.A.: are you sure that Purchase.User.Organization.Users suddenly won't expose all users on the platform? If you patch it at resolver level it's quite easy to overlook a huge security hole!

Instead, we believe authorisation should be handled at the Prisma model level. For each model type, we can define a set of roles that are allowed to access it, together with its fields.

We support two kinds of authorisation definitions:

1. Model level

/// @Auth(read: [Owner( privileges: [x,y,z], smth: else ), Admin])
model Purchases {...}

2. Individual field level

model User {
  /// @Auth(read:[ Owner, Admin ])
  email String
}

Note how roles may accept arbitrary arguments that would be passed to the role matcher (see below).

Usage

  1. yarn add @joindeed/prisma-auth

  2. Define @Auth annotations as Prisma comments (see above, note triple slash)

  3. You can define global role matchers that would apply to all Prisma models, and you may override them per each model:

const config = {
  globalRoles: {
    Owner: {
      matcherDependenciesSelect: (roleArgs) => ({ [roleArgs.userField]: true }),
      matcher: (ctx, record, roleArgs) => ctx.currentUser?.id === record?.[roleArgs.userField],
      queryConstraint: (ctx, roleArgs) => ({
        [roleArgs.userField]: ctx.currentUser?.id,
      }),
    },
  },
  rolesPerType: {
    Purchases: {
      Owner: {
        matcherDependenciesSelect: (roleArgs) => ({ id: true, [roleArgs.userField]: true }),
        matcher: (ctx, record, roleArgs) => someCondition(ctx) && ctx.currentUser?.id === record?.[roleArgs.userField],
        queryConstraint: (ctx, roleArgs) =>
          someCondition(ctx) && {
            [roleArgs.userField]: ctx.currentUser?.id,
          },
      },
    },
  },
}

matcher is used to restrict access to individual records. It should return boolean. queryConstraint is used to generate a where clause for Prisma which should be used to restrict list fields and list relations. roleArgs is used to declare the data requirements needed for the above validators to work (i.e. everything you want to be inside record.* must be listed there). It can be either an object in Prisma's select argument format or a function that takes role arguments and returns that object.

  1. Apply context.withAuth to every Prisma call like this:
resolve: async (parent, args, context) => {
  return context.prisma.purchases.findMany(context.withAuth({
    where: {
      some: 'query'
    }
  }))
}

If you return a composite non-Prisma type from the resolver, you'd need to manually specify the path within the return type and the correct enity type:

resolve: async (parent, args, context) => {
  const users = context.prisma.users.findMany(context.withAuth({
    where: {
      some: 'query'
    }
  }, 'path.toUser', 'UserType'))
  return users.map(user => ({
    path: {
      toUser: users
    }
  }))
}

Additionally, if you want to make any arbitrary Prisma query not related to the GraphQL return type at all (e.g. say you want to count something and get the count with respect to the constraints), and want to get the where constraints for it, you may leave the path argument empty and only provide the type:

resolve: async (parent, args, context) => {
  return context.prisma.users.count(context.withAuth({
    where: {
      some: 'query'
    }
  }, undefined, 'UserType'))
}

NOTE: in this case withAuth would only generate the where constraint, but not a select clause.

If your resolver requires some data to be available on the parent, you should specify it in the config passed to makeAuthorizationMiddlewares:

makeAuthorizationMiddlewares({
  defaultFields: {
    // when `usersCount` field is selected, `users` will be selected too
    Company: (select) => ('usersCount' in select ? { users: true } : {}),
  }
})
  1. Configure your GraphQL schema to use the middleware
import { applyMiddleware } from 'graphql-middleware'
import { makeAuthorizationMiddlewares } from '@joindeed/prisma-auth'
const server = new ApolloServer({
  schema: applyMiddleware(schema, ...makeAuthorizationMiddlewares(config)),
  ...
})

Get in touch if you have ideas how all of this could have been done better!

Plans

Currently, this package only supports read operations, since it's the biggest concern in terms of GraphQL security. update/create/delete are easy to secure with a custom input type.

Nevertheless, we might want to support them in the future.

Release

  • Manually edit version number in package.json (duh).
  • Create a new release on Github, create a tag matching the new version number
  • The actual publish would happen automatically (observe the GH actions that it actually does)

Credit

This package has been proudly developed by the Deed team!

Check us out, we may be hiring!

https://www.joindeed.com/

The basis for the select plugin has been forked from prisma-tools. Thanks, Ahmed Elywa!