Coder Social home page Coder Social logo

avallone-io / rls Goto Github PK

View Code? Open in Web Editor NEW
61.0 3.0 12.0 1.05 MB

Row level security (RLS) package for TypeORM and NestJS

License: ISC License

JavaScript 1.25% TypeScript 98.71% Dockerfile 0.04%
rls rowlevelsecurity typeorm typeorm-rls nestjs typescript

rls's Introduction

Build And Test .github/workflows/release.yml

Description

Row level security utilitary package to apply to NestJS and TypeORM.

This solution does not work by having multiple connections to database (eg: one connection / tenant). Instead, this solution works by applying the database policies for RLS as described in this aws blog post (under the Alternative approach).

Install

$ npm install @avallone-io/rls

Usage

To create a RLSConnection instance you'll need the original connection to db. Setup the typeorm config as usual, then wrap its connection into a RLSConnection instance, for each request.

This will run a set "rls.tenant_id" and set "rls.actor_id" for each request and will reset them after the query is executed.


RLS Policies

Your database policies will have to make use of rls.tenant_id and rls.actor_id in order to apply the isolation. Policy example:

CREATE POLICY tenant_isolation ON public."category" for ALL
USING ("tenant_id" = current_setting('rls.tenant_id'))
with check ("tenant_id" = current_setting('rls.tenant_id'));

Express/KOA

For example, assuming an express application:

app.use((req, res, next) => {
  const dataSource = await new DataSource({...}).initialize(); // create a datasource and initialize it

  // get tenantId and actorId from somewhere (headers/token etc)
  const rlsConnection = new RLSConnection(dataSource, {
    actorId,
    tenantId,
  });

  res.locals.connection = rlsConnection;
  next();
});

// your handlers
const userRepo = res.locals.connection.getRepository(User);
await userRepo.find(); // will return only the results where the db rls policy applies

In the above example, you'll have to work with the supplied connection. Calling TypeORM function directly will work with the original DataSource object which is not RLS aware.

NestJS integration

If you are using NestJS, this library provides helpers for making your connections and queries tenant aware.

Create your TypeORM config and load the TypeORM module using .forRoot. Then you'll need to load the RLSModule with .forRoot where you'll define where to take the tenantId and actorId from. The second part is that you now need to replace the TypeOrmModule.forFeature with RLSModule.forFeature. This should be a 1-to-1 replacement. You can inject non-entity dependent Modules and Providers. First array imports modules, second array injects providers.

When using RLSModule.forRoot it will set your scope to REQUEST! Be sure you understand the implications of this and especially read about the request-scoped authentication strategy on Nestjs docs.

The RLSModule.forRoot accepts the factory funtion as async or non-async function.

app.controller.ts

