Giter Club home page Giter Club logo

trpc-shield's Introduction

Contributors Forks Stargazers Issues MIT License


Logo

tRPC Shield

tRPC permissions as another layer of abstraction!s!
Explore the docs ยป

Report Bug ยท Request Feature

Buy Me A Coffee

Table of Contents
  1. Overview
  2. Supported tRPC Versions
  3. Installation
  4. Usage
  5. Documentation
  6. Contributors
  7. Contributing
  8. Acknowledgments

Overview

tRPC Shield helps you create a permission layer for your application. Using an intuitive rule-API, you'll gain the power of the shield engine on every request. This way you can make sure no internal data will be exposed.

Supported tRPC Versions

tRPC 10

  • 0.2.0 and higher

tRPC 9

  • 0.1.2 and lower

Installation

Using npm

npm install trpc-shield

Using yarn

yarn add trpc-shield

Usage

  • Don't forget to star this repo ๐Ÿ˜‰
import * as trpc from '@trpc/server';
import { rule, shield, and, or, not } from 'trpc-shield';
import { Context } from '../../src/context';

// Rules

const isAuthenticated = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user !== null
})

const isAdmin = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user.role === 'admin'
})

const isEditor = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user.role === 'editor'
})

// Permissions

const permissions = shield<Context>({
  query: {
    frontPage: not(isAuthenticated),
    fruits: and(isAuthenticated, or(isAdmin, isEditor)),
    customers: and(isAuthenticated, isAdmin),
  },
  mutation: {
    addFruitToBasket: isAuthenticated,
  },
});

export const t = trpc.initTRPC.context<Context>().create();

export const permissionsMiddleware = t.middleware(permissions);

export const shieldedProcedure = t.procedure.use(permissionsMiddleware);

For a fully working example, go here.

Documentation

Namespaced routers

export const permissions = shield<Context>({
  user: {
    query: {
      aggregateUser: allow,
      findFirstUser: allow,
      findManyUser: isAuthenticated,
      findUniqueUser: allow,
      groupByUser: allow,
    },
    mutation: {
      createOneUser: isAuthenticated,
      deleteManyUser: allow,
      deleteOneUser: allow,
      updateManyUser: allow,
      updateOneUser: allow,
      upsertOneUser: allow,
    },
  },
});

API

shield(rules?, options?)

Generates tRPC Middleware layer from your rules.

rules

All rules must be created using the rule function.

Limitations
  • All rules must have a distinct name. Usually, you won't have to care about this as all names are by default automatically generated to prevent such problems. In case your function needs additional variables from other parts of the code and is defined as a function, you'll set a specific name to your rule to avoid name generation.
// Normal
const admin = rule<Context>()(async (ctx, type, path, input, rawInput) => true)

// With external data
const admin = (bool) => rule<Context>(`name-${bool}`)(async (ctx, type, path, input, rawInput) => bool)

options

Property Required Default Description
allowExternalErrors false false Toggle catching internal errors.
debug false false Toggle debug mode.
fallbackRule false allow The default rule for every "rule-undefined" field.
fallbackError false Error('Not Authorised!') Error Permission system fallbacks to.

By default shield ensures no internal data is exposed to client if it was not meant to be. Therefore, all thrown errors during execution resolve in Not Authorised! error message if not otherwise specified using error wrapper. This can be turned off by setting allowExternalErrors option to true.

Basic rules

allow, deny are tRPC Shield predefined rules.

allow and deny rules do exactly what their names describe.

Logic Rules

and, or, not, chain, race

and, or and not allow you to nest rules in logic operations.

and rule

and rule allows access only if all sub rules used return true.

chain rule

chain rule allows you to chain the rules, meaning that rules won't be executed all at once, but one by one until one fails or all pass.

The left-most rule is executed first.

or rule

or rule allows access if at least one sub rule returns true and no rule throws an error.

race rule

race rule allows you to chain the rules so that execution stops once one of them returns true.

not

not works as usual not in code works.

You may also add a custom error message as the second parameter not(rule, error).

import { shield, rule, and, or } from 'trpc-shield'

const isAdmin = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user.role === 'admin'
})

const isEditor = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user.role === 'editor'
})

