fix: require explicit pilot tenant and reject development placeholder
This commit is contained in:
parent
23a6f5ca80
commit
df39565b68
10 changed files with 70 additions and 8 deletions
|
|
@ -11,5 +11,6 @@ PLATFORM_GATEWAY_URL=http://host.docker.internal:3000
|
|||
PLATFORM_JWT_ISSUER=https://platform.example.invalid
|
||||
PLATFORM_JWT_AUDIENCE=krona-staff
|
||||
PLATFORM_JWKS_URL=https://platform.example.invalid/.well-known/jwks.json
|
||||
STANDALONE_TENANT_ID=01J00000000000000000000000
|
||||
# Set this to the organization's tenant ID from Krona Business Platform.
|
||||
STANDALONE_TENANT_ID=
|
||||
STANDALONE_ACTOR_ID=01J00000000000000000000001
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Перед следующей командой заполните STANDALONE_TENANT_ID в .env:
|
||||
# используйте ULID организации из Krona Business Platform, не ID сотрудника.
|
||||
docker compose --profile tools run --rm migrate
|
||||
docker compose up --build app postgres
|
||||
```
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ services:
|
|||
PLATFORM_JWT_ISSUER: ${PLATFORM_JWT_ISSUER:-}
|
||||
PLATFORM_JWT_AUDIENCE: ${PLATFORM_JWT_AUDIENCE:-}
|
||||
PLATFORM_JWKS_URL: ${PLATFORM_JWKS_URL:-}
|
||||
STANDALONE_TENANT_ID: ${STANDALONE_TENANT_ID:-01J00000000000000000000000}
|
||||
STANDALONE_TENANT_ID: ${STANDALONE_TENANT_ID:?Set STANDALONE_TENANT_ID to the platform organization ID}
|
||||
STANDALONE_ACTOR_ID: ${STANDALONE_ACTOR_ID:-01J00000000000000000000001}
|
||||
ports: ['8080:8080']
|
||||
depends_on:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,20 @@ cd "$(dirname "$0")"
|
|||
umask 077
|
||||
mkdir -p secrets
|
||||
|
||||
validate_tenant() {
|
||||
: "${STANDALONE_TENANT_ID:?Set STANDALONE_TENANT_ID to the platform organization ID before setup}"
|
||||
if ! printf '%s\n' "$STANDALONE_TENANT_ID" | LC_ALL=C grep -Eq '^[0-9A-HJKMNP-TV-Z]{26}$'; then
|
||||
echo 'STANDALONE_TENANT_ID must be a 26-character ULID' >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$STANDALONE_TENANT_ID" = '01J00000000000000000000000' ]; then
|
||||
echo 'The development placeholder cannot be used for a pilot organization' >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
validate_tenant
|
||||
migration_password="$(openssl rand -hex 32)"
|
||||
app_password="$(openssl rand -hex 32)"
|
||||
manager_token="$(openssl rand -hex 32)"
|
||||
|
|
@ -17,7 +30,7 @@ if [ ! -f .env ]; then
|
|||
printf 'STANDALONE_MANAGER_TOKEN=%s\n' "$manager_token"
|
||||
printf 'STANDALONE_TERMINAL_TOKEN=%s\n' "$terminal_token"
|
||||
printf 'HTTPS_PFX_PASSPHRASE=%s\n' "$pfx_passphrase"
|
||||
printf 'STANDALONE_TENANT_ID=01J00000000000000000000000\n'
|
||||
printf 'STANDALONE_TENANT_ID=%s\n' "$STANDALONE_TENANT_ID"
|
||||
printf 'STANDALONE_ACTOR_ID=01J00000000000000000000001\n'
|
||||
printf 'STANDALONE_TERMINAL_ACTOR_ID=01J00000000000000000000002\n'
|
||||
} > .env
|
||||
|
|
@ -25,6 +38,9 @@ if [ ! -f .env ]; then
|
|||
printf '%s\n' "$terminal_token" > secrets/terminal.token
|
||||
fi
|
||||
|
||||
. ./.env
|
||||
validate_tenant
|
||||
|
||||
if ! grep -q '^VAPID_PUBLIC_KEY=' .env; then
|
||||
docker run --rm \
|
||||
-v "$(cd ../.. && pwd):/app:ro" \
|
||||
|
|
@ -34,6 +50,7 @@ if ! grep -q '^VAPID_PUBLIC_KEY=' .env; then
|
|||
fi
|
||||
|
||||
. ./.env
|
||||
validate_tenant
|
||||
if [ ! -f secrets/staff.pfx ]; then
|
||||
openssl req -x509 -newkey rsa:3072 -sha256 -days 365 -nodes \
|
||||
-keyout secrets/staff.key -out secrets/staff.crt \
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Развёртывание
|
||||
|
||||
После копирования `.env.example` обязательно заполните `STANDALONE_TENANT_ID` идентификатором организации (ULID) из Krona Business Platform. Значения по умолчанию нет; прежняя заглушка `01J00000000000000000000000` не подходит для Compose или пилота. Для первого запуска `deploy/pilot/setup.sh` передайте этот ID через переменную окружения: установщик проверит его и сохранит в своём `.env`. При повторном запуске проверяется существующий `.env`. Для уже заполненного пилота нельзя просто заменить ID: сначала нужна резервная копия, проверка восстановления и перенос tenant-данных.
|
||||
|
||||
Локально скопируйте `.env.example` в `.env` и выполните `docker compose up --build`. Compose последовательно проверит PostgreSQL, создаст или обновит отдельную роль `krona_staff_app` с `NOSUPERUSER NOBYPASSRLS`, выполнит миграции ролью `krona_migrator` и только затем запустит приложение. Runtime-контейнер не получает пароль или URL мигратора. Redis и object storage не включаются до появления реального сценария очередей или файлов.
|
||||
|
||||
Для обновления старого Docker volume, созданного версией с ролью `krona_staff`, задайте в `.env` `POSTGRES_MIGRATION_USER=krona_staff` и прежний `POSTGRES_MIGRATION_PASSWORD`, затем выполните обычный `docker compose up --build`. Сохраняйте эту роль владельцем миграций для данного volume: автоматического переименования владельца существующей базы нет, а перенос владения требует отдельного DBA-плана с повторной настройкой default privileges.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
const PLACEHOLDER_TENANT_ID = '01J00000000000000000000000';
|
||||
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
||||
|
||||
const configSchema = z.object({
|
||||
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
||||
PORT: z.coerce.number().int().min(1).max(65535).default(8080),
|
||||
|
|
@ -23,7 +26,9 @@ const configSchema = z.object({
|
|||
PLATFORM_JWT_ISSUER: optionalUrl(),
|
||||
PLATFORM_JWT_AUDIENCE: emptyStringAsUndefined(z.string().optional()),
|
||||
PLATFORM_JWKS_URL: optionalUrl(),
|
||||
STANDALONE_TENANT_ID: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
|
||||
STANDALONE_TENANT_ID: z
|
||||
.string({ error: 'STANDALONE_TENANT_ID is required and must be a ULID' })
|
||||
.regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
|
||||
STANDALONE_ACTOR_ID: z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/),
|
||||
VAPID_PUBLIC_KEY: emptyStringAsUndefined(z.string().optional()),
|
||||
VAPID_PRIVATE_KEY: emptyStringAsUndefined(z.string().optional()),
|
||||
|
|
@ -42,6 +47,14 @@ export type AppConfig = z.infer<typeof configSchema>;
|
|||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
const config = configSchema.parse(env);
|
||||
if (
|
||||
config.STANDALONE_TENANT_ID === PLACEHOLDER_TENANT_ID &&
|
||||
!(config.NODE_ENV === 'development' && LOOPBACK_HOSTS.has(config.HOST.toLowerCase()))
|
||||
) {
|
||||
throw new Error(
|
||||
'STANDALONE_TENANT_ID uses the development placeholder, which is allowed only in development on loopback',
|
||||
);
|
||||
}
|
||||
if (Boolean(config.HTTPS_PFX_PATH) !== Boolean(config.HTTPS_PFX_PASSPHRASE)) {
|
||||
throw new Error('HTTPS_PFX_PATH and HTTPS_PFX_PASSPHRASE must be configured together');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { loadConfig } from '../../src/config.js';
|
|||
|
||||
const config = loadConfig({
|
||||
DATABASE_URL: 'postgres://localhost/staff',
|
||||
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
||||
STANDALONE_TENANT_ID: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
|
||||
STANDALONE_ACTOR_ID: '01J00000000000000000000001',
|
||||
LOG_LEVEL: 'silent',
|
||||
NODE_ENV: 'test',
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function assertOpenApiDocument(value: unknown): asserts value is OpenApiDocument
|
|||
|
||||
const config = loadConfig({
|
||||
DATABASE_URL: 'postgres://localhost/staff',
|
||||
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
||||
STANDALONE_TENANT_ID: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
|
||||
STANDALONE_ACTOR_ID: '01J00000000000000000000001',
|
||||
LOG_LEVEL: 'silent',
|
||||
NODE_ENV: 'test',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { loadConfig } from '../../src/config.js';
|
|||
|
||||
const base = {
|
||||
DATABASE_URL: 'postgres://localhost/staff',
|
||||
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
||||
STANDALONE_TENANT_ID: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
|
||||
STANDALONE_ACTOR_ID: '01J00000000000000000000001',
|
||||
LOG_LEVEL: 'silent',
|
||||
NODE_ENV: 'test',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { loadConfig } from '../../src/config.js';
|
|||
|
||||
const base = {
|
||||
DATABASE_URL: 'postgres://localhost/staff',
|
||||
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
||||
STANDALONE_TENANT_ID: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
|
||||
STANDALONE_ACTOR_ID: '01J00000000000000000000001',
|
||||
};
|
||||
|
||||
|
|
@ -18,6 +18,33 @@ describe('runtime modes', () => {
|
|||
expect(() => loadConfig({ ...base, NODE_ENV: 'production' })).toThrow(
|
||||
/standalone is not allowed/,
|
||||
));
|
||||
it('requires a standalone tenant ID', () => {
|
||||
expect(() => loadConfig({ ...base, STANDALONE_TENANT_ID: undefined })).toThrow(
|
||||
/STANDALONE_TENANT_ID/,
|
||||
);
|
||||
});
|
||||
it('allows the placeholder only in development on loopback', () => {
|
||||
expect(
|
||||
loadConfig({
|
||||
...base,
|
||||
NODE_ENV: 'development',
|
||||
HOST: '127.0.0.1',
|
||||
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
||||
}).STANDALONE_TENANT_ID,
|
||||
).toBe('01J00000000000000000000000');
|
||||
});
|
||||
it('rejects the placeholder in test mode and development on a non-loopback host', () => {
|
||||
const placeholder = {
|
||||
...base,
|
||||
STANDALONE_TENANT_ID: '01J00000000000000000000000',
|
||||
};
|
||||
expect(() => loadConfig({ ...placeholder, NODE_ENV: 'test' })).toThrow(
|
||||
/allowed only in development on loopback/,
|
||||
);
|
||||
expect(() => loadConfig({ ...placeholder, NODE_ENV: 'development', HOST: '0.0.0.0' })).toThrow(
|
||||
/allowed only in development on loopback/,
|
||||
);
|
||||
});
|
||||
it('treats empty optional platform variables as absent in compose', () =>
|
||||
expect(
|
||||
loadConfig({
|
||||
|
|
|
|||
Loading…
Reference in a new issue