Skip to content

FEAT: Multi tenant feature #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
36c2e08
FEAT: Multi-tenancy support
bharathkeyvalue Dec 11, 2024
66d5966
TEST: multi-tenent test fixes
bharathkeyvalue Dec 11, 2024
1e4d149
FEAT: save tenantId in execution context
bharathkeyvalue Dec 11, 2024
2900db2
FEAT: add postgres rls for tenant isolation
sruuuthy Dec 11, 2024
67f6134
FIX: make execution context binder injectable
sruuuthy Dec 11, 2024
65513d8
FEAT: use new database connection per tenant
sruuuthy Dec 12, 2024
7572a60
FEAT: add tenant creation api
bharathkeyvalue Dec 12, 2024
5e0b2a6
REFACTOR: remove tenantId in response
bharathkeyvalue Dec 12, 2024
9d10fea
FEAT: modified usages of db queries to use tenant specific connection
sruuuthy Dec 12, 2024
001b8bb
FIX: add configurable max connection limit per tenant if needed
sruuuthy Dec 13, 2024
ea0526f
REFACTOR: rename executionId.middleware.ts to executionContext.middle…
sruuuthy Dec 13, 2024
93da280
FEAT: use sepearate db users for migrations and tenant operations
sruuuthy Dec 13, 2024
29168b2
FIX: use dynamic connection for user permission updation
sruuuthy Dec 13, 2024
f9c3033
FIX: use admin user for db connection setup
sruuuthy Dec 13, 2024
f0c0d75
FEAT: add env validations for new postgres user variables
sruuuthy Dec 13, 2024
c0ec4c5
DOCS: update README with PostgresSQL admin and tenant user setup
sruuuthy Dec 13, 2024
d0f032c
FEAT: add tenant module
bharathkeyvalue Dec 13, 2024
c985e37
REFACTOR: use NestJS dependency injection in the request scope to obt…
sruuuthy Dec 17, 2024
12e0a8e
REFACTOR: add util function for extracting token
bharathkeyvalue Dec 17, 2024
1e7837b
BLD: Add db init script for creation of tenant user and db
sruuuthy Dec 18, 2024
cf1df66
DOC: Added description for multi tenancy support
sruuuthy Dec 18, 2024
6fef4da
FEAT: Handle login for multi-tenancy
bharathkeyvalue Dec 18, 2024
28d64e2
DOC: Add description for env variables used for handling multi-tenanc…
bharathkeyvalue Dec 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ Developers can customise this as per their requirement.
- Clone the repo and execute command `npm install`
- Create a copy of the env.sample file and rename it as .env
- Install postgres and redis
- Create a restricted tenant user that cannot bypass RLS policies
- Provide postgres, redis secrets and default user details in .env file as mentioned below

| Database configuration(Required) | |
|--|--|
|POSTGRES_HOST | localhost |
|POSTGRES_PORT | 5432|
|POSTGRES_USER | postgres |
|POSTGRES_PASSWORD | postgres |
|POSTGRES_ADMIN_USER | postgres |
|POSTGRES_ADMIN_PASSWORD | postgres |
|POSTGRES_TENANT_USER | tenant |
|POSTGRES_TENANT_PASSWORD | tenant |
|POSTGRES_DB | auth_service |

 
Expand All @@ -80,6 +83,7 @@ Developers can customise this as per their requirement.
|JWT_TOKEN_EXPTIME|3600 |
|JWT_REFRESH_TOKEN_EXP_TIME| 36000 |
|ENV | local|
|AUTH_KEY| Required authentication key for tenant creation |

 
| Other Configuration(Required) | |
Expand Down Expand Up @@ -114,6 +118,12 @@ Developers can customise this as per their requirement.
| OTP_WINDOW | 300 |
| OTP_STEP | 1 |

 
|Multi-Tenancy Configuration(Optional) | |
|--|--|
|MULTI_TENANCY_ENABLED | A boolean that indicates if multi-tenancy is enabled, used for handling user login |
|DEFAULT_TENANT_ID | Default tenant id to be used when multi-tenancy is disabled |

- Run `npm run migration:run`
- Run `npm run start`
- Service should be up and running in http://localhost:${PORT}.
Expand All @@ -131,4 +141,18 @@ GraphQL endpoint