const isOwner = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user.role === 'owner'
})

const permissions = shield<Context>({
  query: {
    users: or(isAdmin, isEditor),
  },
  mutation: {
    createBlogPost: or(isAdmin, and(isOwner, isEditor)),
  },
})

Global Fallback Error

tRPC Shield allows you to set a globally defined fallback error that is used instead of Not Authorised! default response. This might be particularly useful for localization. You can use string or even custom Error to define it.

const permissions = shield<Context>(
  {
    query: {
      items: allow,
    },
  },
  {
    fallbackError: 'To je napaka!', // meaning "This is a mistake" in Slovene.
  },
)

const permissions = shield<Context>(
  {
    query: {
      items: allow,
    },
  },
  {
    fallbackError: new CustomError('You are something special!'),
  },
)

Whitelisting vs Blacklisting

Shield allows you to lock-in access. This way, you can seamlessly develop and publish your work without worrying about exposing your data. To lock in your service simply set fallbackRule to deny like this;

const permissions = shield<Context>(
  {
    query: {
      users: allow,
    },
  },
  { fallbackRule: deny },
)

Contributors

This project exists thanks to all the people who contribute.

Contributing

We are always looking for people to help us grow trpc-shield! If you have an issue, feature request, or pull request, let us know!

Acknowledgments

A huge thank you goes to everybody who worked on Graphql Shield, as this project is based on it.

Also thanks goes to flaticon, for use of one of their icons in the logo: Shield icons created by Freepik - Flaticon

trpc-shield's People

Contributors

lottamus avatar necromant1k avatar omar-dulaimi avatar royletron avatar svengau avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

trpc-shield's Issues

Permissions of namespace routes collide if they have the same name

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

I group all my routes in namespaces and usually have the same name inside a namespace. If that's the case, the first route permissions are applied.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

Define the following permissions

const permissions = shield<Context>(
  {
    users: {
      mutation: {
        createOne: and(isAuthenticated, isAdmin)
      }
    },
    posts: {
      mutation: {
        createOne: isAuthenticated,
      }
    }
  }
);

The isAdmin guard is called when making a request to posts.createOne

Expected behavior

isAdmin is not executed and the proper guards are executed.

behavior

isAdmin is executed when calling posts.createOne

Context Extension not correctly typed

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

trpc middleware allows extending the context, in particular Context Extension.

See https://trpc.io/docs/server/middlewares#context-extension

To Reproduce

To reproduce, simply;

  1. add the isAuthenticated rule to the official example,
  2. and use it on one query (example findManyUser).
  3. In findManyUser, check ctx.user. It's still null.

Expected behavior

In findManyUser, ctx.user should be typed as a string

ps: Thanks for this awesome job, I come from the graphql world, and really appreciated the graphql-shield.

Unexpected behavior with prisma-trpc-generator

I'd like to verify this is a bug and not an issue with my configurations before further investigation.

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

I'm using this in combination with prisma-trpc-generator (Thank you @omar-dulaimi for building these!). I am also using SuperJSON transformer end-to-end.

I started using my own permissions, instead of the ones generated with all allow defaults.

I am working with the path account.findUnique.

To keep things short and concise, this pattern does not work (results in fallback denial):

export const permissions = shield<Context>(
  {
    query: {
      findUniqueAccount: allow,
      ...
    },
  },
  {
    fallbackRule: deny,
  },
);

but this does work:

export const permissions = shield<Context>(
  {
    query: {
      findUnique: allow,
      ...
    },
  },
  {
    fallbackRule: deny,
  },
);

This ties back to the issue raised here: #1 (comment) and the example permission (https://github.com/omar-dulaimi/trpc-shield/blob/master/example/prisma/shield/shield.ts) also follows the earlier pattern, not the latter which is working.

To Reproduce

Things are set up pretty traditionally. Shield is working, but the nested router mapping does not.

export const appRouter = t.router({
  account: accountsRouter,
});

Additional context

Although it's not working as intended, I've observed a singular namespace will work as intended.

This works:

export const permissions = shield<Context>(
  {
    account: {
      query: {
        findMany: allow,
      },
    },
  },
  {
    fallbackRule: deny,
  },
);

This does not work:

