Skip to content

Commit

Permalink
feat(example-multi-tenancy): add multi-tenancy action and strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 10, 2020
1 parent e486b75 commit 0c99edb
Show file tree
Hide file tree
Showing 29 changed files with 1,013 additions and 3 deletions.
188 changes: 187 additions & 1 deletion examples/multi-tenancy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,190 @@
An example application to demonstrate how to implement multi-tenancy with
LoopBack 4.

[![LoopBack](<https://github.com/strongloop/loopback-next/raw/master/docs/site/imgs/branding/Powered-by-LoopBack-Badge-(blue)[email protected]>)](http://loopback.io/)
## Key artifacts

### MultiTenancyStrategy

This interface defines the contract for multi-tenancy strategies to implement
the logic to identify a tenant and bind tenant specific resources to the request
context.

```ts
/**
* Interface for a multi-tenancy strategy to implement
*/
export interface MultiTenancyStrategy {
/**
* Name of the strategy
*/
name: string;
/**
* Identify the tenant for a given http request
* @param requestContext - Http request
*/
identifyTenant(
requestContext: RequestContext,
): ValueOrPromise<Tenant | undefined>;

/**
* Bind tenant-specific resources for downstream artifacts with dependency
* injection
* @param requestContext - Request context
*/
bindResources(
requestContext: RequestContext,
tenant: Tenant,
): ValueOrPromise<void>;
}
```

### MultiTenancyActionProvider

`MultiTenancyActionProvider` serves two purposes:

- Provides an action (`MultiTenancyAction`) for the REST sequence to enforce
multi-tenancy
- Exposes an extension point to plug in multi-tenancy strategies

### Implement MultiTenancyStrategy

The example includes a few simple implementations of `MultiTenancyStrategy`:

#### Identify tenant id for a given http request

- JWTStrategy - use JWT token from `Authorization` header
- HeaderStrategy - use `x-tenant-id` header
- QueryStrategy - use `tenant-id` query parameter
- HostStrategy - use `host` header

#### Bind tenant specific resources to the request context

We simply rebind `datasources.db` to a tenant specific datasource to select the
right datasource for `UserRepository`.

```ts
bindResources(
requestContext: RequestContext,
tenant: Tenant,
): ValueOrPromise<void> {
requestContext
.bind('datasources.db')
.toAlias(`datasources.db.${tenant.id}`);
}
```
### Register multi-tenancy strategies
Multi-tenancy strategies are registered to the extension point using
`extensionFor` template:
```ts
app.add(
createBindingFromClass(JWTStrategy).apply(
extensionFor(MULTI_TENANCY_STRATEGIES),
),
);
```
We group multiple registrations in `src/multi-tenancy/component.ts` using the
`MultiTenancyComponent`:
```ts
export class MultiTenancyComponent implements Component {
bindings = [
createBindingFromClass(MultiTenancyActionProvider),
...MultiTenancyComponent.createStrategyBindings(),
];

static createStrategyBindings() {
return [
createBindingFromClass(JWTStrategy).apply(
extensionFor(MULTI_TENANCY_STRATEGIES),
),
createBindingFromClass(HeaderStrategy).apply(
extensionFor(MULTI_TENANCY_STRATEGIES),
),
createBindingFromClass(QueryStrategy).apply(
extensionFor(MULTI_TENANCY_STRATEGIES),
),
createBindingFromClass(HostStrategy).apply(
extensionFor(MULTI_TENANCY_STRATEGIES),
),
];
}
}
```
### Configure what strategies to be used
The `MultiTenancyBindings.STRATEGIES` binding configures what strategies are
checked in order.
```ts
app.bind(MultiTenancyBindings.STRATEGIES).to(['jwt', 'header']);
```
### Register MultiTenancyAction
`MultiTenancyAction` is added to `src/sequence.ts` so that REST requests will be
intercepted to enforce multiple tenancy before other actions.
```ts
export class MySequence implements SequenceHandler {
constructor(
// ...
@inject(MultiTenancyBindings.ACTION)
public multiTenancy: MultiTenancyAction,
) {}

async handle(context: RequestContext) {
try {
const {request, response} = context;
await this.multiTenancy(context);
// ...
} catch (err) {
this.reject(context, err);
}
}
}
```

## Use

```sh
npm start
```

The strategies expect clients to set tenant id for REST API requests.

- `jwt`: set `Authorization` header as
`Authorization: Bearer <signed-jwt-token>`
- `header`: set `x-tenant-id` header as `x-tenant-id: <tenant-id>`
- `query`: set `tenant-id` query parameter, such as: `?tenant-id=<tenant-id>`

Check out acceptance tests to understand how to pass tenant id using different
strategies:

- src/tests/acceptance/user.controller.header.acceptance.ts
- src/tests/acceptance/user.controller.jwt.acceptance.ts

You can use environment variable `DEBUG=loopback:multi-tenancy:*` to print out
information about the multi-tenancy actions.

## Contributions

- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md)
- [Join the team](https://github.com/strongloop/loopback-next/issues/110)

## Tests

Run `npm test` from the root folder.

## Contributors

See
[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors).

## License

MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/example-multi-tenancy
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Client, expect} from '@loopback/testlab';
import {ExampleMultiTenancyApplication} from '../..';
import {MultiTenancyBindings} from '../../multi-tenancy';
import {setupApplication} from './test-helper';

describe('UserController with header-based multi-tenancy', () => {
let app: ExampleMultiTenancyApplication;
let client: Client;

before('setupApplication', async () => {
({app, client} = await setupApplication());
app.bind(MultiTenancyBindings.STRATEGIES).to(['jwt', 'header', 'query']);
});

before('create users', async () => {
// Tenant abc
await client.post('/users').set('x-tenant-id', 'abc').send({name: 'John'});
// Tenant xyz
await client.post('/users').set('x-tenant-id', 'xyz').send({name: 'Mary'});
// No tenant
await client.post('/users').send({tenantId: '', name: 'Jane'});
});

after(async () => {
await app.stop();
});

it('Get users by tenantId - abc', async () => {
const res = await client
.get('/users')
.set('x-tenant-id', 'abc')
.expect(200);
expect(res.body).to.eql([{tenantId: 'abc', id: '1', name: 'John'}]);
});

it('Get users by tenantId - xyz', async () => {
const res = await client
.get('/users')
.set('x-tenant-id', 'xyz')
.expect(200);
expect(res.body).to.eql([{tenantId: 'xyz', id: '1', name: 'Mary'}]);
});

it('Get users by tenantId - none', async () => {
const res = await client.get('/users').expect(200);
expect(res.body).to.eql([{tenantId: '', id: '1', name: 'Jane'}]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright IBM Corp. 2020. All Rights Reserved.
// Node module: @loopback/example-multi-tenancy
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Client, expect, supertest} from '@loopback/testlab';
import {sign} from 'jsonwebtoken';
import {ExampleMultiTenancyApplication} from '../..';
import {MultiTenancyBindings} from '../../multi-tenancy';
import {setupApplication} from './test-helper';

describe('UserController with jwt-based multi-tenancy', () => {
let app: ExampleMultiTenancyApplication;
let client: Client;

before('setupApplication', async () => {
({app, client} = await setupApplication());
app.bind(MultiTenancyBindings.STRATEGIES).to(['jwt', 'header', 'query']);
});

before('create users', async () => {
// Tenant abc
await addJWT(client.post('/users'), 'abc').send({name: 'John'}).expect(200);
// Tenant xyz
await addJWT(client.post('/users'), 'xyz').send({name: 'Mary'}).expect(200);
// No tenant
await client.post('/users').send({name: 'Jane'});
});

after(async () => {
await app.stop();
});

it('Get users by tenantId - abc', async () => {
const res = await addJWT(client.get('/users'), 'abc').expect(200);
expect(res.body).to.eql([{tenantId: 'abc', id: '1', name: 'John'}]);
});

it('Get users by tenantId - xyz', async () => {
const res = await addJWT(client.get('/users'), 'xyz').expect(200);
expect(res.body).to.eql([{tenantId: 'xyz', id: '1', name: 'Mary'}]);
});

it('Get users by tenantId - none', async () => {
const res = await client.get('/users').expect(200);
expect(res.body).to.eql([{tenantId: '', id: '1', name: 'Jane'}]);
});

function addJWT(test: supertest.Test, tenantId: string) {
const tenant = {
tenantId,
};
const jwt = sign(tenant, 'my-secret');
const token = `Bearer ${jwt}`;
return test.set('authorization', token);
}
});
7 changes: 5 additions & 2 deletions examples/multi-tenancy/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@

import {BootMixin} from '@loopback/boot';
import {ApplicationConfig} from '@loopback/core';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {
RestExplorerBindings,
RestExplorerComponent,
} from '@loopback/rest-explorer';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {ServiceMixin} from '@loopback/service-proxy';
import path from 'path';
import {MultiTenancyComponent} from './multi-tenancy/component';
import {MySequence} from './sequence';

export class ExampleMultiTenancyApplication extends BootMixin(
Expand All @@ -33,6 +34,8 @@ export class ExampleMultiTenancyApplication extends BootMixin(
});
this.component(RestExplorerComponent);

this.component(MultiTenancyComponent);

this.projectRoot = __dirname;
// Customize @loopback/boot Booter Conventions here
this.bootOptions = {
Expand Down
1 change: 1 addition & 0 deletions examples/multi-tenancy/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
// License text available at https://opensource.org/licenses/MIT

export * from './ping.controller';
export * from './user.controller';
Loading

0 comments on commit 0c99edb

Please sign in to comment.