http://localhost:${PORT}/auth/api/graphql

[API Documentation](https://documenter.getpostman.com/view/10091423/U16ev8cG)
[API Documentation](https://documenter.getpostman.com/view/10091423/U16ev8cG)

## Multi-tenancy Support

This service supports multi-tenancy with complete data isolation between tenants at the database level using PostgreSQL row-level security (RLS). Each tenant's data is isolated using a tenant_id column and RLS policies.

### How it Works

1. Every tenant specific entities in the system has a `tenant_id` column
2. PostgreSQL Row Level Security (RLS) policies are enabled on all tables
3. The `app.tenant_id` configuration parameter is set for each request
4. Database queries are automatically filtered by the `tenant_id` through RLS policies

##### Setting up Database User for Multi-tenancy
Create a database user with restricted access (can be done using the provided init-db.sh)
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ services:
postgres:
container_name: postgres
image: postgres:12
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_ADMIN_USER: ${POSTGRES_USER}
PGPASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_TENANT_USER: ${POSTGRES_TENANT_USER}
POSTGRES_TENANT_PASSWORD: ${POSTGRES_TENANT_PASSWORD}
expose:
- '5432'
ports:
- '5432:5432'
volumes:
- /data/postgres:/var/lib/postgresql/data
- ./init-db.sh:/docker-entrypoint-initdb.d/init-db.sh
env_file:
- docker.env
networks:
Expand Down
2 changes: 2 additions & 0 deletions docker.env.sample
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=authentication-service
POSTGRES_TENANT_USER=tenant
POSTGRES_TENANT_PASSWORD=tenant
PGADMIN_DEFAULT_EMAIL=
PGADMIN_DEFAULT_PASSWORD=
12 changes: 10 additions & 2 deletions env.sample
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Superuser for migrations
POSTGRES_ADMIN_USER=postgres
POSTGRES_ADMIN_PASSWORD=postgres
# Minimal user with restricted access
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a snippet in the readme how multi-tenancy is handled? And also the ways to create a tenant Postgres user.

Let's add a DB Init script in the docker-compose.yml file as well

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in commit here

POSTGRES_TENANT_USER=tenant
POSTGRES_TENANT_PASSWORD=tenant
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=authentication-service
POSTGRES_TENANT_MAX_CONNECTION_LIMIT=10
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_CACHE_TTL=3600
Expand Down Expand Up @@ -32,3 +37,6 @@ MIN_RECAPTCHA_SCORE=.5
RECAPTCHA_VERIFY_URL=https://www.google.com/recaptcha/api/siteverify
DEFAULT_ADMIN_PASSWORD=adminpassword
INVITATION_TOKEN_EXPTIME = 7d
AUTH_KEY=
MULTI_TENANCY_ENABLED=
DEFAULT_TENANT_ID=
33 changes: 33 additions & 0 deletions init-db.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/bash
set -e

# Create the database if it doesn't exist
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_DB') THEN
CREATE DATABASE "$POSTGRES_DB"
WITH
OWNER = postgres
ENCODING = 'UTF8';
END IF;
END
\$\$;
EOSQL

# Create tenant user if it doesn't exist
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
DO \$\$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_user WHERE usename = '$POSTGRES_TENANT_USER') THEN
CREATE USER $POSTGRES_TENANT_USER WITH PASSWORD '$POSTGRES_TENANT_PASSWORD';
END IF;
END
\$\$;

GRANT CONNECT ON DATABASE "$POSTGRES_DB" TO "$POSTGRES_TENANT_USER";
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$POSTGRES_TENANT_USER";
GRANT USAGE ON SCHEMA public TO "$POSTGRES_TENANT_USER";
EOSQL

echo "Database setup complete"
34 changes: 26 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"twilio": "^3.67.1",
"typeorm": "^0.3.11",
"typeorm-naming-strategies": "^2.0.0",
"uuid": "^8.3.2",
"winston": "^3.3.3"
},
"devDependencies": {
Expand All @@ -86,6 +87,7 @@
"@types/speakeasy": "^2.0.6",
"@types/supertest": "^2.0.10",
"@types/totp-generator": "0.0.2",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"eslint": "^7.22.0",
Expand Down
15 changes: 11 additions & 4 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module } from '@nestjs/common';
import * as Joi from '@hapi/joi';
import { ConfigModule } from '@nestjs/config';