export const permissions = shield<Context>(
  {
    account: {
      query: {
        findMany: allow,
      },
    },
    player: {
      query: {
        findUnique: allow,
      },
    },
  },
  {
    fallbackRule: deny,
  },
);

Hoping for some clarity on the correct working patterns.

Rule function return undefined value for the input param.

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

Rule function return undefined for the input params. I'm using version "^0.4.0".

const isRuleName = rule<Context>()(
  async (ctx, type, path, input, rawInput, options) => {
     console.log(input) // undefined
     return true
  }
);

What I have

const permissions = shield<Context>({
  project: {
    mutation: {
      delete: isRuleName,
    },
  }
});

export const permissionsMiddleware = middleware(permissions);
export const protectedProcedure = t.procedure.use(permissionsMiddleware);

const appRouter = router({
project: router({
    delete: protectedProcedure
      .input(z.object({ id: z.string(), owner: z.string() }))
      .mutation(async ({ ctx, input }) => {
        return await prisma.project.delete({
          where: {
            id: input.id,
          },
        });
      }),
   })

Additional context

RawInput returns the content of the input.

Typescript Support?

Feature request

Is your feature request related to a problem? Please describe

Typescript support for shield method

Describe the solution you'd like

Typed records inside of the shield based on AppRouter

Describe alternatives you've considered

No typescript support

fallback rule not working on nested routers

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

The fallbackRule option is not working on nested routers.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

const trpcShield = shield<Context>(
  {
    user: {
      query: {
        getCurrentUser: isAdmin,
        users: isAdmin,
        user: or(isAdmin, userOwnsUser)
      },
      mutation: {
        updateUser: or(isAdmin, userIsArg(['user', 'id'])),
        createUser: isAdmin
      }
    },
   { fallbackRule: deny }
 }
)

Expected behavior

undefined routes should be denied

behavior

undefined routes are allowed

Additional context

Add any other context about the problem here.

[Resolved] trpc-shield not working on merged procedures because query name was incorrectly set in permissions object.

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

trpc-shield doesn't seem to work in the context of a monorepo. Maybe I'm just doing something wrong?

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

I have created a monorepo so you can observe the behaviour. Available at:
https://github.com/gablabelle/bug-reproduction-monorepo-trpc-shield

1- git clone repo
2- yarn install
3- yarn dev
4- Context is located at apps/my-t3-app/src/server/router/context.ts

Expected behavior

Should deny the example.hello call
Screen Shot 2022-07-17 at 12 04 32

but it doesn't

Screen Shot 2022-07-17 at 12 05 33

Fallback rule not working on nested routers

Bug report

  • I have checked other issues to make sure this is not a duplicate.

Describe the bug

The fallbackRule option is not working on nested routers.

To Reproduce

Steps to reproduce the behavior, please provide code snippets or a repository:

const trpcShield = shield<Context>(
  {
    user: {
      query: {
        getCurrentUser: isAdmin,
        users: isAdmin,
        user: or(isAdmin, userOwnsUser)
      },
      mutation: {
        updateUser: or(isAdmin, userIsArg(['user', 'id'])),
        createUser: isAdmin
      }
    },
   { fallbackRule: deny } // This does not apply
 }
)

Expected behavior

Routes that do not have any permissions set, should fallback to the fallbackRule in a namespaced router setup.

behavior

fallbackRule does not apply and all routes are unprotected

Additional context

This issue was first raised in #23. The issue has been closed without any context provided as to if the bug was resolved or if the author went with another approach.

A fix for this bug was already pushed in #22 a while ago. It would be great to receive a review on this to merge and publish this fix soon. If the orignal PR author does not respond, I also do not mind opening a new PR!

Fixing this is critical to our project, as it makes using trpc-shield with namespaced routes super unsafe. If you forget to properly add a route to the shield config or have a small spelling mistake, it can compromise your entire application.

Unclear how to access "parent"

In the examples on the home page you have the following as a rule:

const isOwner = rule<Context>()(async (ctx, type, path, input, rawInput) => {
  return ctx.user.items.some((id) => id === parent.id)
})

Where is parent coming from? How to use trpc-shield to define a rule and access the object or parent in question is unclear.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.