|
| 1 | +import crypto from 'node:crypto' |
| 2 | +import { LRUCache } from 'lru-cache' |
| 3 | +import objectSizeOf from 'object-sizeof' |
| 4 | +import { S3Credentials, S3CredentialsManagerStore, S3CredentialsRaw } from './store' |
| 5 | +import { createMutexByKey } from '@internal/concurrency' |
| 6 | +import { ERRORS } from '@internal/errors' |
| 7 | +import { getConfig } from '../../../config' |
| 8 | +import { decrypt, encrypt } from '@internal/auth' |
| 9 | +import { PubSubAdapter } from '@internal/pubsub' |
| 10 | + |
| 11 | +const TENANTS_S3_CREDENTIALS_UPDATE_CHANNEL = 'tenants_s3_credentials_update' |
| 12 | + |
| 13 | +const tenantS3CredentialsCache = new LRUCache<string, S3Credentials>({ |
| 14 | + maxSize: 1024 * 1024 * 50, // 50MB |
| 15 | + ttl: 1000 * 60 * 60, // 1 hour |
| 16 | + sizeCalculation: (value) => objectSizeOf(value), |
| 17 | + updateAgeOnGet: true, |
| 18 | + allowStale: false, |
| 19 | +}) |
| 20 | + |
| 21 | +const s3CredentialsMutex = createMutexByKey<S3Credentials>() |
| 22 | + |
| 23 | +export class S3CredentialsManager { |
| 24 | + private dbServiceRole: string |
| 25 | + |
| 26 | + constructor(private storage: S3CredentialsManagerStore) { |
| 27 | + const { dbServiceRole } = getConfig() |
| 28 | + this.dbServiceRole = dbServiceRole |
| 29 | + } |
| 30 | + |
| 31 | + /** |
| 32 | + * Keeps the in memory config cache up to date |
| 33 | + */ |
| 34 | + async listenForTenantUpdate(pubSub: PubSubAdapter): Promise<void> { |
| 35 | + await pubSub.subscribe(TENANTS_S3_CREDENTIALS_UPDATE_CHANNEL, (cacheKey) => { |
| 36 | + tenantS3CredentialsCache.delete(cacheKey) |
| 37 | + }) |
| 38 | + } |
| 39 | + |
| 40 | + /** |
| 41 | + * Create S3 Credential for a tenant |
| 42 | + * @param tenantId |
| 43 | + * @param data |
| 44 | + */ |
| 45 | + async createS3Credentials( |
| 46 | + tenantId: string, |
| 47 | + data: { description: string; claims?: S3Credentials['claims'] } |
| 48 | + ) { |
| 49 | + const existingCount = await this.countS3Credentials(tenantId) |
| 50 | + |
| 51 | + if (existingCount >= 50) { |
| 52 | + throw ERRORS.MaximumCredentialsLimit() |
| 53 | + } |
| 54 | + |
| 55 | + const accessKey = crypto.randomBytes(32).toString('hex').slice(0, 32) |
| 56 | + const secretKey = crypto.randomBytes(64).toString('hex').slice(0, 64) |
| 57 | + |
| 58 | + if (data.claims) { |
| 59 | + delete data.claims.iss |
| 60 | + delete data.claims.issuer |
| 61 | + delete data.claims.exp |
| 62 | + delete data.claims.iat |
| 63 | + } |
| 64 | + |
| 65 | + const claims = { |
| 66 | + ...(data.claims || {}), |
| 67 | + role: data.claims?.role ?? this.dbServiceRole, |
| 68 | + issuer: `supabase.storage.${tenantId}`, |
| 69 | + sub: data.claims?.sub, |
| 70 | + } |
| 71 | + |
| 72 | + const id = await this.storage.insert(tenantId, { |
| 73 | + description: data.description, |
| 74 | + claims, |
| 75 | + accessKey, |
| 76 | + secretKey: encrypt(secretKey), |
| 77 | + }) |
| 78 | + |
| 79 | + return { |
| 80 | + id, |
| 81 | + access_key: accessKey, |
| 82 | + secret_key: secretKey, |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + async getS3CredentialsByAccessKey(tenantId: string, accessKey: string): Promise<S3Credentials> { |
| 87 | + const cacheKey = `${tenantId}:${accessKey}` |
| 88 | + const cachedCredentials = tenantS3CredentialsCache.get(cacheKey) |
| 89 | + |
| 90 | + if (cachedCredentials) { |
| 91 | + return cachedCredentials |
| 92 | + } |
| 93 | + |
| 94 | + return s3CredentialsMutex(cacheKey, async () => { |
| 95 | + const cachedCredentials = tenantS3CredentialsCache.get(cacheKey) |
| 96 | + |
| 97 | + if (cachedCredentials) { |
| 98 | + return cachedCredentials |
| 99 | + } |
| 100 | + |
| 101 | + const data = await this.storage.getOneByAccessKey(tenantId, accessKey) |
| 102 | + |
| 103 | + if (!data) { |
| 104 | + throw ERRORS.MissingS3Credentials() |
| 105 | + } |
| 106 | + |
| 107 | + data.secretKey = decrypt(data.secretKey) |
| 108 | + |
| 109 | + tenantS3CredentialsCache.set(cacheKey, data) |
| 110 | + |
| 111 | + return data |
| 112 | + }) |
| 113 | + } |
| 114 | + |
| 115 | + deleteS3Credential(tenantId: string, credentialId: string): Promise<number> { |
| 116 | + return this.storage.delete(tenantId, credentialId) |
| 117 | + } |
| 118 | + |
| 119 | + listS3Credentials(tenantId: string): Promise<S3CredentialsRaw[]> { |
| 120 | + return this.storage.list(tenantId) |
| 121 | + } |
| 122 | + |
| 123 | + async countS3Credentials(tenantId: string) { |
| 124 | + return this.storage.count(tenantId) |
| 125 | + } |
| 126 | +} |
0 commit comments