Expand All @@ -7,15 +7,18 @@ import { AppGraphQLModule } from './graphql/graphql.module';
import { UserAuthModule } from './authentication/authentication.module';
import { AuthorizationModule } from './authorization/authorization.module';
import { HealthModule } from './health/health.module';
import { ExecutionContextBinder } from './middleware/executionContext.middleware';

@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required(),
POSTGRES_ADMIN_USER: Joi.string().required(),
POSTGRES_ADMIN_PASSWORD: Joi.string().required(),
POSTGRES_TENANT_USER: Joi.string().required(),
POSTGRES_TENANT_PASSWORD: Joi.string().required(),
POSTGRES_DB: Joi.string().required(),
PORT: Joi.number(),
JWT_SECRET: Joi.string().required().min(10),
Expand All @@ -30,4 +33,8 @@ import { HealthModule } from './health/health.module';
controllers: [],
providers: [],
})
export class AppModule {}
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(ExecutionContextBinder).forRoutes('*');
}
}
20 changes: 20 additions & 0 deletions src/authentication/authKey.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { GqlExecutionContext } from '@nestjs/graphql';

@Injectable()
export class AuthKeyGuard implements CanActivate {
constructor(private configService: ConfigService) {}

canActivate(context: ExecutionContext): boolean {
const ctx = GqlExecutionContext.create(context).getContext();
if (ctx) {
const authKeyInHeader = ctx.headers['x-api-key'];
if (authKeyInHeader) {
const secretKey = this.configService.get('AUTH_KEY') as string;
return secretKey === authKeyInHeader;
}
}
return false;
}
}
35 changes: 20 additions & 15 deletions src/authentication/authentication.graphql
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
type Mutation {
passwordLogin(input: UserPasswordLoginInput!): TokenResponse
passwordSignup(input: UserPasswordSignupInput!): UserSignupResponse
inviteTokenSignup(input: UserInviteTokenSignupInput): InviteTokenResponse
refreshInviteToken(id: ID!):InviteTokenResponse
setPasswordForInvite(input: UserPasswordForInviteInput): UserSignupResponse
revokeInviteToken(id: ID!): Boolean
otpLogin(input: UserOTPLoginInput!): TokenResponse
otpSignup(input: UserOTPSignupInput!): UserSignupResponse
changePassword(input: UserPasswordInput!): User
refresh(input: RefreshTokenInput!): TokenResponse
logout: String
generateOtp(input: GenerateOtpInput): String
passwordLogin(input: UserPasswordLoginInput!): TokenResponse
passwordSignup(input: UserPasswordSignupInput!): UserSignupResponse
inviteTokenSignup(input: UserInviteTokenSignupInput): InviteTokenResponse
refreshInviteToken(id: ID!): InviteTokenResponse
setPasswordForInvite(input: UserPasswordForInviteInput): UserSignupResponse
revokeInviteToken(id: ID!): Boolean
otpLogin(input: UserOTPLoginInput!): TokenResponse
otpSignup(input: UserOTPSignupInput!): UserSignupResponse
changePassword(input: UserPasswordInput!): User
refresh(input: RefreshTokenInput!): TokenResponse
logout: String
generateOtp(input: GenerateOtpInput): String
}

input UserPasswordSignupInput {
Expand All @@ -20,6 +20,7 @@ input UserPasswordSignupInput {
firstName: String!
middleName: String
lastName: String!
tenantDomain: String
}

input UserOTPSignupInput {
Expand All @@ -28,16 +29,19 @@ input UserOTPSignupInput {
firstName: String!
middleName: String
lastName: String!
tenantDomain: String
}

input UserPasswordLoginInput {
username: String!
password: String!
tenantDomain: String
}

input UserOTPLoginInput {
username: String!
otp: String!
tenantDomain: String
}

type TokenResponse {
Expand Down Expand Up @@ -65,11 +69,12 @@ input RefreshTokenInput {
}

input GenerateOtpInput {
phone: String!
phone: String!
tenantDomain: String
}

input Enable2FAInput{
code: String!
input Enable2FAInput {
code: String!
}

input UserInviteTokenSignupInput {
Expand Down
Loading