Feathers: Multi tenant app

Created on 11 Jan 2018  路  7Comments  路  Source: feathersjs/feathers

Hello.

I am just starting with Feathers. We are going to create a multi tenant app, so we are thinking that in every request user has to pass the company ID.
This company ID will separate data in the database.

I read a similar issue https://github.com/feathersjs/authentication-local/issues/14 but could not understand what is the recoomended way to do it.

What is the correct way to accomplish this in Feathers? Would be nice that the company parameter are pre set in the service, so, we dont have to write that paramter in every service

Thank you!

Most helpful comment

In Feathers 4 and later this can be done in an easier way via:

app.service('myservice').hooks({
  before: async context => {
    const { companyId } = context.params.user;

    // limit query to users company id
    context.params.query.companyId = companyId;
  }
});

All 7 comments

There are different ways to approach this the most common being to limit the access to the authenticated users company in hooks. When requesting multiple entries, limit the query to context.params.user.companyId, when requesting a single one, check first if the companyId matches the user's company id:

app.service('myservice').hooks({
  before: async context => {
    const { companyId } = context.params.user;

    if(!context.id) { // When requesting multiple entries
      // limit query to users company id
      context.params.query.companyId = companyId;
    } else if(context.params.caller !== context.path) {
      // Get the individual entry first and check if access is allowed
      const result = await context.service.get(context.id, Object.assign(context.params, {
        caller: context.path
      }));

      if(result.companyId !== companyId) {
        throw new Error('You are not allowed to access this');
      }
    }
  }
});

@daffl Why wouldn't you just use context.params.query.companyId = companyId for both cases? Seems like it would be better to limit the results returned from the data source rather than filtering them out in the application logic.

There is no filtering in the application logic. In the case of single entries queried by their id the adapters do not use the query so you'll have to check first if a user is even allowed to access it.

the code is going in the infinite loop because of context.service.get in get service
basically i want user to retrieve it's own resource

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue with a link to this issue for related bugs.

In Feathers 4 and later this can be done in an easier way via:

app.service('myservice').hooks({
  before: async context => {
    const { companyId } = context.params.user;

    // limit query to users company id
    context.params.query.companyId = companyId;
  }
});

Hi, in our case we have the following structure:

  1. One user can be attached to multiple accounts
  2. In the user object we store all accounts in a simple StringArray
  3. When the user request a resource from the client, he need to send the accountId (which is actually our tenantId) inside the request url parameter (they can also sent in the request headers if they want)
  4. We created restrictToAccount hook which check if the user has access to this account. Because we store all account ids under the user object its a simple check that only check if the accountId that the user try to access contained in the user account ids list. This approach also secure all our requests (think about the case that the user find some accountId of another tenant and he try to "hack" by sending this account id inside the request so the restrictToAccount is mandatory step. In this case the server will return 403 (Forbidden) and not an empty array)
  5. Because we store in each and every table/collection the accountId (this is how multi-tenant works) then we can simply go with @daffl approach and do:

context.params.query.accountId = accountId

we need to set the accountId only if the user sent it in the request headers. If the user sent it as a url parameter we can just ignore it and let feathers framework to do its magic.

to summarize it:

  1. I think that account or company under the user object needs to be an array. this will make your services more flexible and will allow you to introduce a concept of Organization & Accounts where one Organization can have multiple Accounts that each one of them is a tenant

  2. I think we should use the restirctToAccount approach and return 403 if the user try to access to some account that he/she not allowed to access to. In this way you can also use some kind of analytics that will help you to understand if someone tries to "hack" to your services

  3. In case you use multi-tenant approach (and not single-tenant) you need to store the accountId in each and every record in your backend (of course that this is true only to resources which private to specific account. Sometimes you have shared resources which not require it)

@daffl what do you think about this approach?
Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Vincz picture Vincz  路  4Comments

eric-burel picture eric-burel  路  3Comments

NetOperatorWibby picture NetOperatorWibby  路  4Comments

corymsmith picture corymsmith  路  4Comments

rrubio picture rrubio  路  4Comments