Skip to content

Commit

Permalink
feat: default service-key for single tenant (#420)
Browse files Browse the repository at this point in the history
  • Loading branch information
fenos authored Jan 16, 2024
1 parent 39f7413 commit b287d5f
Show file tree
Hide file tree
Showing 14 changed files with 72 additions and 87 deletions.
7 changes: 2 additions & 5 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,12 @@ SERVER_REGION=region-of-where-your-service-is-running
#######################################
AUTH_JWT_SECRET=f023d3db-39dc-4ac9-87b2-b2be72e9162b
AUTH_JWT_ALGORITHM=HS256
AUTH_ENCRYPTION_KEY=encryptionkey


#######################################
# Single Tenant
#######################################
TENANT_ID=bjhaohmqunupljrqypxz
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMzUzMTk4NSwiZXhwIjoxOTI5MTA3OTg1fQ.mqfi__KnQB4v6PkIjkhzfwWrYyF94MEbSC6LnuvVniE
SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjEzNTMxOTg1LCJleHAiOjE5MjkxMDc5ODV9.th84OKK0Iz8QchDyXZRrojmKSEZ-OuitQm_5DvLiSIc


#######################################
# Multi Tenancy
Expand All @@ -33,7 +29,8 @@ SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiw
# MULTI_TENANT=true
DATABASE_MULTITENANT_URL=postgresql://postgres:[email protected]:5433/postgres
REQUEST_X_FORWARDED_HOST_REGEXP=
ADMIN_API_KEYS=apikey
SERVER_ADMIN_API_KEYS=apikey
AUTH_ENCRYPTION_KEY=encryptionkey


#######################################
Expand Down
14 changes: 7 additions & 7 deletions docker-compose-infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ services:
- '5432:5432'
healthcheck:
test: [ "CMD-SHELL", "pg_isready", "-d", "postgres" ]
interval: 50s
interval: 5s
timeout: 60s
retries: 5
retries: 20
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
Expand All @@ -27,9 +27,9 @@ services:
target: /docker-entrypoint-initdb.d/init.sql
healthcheck:
test: [ "CMD-SHELL", "pg_isready", "-d", "postgres" ]
interval: 50s
interval: 5s
timeout: 60s
retries: 5
retries: 20
environment:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
Expand Down Expand Up @@ -118,9 +118,9 @@ services:
- '9001:9001'
healthcheck:
test: timeout 5s bash -c ':> /dev/tcp/127.0.0.1/9000' || exit 1
interval: 10s
timeout: 5s
retries: 2
interval: 5s
timeout: 20s
retries: 10
environment:
MINIO_ROOT_USER: supa-storage
MINIO_ROOT_PASSWORD: secret1234
Expand Down
5 changes: 0 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,10 @@ services:
environment:
# Server
SERVER_PORT: 5000
SERVER_REGION: local
# Auth
AUTH_JWT_SECRET: f023d3db-39dc-4ac9-87b2-b2be72e9162b
AUTH_JWT_ALGORITHM: HS256
AUTH_ENCRYPTION_KEY: encryptionkey
# Single tenant Mode
TENANT_ID: bjwdssmqcnupljrqypxz
ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYxMzUzMTk4NSwiZXhwIjoxOTI5MTA3OTg1fQ.mqfi__KnQB4v6PkIjkhzfwWrYyF94MEbSC6LnuvVniE
SERVICE_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaWF0IjoxNjEzNTMxOTg1LCJleHAiOjE5MjkxMDc5ODV9.th84OKK0Iz8QchDyXZRrojmKSEZ-OuitQm_5DvLiSIc
DATABASE_URL: postgres://postgres:postgres@tenant_db:5432/postgres
DATABASE_POOL_URL: postgresql://postgres:postgres@pg_bouncer:6432/postgres
# Migrations
Expand Down
16 changes: 1 addition & 15 deletions src/auth/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { getJwtSecret as getJwtSecretForTenant } from '../database/tenant'
import jwt from 'jsonwebtoken'
import { getConfig } from '../config'

const { isMultitenant, jwtSecret, jwtAlgorithm } = getConfig()
const { jwtAlgorithm } = getConfig()

interface jwtInterface {
sub?: string
Expand All @@ -21,19 +20,6 @@ export type SignedUploadToken = {
exp: number
}

/**
* Gets the JWT secret key from the env PGRST_JWT_SECRET when running in single-tenant
* or querying the multi-tenant database by the given tenantId
* @param tenantId
*/
export async function getJwtSecret(tenantId: string): Promise<string> {
let secret = jwtSecret
if (isMultitenant) {
secret = await getJwtSecretForTenant(tenantId)
}
return secret
}

/**
* Verifies if a JWT is valid
* @param token
Expand Down
27 changes: 18 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import dotenv from 'dotenv'
import jwt from 'jsonwebtoken'

export type StorageBackendType = 'file' | 's3'

Expand All @@ -8,7 +9,6 @@ type StorageConfigType = {
headersTimeout: number
adminApiKeys: string
adminRequestIdHeader?: string
anonKey: string
encryptionKey: string
uploadFileSizeLimit: number
uploadFileSizeLimitStandard?: number
Expand Down Expand Up @@ -132,12 +132,12 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
// Tenant
tenantId:
getOptionalConfigFromEnv('PROJECT_REF') ||
getOptionalIfMultitenantConfigFromEnv('TENANT_ID') ||
'',
getOptionalConfigFromEnv('TENANT_ID') ||
'storage-single-tenant',
isMultitenant: getOptionalConfigFromEnv('MULTI_TENANT', 'IS_MULTITENANT') === 'true',

// Server
region: getConfigFromEnv('SERVER_REGION', 'REGION'),
region: getOptionalConfigFromEnv('SERVER_REGION', 'REGION') || 'not-specified',
version: getOptionalConfigFromEnv('VERSION') || '0.0.0',
keepAliveTimeout: parseInt(getOptionalConfigFromEnv('SERVER_KEEP_ALIVE_TIMEOUT') || '61', 10),
headersTimeout: parseInt(getOptionalConfigFromEnv('SERVER_HEADERS_TIMEOUT') || '65', 10),
Expand All @@ -162,14 +162,16 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
),

// Auth
anonKey: getOptionalIfMultitenantConfigFromEnv('ANON_KEY') || '',
serviceKey: getOptionalIfMultitenantConfigFromEnv('SERVICE_KEY') || '',
serviceKey: getOptionalConfigFromEnv('SERVICE_KEY') || '',

encryptionKey: getOptionalConfigFromEnv('AUTH_ENCRYPTION_KEY', 'ENCRYPTION_KEY') || '',
jwtSecret: getOptionalIfMultitenantConfigFromEnv('AUTH_JWT_SECRET', 'PGRST_JWT_SECRET') || '',
jwtAlgorithm: getOptionalConfigFromEnv('AUTH_JWT_ALGORITHM', 'PGRST_JWT_ALGORITHM') || 'HS256',

// Upload
uploadFileSizeLimit: Number(getConfigFromEnv('UPLOAD_FILE_SIZE_LIMIT', 'FILE_SIZE_LIMIT')),
uploadFileSizeLimit: Number(
getOptionalConfigFromEnv('UPLOAD_FILE_SIZE_LIMIT', 'FILE_SIZE_LIMIT')
),
uploadFileSizeLimitStandard: parseInt(
getOptionalConfigFromEnv(
'UPLOAD_FILE_SIZE_LIMIT_STANDARD',
Expand All @@ -193,7 +195,7 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
getOptionalConfigFromEnv('TUS_USE_FILE_VERSION_SEPARATOR') === 'true',

// Storage
storageBackendType: getConfigFromEnv('STORAGE_BACKEND') as StorageBackendType,
storageBackendType: getOptionalConfigFromEnv('STORAGE_BACKEND') as StorageBackendType,

// Storage - File
storageFilePath: getOptionalConfigFromEnv('STORAGE_FILE_BACKEND_PATH', 'STORAGE_FILE_PATH'),
Expand All @@ -203,7 +205,7 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
getOptionalConfigFromEnv('STORAGE_S3_MAX_SOCKETS', 'GLOBAL_S3_MAX_SOCKETS') || '200',
10
),
storageS3Bucket: getConfigFromEnv('STORAGE_S3_BUCKET', 'GLOBAL_S3_BUCKET'),
storageS3Bucket: getOptionalConfigFromEnv('STORAGE_S3_BUCKET', 'GLOBAL_S3_BUCKET'),
storageS3Endpoint: getOptionalConfigFromEnv('STORAGE_S3_ENDPOINT', 'GLOBAL_S3_ENDPOINT'),
storageS3ForcePathStyle:
getOptionalConfigFromEnv('STORAGE_S3_FORCE_PATH_STYLE', 'GLOBAL_S3_FORCE_PATH_STYLE') ===
Expand Down Expand Up @@ -328,6 +330,13 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
getOptionalConfigFromEnv('RATE_LIMITER_REDIS_COMMAND_TIMEOUT') || '2',
10
),
} as StorageConfigType

if (!config.isMultitenant && !config.serviceKey) {
config.serviceKey = jwt.sign({ role: config.dbServiceRole }, config.jwtSecret, {
expiresIn: '10y',
algorithm: config.jwtAlgorithm as jwt.Algorithm,
})
}

return config
Expand Down
63 changes: 28 additions & 35 deletions src/database/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { JwtPayload } from 'jsonwebtoken'
import { PubSubAdapter } from '../pubsub'

interface TenantConfig {
anonKey: string
anonKey?: string
databaseUrl: string
databasePoolUrl?: string
maxConnections?: number
Expand All @@ -26,11 +26,23 @@ export interface Features {
}
}

const { isMultitenant, serviceKey, jwtSecret } = getConfig()
const { isMultitenant, dbServiceRole, serviceKey, jwtSecret } = getConfig()

const tenantConfigCache = new Map<string, TenantConfig>()

let singleTenantServiceKeyPayload: ({ role: string } & JwtPayload) | undefined = undefined
const singleTenantServiceKey:
| {
jwt: string
payload: { role: string } & JwtPayload
}
| undefined = !isMultitenant
? {
jwt: serviceKey,
payload: {
role: dbServiceRole,
},
}
: undefined

/**
* Runs migrations in a specific tenant
Expand Down Expand Up @@ -116,44 +128,22 @@ export async function getTenantConfig(tenantId: string): Promise<TenantConfig> {
return config
}

/**
* Get the anon key from the tenant config
* @param tenantId
*/
export async function getAnonKey(tenantId: string): Promise<string> {
const { anonKey } = await getTenantConfig(tenantId)
return anonKey
}

export async function getServiceKeyUser(tenantId: string) {
let serviceKeyPayload: { role?: string } | undefined
let tenantJwtSecret = jwtSecret
let tenantServiceKey = serviceKey

if (isMultitenant) {
const tenant = await getTenantConfig(tenantId)
serviceKeyPayload = tenant.serviceKeyPayload
tenantJwtSecret = tenant.jwtSecret
tenantServiceKey = tenant.serviceKey
} else {
serviceKeyPayload = await getSingleTenantServiceKeyPayload()
}

return {
jwt: tenantServiceKey,
payload: serviceKeyPayload,
jwtSecret: tenantJwtSecret,
return {
jwt: tenant.serviceKey,
payload: tenant.serviceKeyPayload,
jwtSecret: tenant.jwtSecret,
}
}
}

export async function getSingleTenantServiceKeyPayload() {
if (singleTenantServiceKeyPayload) {
return singleTenantServiceKeyPayload
return {
jwt: singleTenantServiceKey!.jwt,

Check warning on line 143 in src/database/tenant.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Forbidden non-null assertion
payload: singleTenantServiceKey!.payload,

Check warning on line 144 in src/database/tenant.ts

View workflow job for this annotation

GitHub Actions / Test / OS ubuntu-20.04 / Node 20

Forbidden non-null assertion
jwtSecret: jwtSecret,
}

singleTenantServiceKeyPayload = await verifyJWT(serviceKey, jwtSecret)

return singleTenantServiceKeyPayload
}

/**
Expand All @@ -170,7 +160,10 @@ export async function getServiceKey(tenantId: string): Promise<string> {
* @param tenantId
*/
export async function getJwtSecret(tenantId: string): Promise<string> {
const { jwtSecret } = await getTenantConfig(tenantId)
if (isMultitenant) {
const { jwtSecret } = await getTenantConfig(tenantId)
return jwtSecret
}
return jwtSecret
}

Expand Down
3 changes: 2 additions & 1 deletion src/http/plugins/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fastifyPlugin from 'fastify-plugin'
import { createResponse } from '../generic-routes'
import { getJwtSecret, getOwner } from '../../auth'
import { getOwner } from '../../auth'
import { getJwtSecret } from '../../database/tenant'

declare module 'fastify' {
interface FastifyRequest {
Expand Down
3 changes: 2 additions & 1 deletion src/http/routes/object/getSignedObject.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FastifyInstance } from 'fastify'
import { FromSchema } from 'json-schema-to-ts'
import { getConfig } from '../../../config'
import { getJwtSecret, SignedToken, verifyJWT } from '../../../auth'
import { SignedToken, verifyJWT } from '../../../auth'
import { StorageBackendError } from '../../../storage'
import { getJwtSecret } from '../../../database/tenant'

const { storageS3Bucket } = getConfig()

Expand Down
3 changes: 2 additions & 1 deletion src/http/routes/object/uploadSignedObject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FastifyInstance } from 'fastify'
import { FromSchema } from 'json-schema-to-ts'
import { getJwtSecret, SignedUploadToken, verifyJWT } from '../../../auth'
import { SignedUploadToken, verifyJWT } from '../../../auth'
import { StorageBackendError } from '../../../storage'
import { getJwtSecret } from '../../../database/tenant'

const uploadSignedObjectParamsSchema = {
type: 'object',
Expand Down
5 changes: 3 additions & 2 deletions src/http/routes/render/renderSignedImage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { getConfig } from '../../../config'
import { FromSchema } from 'json-schema-to-ts'
import { FastifyInstance } from 'fastify'
import { getConfig } from '../../../config'
import { ImageRenderer } from '../../../storage/renderer'
import { getJwtSecret, SignedToken, verifyJWT } from '../../../auth'
import { SignedToken, verifyJWT } from '../../../auth'
import { StorageBackendError } from '../../../storage'
import { getJwtSecret } from '../../../database/tenant'

const { storageS3Bucket } = getConfig()

Expand Down
4 changes: 2 additions & 2 deletions src/http/routes/tus/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const {
storageS3Bucket,
storageS3Endpoint,
storageS3ForcePathStyle,
region,
storageS3Region,
tusUrlExpiryMs,
tusPath,
storageBackendType,
Expand All @@ -51,7 +51,7 @@ function createTusStore() {
expirationPeriodInMilliseconds: tusUrlExpiryMs,
s3ClientConfig: {
bucket: storageS3Bucket,
region: region,
region: storageS3Region,
endpoint: storageS3Endpoint,
forcePathStyle: storageS3ForcePathStyle,
},
Expand Down
3 changes: 2 additions & 1 deletion src/storage/object.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StorageBackendAdapter, ObjectMetadata, withOptionalVersion } from './backend'
import { Database, FindObjectFilters, SearchObjectOption } from './database'
import { mustBeValidKey } from './limits'
import { getJwtSecret, signJWT } from '../auth'
import { signJWT } from '../auth'
import { getConfig } from '../config'
import { FastifyRequest } from 'fastify'
import { Uploader } from './uploader'
Expand All @@ -15,6 +15,7 @@ import {
} from '../queue'
import { randomUUID } from 'crypto'
import { StorageBackendError } from './errors'
import { getJwtSecret } from '../database/tenant'

export interface UploadObjectOptions {
objectName: string
Expand Down
3 changes: 1 addition & 2 deletions src/test/bucket.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'use strict'
import dotenv from 'dotenv'
import app from '../app'
import { getConfig } from '../config'
import { S3Backend } from '../storage/backend'

dotenv.config({ path: '.env.test' })
const { anonKey } = getConfig()
const anonKey = process.env.ANON_KEY || ''

beforeAll(() => {
jest.spyOn(S3Backend.prototype, 'deleteObjects').mockImplementation(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/test/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { Knex } from 'knex'

dotenv.config({ path: '.env.test' })

const { anonKey, jwtSecret, serviceKey, tenantId } = getConfig()
const { jwtSecret, serviceKey, tenantId } = getConfig()
const anonKey = process.env.ANON_KEY || ''

let tnx: Knex.Transaction | undefined
async function getSuperuserPostgrestClient() {
Expand Down

0 comments on commit b287d5f

Please sign in to comment.