@Module({
  imports: [
    TypeOrmModule.forRoot(...),
    RLSModule.forRoot([/*Module*/], [/*Service*/], async (req: Request, /*serviceInstance*/) => {
      // You can take the tenantId and actorId from headers/tokens etc
      const tenantId = req.headers['tenant_id'];
      const actorId = req.headers['actor_id'];

      return {
        actorId,
        tenantId,
      };
    }),
    RLSModule.forFeature([Post, Category]) // <- this
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Now you can use the normal module injection for repositories, services etc.

To inject the RLS connection within a service, you can do by using @Inject(TENANT_CONNECTION) where TENANT_CONNECTION is imported from @avallone-io/rls.

export class AppService {
  constructor(
    @InjectRepository(Category)
    private categoryRepo: Repository<Category>,
    @Inject(TENANT_CONNECTION)
    private connection: RLSConnection,
  ) {}

  // you can now use categoryRepo as normal but it will
  // be scoped for RLS. Same with the connection.
}

Same as before, do not use the TypeORM functions directly from the dataSource as that will give you the default connection to the database, not the wrapped instance.

For more specific examples, check the test/nestjs/src.

Typeorm >v0.3.0

Since typeorm v0.3.0, the Connection class has been replaced by DataSource. This module still uses Connection as its language which is also helpful now to differenciate between the actual database connection (typeorm DataSource) and RLS wrapper (RLSConnection). However, if you want to be on par with typeorm terminalogy, there is an alias for RLSConnection called RLSDataSource.

rls's People

Contributors

ahmadnazir avatar dependabot[bot] avatar semantic-release-bot avatar stelescuraul avatar tylerhatkinson 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

Watchers

 avatar  avatar  avatar

rls's Issues

Support for @nestjs/typeorm 8.1

Hi!

Thank you for the lib!

Looks there was a breaking change in v8.1.0 of @nestjs/typeorm - they have dropped helpers folder. You can see it here.

rls.module depends on one of helpers. Looks like helpers were not considered as part of public API of the lib, since the change was introduced in minor version. v8.0.3 still works fine.

So this issue is about:

  1. Support new version of @nestjs/typeorm
  2. Think if we really need to depend on private API of the module.

Handling race conditions

Description

There seems to be a race condition in the RLS package stemming from non-atomic parameter operations when executing concurrent queries. This issue can lead to incorrect parameter states and unpredictable query results.

Issue Detail

The core of the problem lies in the RLS package’s approach to managing parameters: it sets and resets them before and after each query. In concurrent scenarios, such as the following pseudo-code, this behavior causes parameters to be reset prematurely:

const entities = [ /* array of entities */ ];

const promises = entities.map(async e => runQuery(e, entityManager));
await Promise.all(promises);

Here, the non-atomic nature of setting and resetting parameters leads to a situation where, by the time one query starts using the parameters, another might have already reset them, as depicted in this sequence diagram:

image

Proposed Solution

A practical solution is to allow manual control over the parameter lifecycle, enabling developers to set and reset parameters explicitly, avoiding race conditions:

// Manually set parameters once
setParams(params, entityManager);

const promises = entities.map(async e => runQueryWithoutAutoParams(e, entityManager));
await Promise.all(promises);

// Manually reset parameters once
resetParams(params, entityManager);

I would like to hear your thoughts before we start working on it.

Is there a way to set the tenant After the request has gone through the controller?

Hello,

We are using nestjs and just added the rls package.
Everything works fine but we have a case in which the tenant_id is only know after we hit the request and do some processing.
Is it possible to set it afterwards?

We tried setting it on the connection from the RLS Connection injection but that did not affect the repository.

Thank you!

What is the purpose of actor_id?

Nice library! This is exactly what I'm looking for.

But I can think what the actor_id is used for. Should this be used somewhere in the policies, too? As I know you were inspired by the AWS Blog Post (https://aws.amazon.com/de/blogs/database/multi-tenant-data-isolation-with-postgresql-row-level-security/). I can't find anything about actors in this blog post.

  • What is the purpose of actor id? Is this something like the user id?
  • Is it ok to set the actor id to an empty string or even to null?

Escaping Request.SCOPE with CLS (AsyncLocalStorage)

Hey! Thanks so much for this library, it is so useful and I wonder why it is not more widely used, due to the importance of the problem it solves.

One obvious thing where it could become even better is escaping the request-scoped-ness altogether. Which seems to be pretty much possible with things like CLS (continuation-local storage), which leverages NodeJS Async Local Storage.

I think it would be quite beneficial in general to add support for integration with CLS libs like nestjs-cls, which would allow less-constrained usage for everyone.

It seems like integration should be quite straightforward, I forked this library and tried to give it a shot following nest-js cls docs by simply swapping the request object with CLS_REQ thus lifting the Request.SCOPE constraint.

Although it looks like it should be enough, for some reason I can't get TENANT_CONNECTION to be injected in derived features like repositories. To be honest I am quite new to Nest.js and I am not yet that comfortable with Injection Scopes, Dynamic Modules & etc. So probably it is something rather straightforward, just my inexperience is blocking things here.

Would love to hear your thoughts on this!

Add support for custom current_settings values

Hey guys, great package!

Suggestion: would you consider adding other "current_settings" parameters that can be set by the application dynamically? Eg:

{
    // Supported already
    actorId,
    tenantId,
    
    // Optional: Custom values set at runtime
    actorRole: "admin",
    otherCustomKey: "customValue"
  }

So that RLS policies can leverage this extra param?

Thanks!

Any SQL errors in a transaction are masked

Any errors that happen during a transaction, are masked by the current transaction is aborted, commands ignored until end of transaction block error, due to the call made to await super.query('reset rls.actor_id; reset rls.tenant_id;');despite there being an error and the call being transactional.

I believe the solution to this issue would be to check if this.isTransactionActive is true and if there is an error, do not attempt to run the reset rls.actor_id; reset rls.tenant_id; query.

Question: How does this mechanism with concurrent requests? Does it work within transactions? Errors?

I have a few questions about the mechanism this library uses to actually set / unset the values used for the RLS policy checks. I am neither a Postgres or a RLS expert, so forgive me if these questions are overly simplistic or out of place!

  1. Can you tell me anything about the mechanism the library uses to prevent the tenant id / author id "bleeding" into subsequent requests? In a situation where connection pooling is happening and a connection might be shared by different tenants, is there an opportunity for bleed over?
  2. Does this library work both in and out of a transaction? If I use the manager to create a new transaction, and make several queries inside of it, will the tenant id / author id be set appropriately for all of those queries within the transaction?
  3. I see that you both set and unset the tenant / author ids. Say I write a very bad, invalid query, is there a chance that the query will fail in such a way that the tenant / author id would not be appropriate "unset" and so would have a potential of bleeding into subsequent requests with different tenants re-using the connection?

Again, these questions may all be super short sighted / simplistic or just irrelevant (because of the mechanism being used), sorry in advance if that's the case but thanks for any assurances :)

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.