- dialplan_extensions table (tenant-scoped, RLS): structured editor per agente.md secao 43 -- context, condition field/expr, actions/anti-actions (JSON), continue, order, enabled. One condition per extension (deliberate simplification vs raw FreeSWITCH's multi-condition extensions). - dialplan_versions table (tenant-scoped, RLS): generate/validate/version/ activate flow (secao 44). Reactivating an older version IS the rollback mechanism -- no separate endpoint needed. - apps/api/src/dialplan: extensions CRUD + versions/generate (builds XML, validates well-formedness with fast-xml-parser, saves as DRAFT) + versions/:id/activate (atomically flips ACTIVE, supersedes the previous one). Reused freeswitch.view/.configure permissions rather than inventing new ones not in the agente.md permission list. - packages/telephony: buildDialplanXml() plus ALLOWED_DIALPLAN_APPLICATIONS, an explicit allowlist (answer/bridge/playback/hangup/set/export/... -- deliberately no system/exec/socket) guarding against a tenant configuring a dialplan action that runs arbitrary commands on the FreeSWITCH host (agente.md secao 180) - b2bcall-fs-config resolves dialplan dynamically per call (unlike Trunks' file+rescan approach -- dialplan is fetched fresh via mod_xml_curl on every call anyway) by tenant id from the variable_b2bcall_tenant_id channel variable already injected at directory resolution, then serving whichever DialplanVersion is ACTIVE for that context - verified end-to-end: created a rule for destination_number 7000, generated and activated v1, originated a call that actually routed through the dialplan (not bypassing it via &app()) -- CALL_CREATED -> CALL_ANSWERED -> CALL_ENDED with the correct tenantId throughout. Created and activated a v2, then rolled back to v1 by reactivating it; status transitions (ACTIVE/SUPERSEDED) all confirmed via the API. CRITICAL FINDING, fixed in this same phase: deliberately testing that the application allowlist rejects 'system' got back 201 instead of 400 -- NestJS's ValidationPipe had been silently inert across all of apps/api's @Body() DTOs since the API was first created. Root cause: running via (esbuild) instead of a real build -- esbuild doesn't always resolve cross-file parameter types for design:paramtypes metadata, and Nest skips validation without any error when it can't determine the DTO class. Fixed by always building with tsc before running (tsc && tsx dist/main.js -- still via tsx because internal workspace packages aren't built to JS yet). Re-verified with two deliberate bad-input tests post-fix, both correctly rejected with 400. A stray malicious test row (dialplan action 'system') created while the bug was live was deleted; it was never baked into an activated version, so nothing could have executed it. See docs/VALIDATION_PIPE_BUG.md for the full writeup. docs/DIALPLAN.md, docs/VALIDATION_PIPE_BUG.md, docs/EXTENSIONS.md updated
64 lines
2.6 KiB
SQL
64 lines
2.6 KiB
SQL
-- CreateEnum
|
|
CREATE TYPE "dialplan_version_status" AS ENUM ('DRAFT', 'ACTIVE', 'SUPERSEDED');
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "dialplan_extensions" (
|
|
"id" UUID NOT NULL,
|
|
"tenant_id" UUID NOT NULL,
|
|
"context" TEXT NOT NULL DEFAULT 'default',
|
|
"name" TEXT NOT NULL,
|
|
"condition_field" TEXT NOT NULL,
|
|
"condition_expr" TEXT NOT NULL,
|
|
"actions" JSONB NOT NULL,
|
|
"anti_actions" JSONB,
|
|
"continue_on_false" BOOLEAN NOT NULL DEFAULT false,
|
|
"order" INTEGER NOT NULL DEFAULT 0,
|
|
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
"deleted_at" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "dialplan_extensions_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateTable
|
|
CREATE TABLE "dialplan_versions" (
|
|
"id" UUID NOT NULL,
|
|
"tenant_id" UUID NOT NULL,
|
|
"context" TEXT NOT NULL,
|
|
"version" INTEGER NOT NULL,
|
|
"generated_xml" TEXT NOT NULL,
|
|
"status" "dialplan_version_status" NOT NULL DEFAULT 'DRAFT',
|
|
"created_by_user_id" UUID,
|
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
"activated_at" TIMESTAMP(3),
|
|
|
|
CONSTRAINT "dialplan_versions_pkey" PRIMARY KEY ("id")
|
|
);
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "dialplan_extensions_tenant_id_context_idx" ON "dialplan_extensions"("tenant_id", "context");
|
|
|
|
-- CreateIndex
|
|
CREATE INDEX "dialplan_versions_tenant_id_context_status_idx" ON "dialplan_versions"("tenant_id", "context", "status");
|
|
|
|
-- CreateIndex
|
|
CREATE UNIQUE INDEX "dialplan_versions_tenant_id_context_version_key" ON "dialplan_versions"("tenant_id", "context", "version");
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "dialplan_extensions" ADD CONSTRAINT "dialplan_extensions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
-- AddForeignKey
|
|
ALTER TABLE "dialplan_versions" ADD CONSTRAINT "dialplan_versions_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
|
|
-- Tabelas de negocio tenant-scoped: RLS obrigatorio (ver docs/TENANT_ISOLATION.md).
|
|
ALTER TABLE "dialplan_extensions" ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE "dialplan_extensions" FORCE ROW LEVEL SECURITY;
|
|
CREATE POLICY "tenant_isolation" ON "dialplan_extensions"
|
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|
|
|
|
ALTER TABLE "dialplan_versions" ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE "dialplan_versions" FORCE ROW LEVEL SECURITY;
|
|
CREATE POLICY "tenant_isolation" ON "dialplan_versions"
|
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|