Skip to content
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

feat: default service-key for single tenant #420

Merged
merged 1 commit into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 { PubSubAdapter } from '../pubsub'

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

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 All @@ -46,7 +58,7 @@
try {
await runMigrationsOnTenant(databaseUrl)
console.log(`${tenantId} migrations ran successfully`)
} catch (error: any) {

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

View workflow job for this annotation

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

Unexpected any. Specify a different type
if (logOnError) {
console.error(`${tenantId} migration error:`, error.message)
return
Expand Down Expand Up @@ -116,44 +128,22 @@
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 @@
* @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
Loading