feat: workspace settings as one document, applied server-side (#11740)

* feat: add the workspace settings document, applied server-side

* feat: add wmill workspace get-settings and apply-settings

* fix: warn on unknown settings keys and refuse ee-only ones in the dry run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: stub the settings document endpoint in the push settings unit tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: refuse mistyped settings and keep 4xx status on a partial settings apply

Reject non-boolean switches and non-string Slack credentials instead of
coercing them into destructive writes, preserve the failing endpoint's
status when wrapping a partial-apply error, keep the stored Slack secret
when an older CLI pushes one encrypted under another workspace's key,
print the plan's warnings in apply-settings, and state the admin check
load_settings_document leaves to its caller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: compare handler arguments verbatim and type-check scalar settings

A handler's extra_args is stored as sent and its nulls reach the job as
arguments, so an added or removed null argument is now a change. Scalar
settings are type-checked in the plan so a dry run never reports a change
the endpoint then refuses, and an error handler's omitted mute flags
compare as the false the endpoint stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: require a 32-bit integer rate limit in the settings plan

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: data table permissions and protection rules in the settings document

The settings document gains datatable_permissions, each data table's roles
named by their Postgres role name and written by catalog id, and
protection_rules, keyed by rule name. Both are managed only when stated and
stated as a whole. The dev workspace lock stays with the dev workspace
setting.

wmill sync carries datatable_permissions only with includeDatatablePermissions
(or --include-datatable-permissions), since tenants and roles are specific to
a workspace and an instance. protection_rules stay out of settings.yaml:
protection-rules.yaml syncs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: validate the default data table role and plan permissions against a replaced datatable

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-10-14 12:35:08 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 17fefcf777
commit 9a5ec223bc
25 changed files with 2449 additions and 136 deletions
+3 -1
View File
@@ -86,7 +86,8 @@ export async function downloadZip(
skipWorkspaceDependencies?: boolean,
skipDatatableMigrations?: boolean,
defaultTs?: "bun" | "deno",
syncBehavior?: string
syncBehavior?: string,
includeDatatablePermissions?: boolean,
): Promise<JSZip | TarAsZip | undefined> {
const requestHeaders = new Headers();
requestHeaders.set("Authorization", "Bearer " + workspace.token);
@@ -112,6 +113,7 @@ export async function downloadZip(
}&skip_secrets=${skipSecrets ?? false}&include_schedules=${includeSchedules ?? false
}&include_triggers=${includeTriggers ?? false}&include_users=${includeUsers ?? false
}&include_groups=${includeGroups ?? false}&include_settings=${includeSettings ?? false
}&include_datatable_permissions=${includeDatatablePermissions ?? false
}&include_key=${includeKey ?? false}&include_workspace_dependencies=${includeWorkspaceDependenciesValue}&skip_datatable_migrations=${skipDatatableMigrations ?? false}&default_ts=${defaultTs ?? "bun"}&skip_resource_types=${skipResourceTypes ?? false}&settings_version=v2&preserve_extra_perms=true&sync_behavior_version=${syncBehavior ?? "v0"}`;
const baseUrl = workspace.remote + "api/w/" + workspace.workspaceId + "/workspaces/tarball?";
+21
View File
@@ -14,6 +14,7 @@ import { colors } from "@cliffy/ansi/colors";
import { Command } from "@cliffy/command";
import { Confirm } from "@cliffy/prompt/confirm";
import * as log from "../../core/log.ts";
import { SETTINGS_MANAGED_ONLY_WHEN_PRESENT } from "../../core/settings.ts";
import * as path from "node:path";
import { sep as SEP } from "node:path";
import {
@@ -2576,6 +2577,14 @@ const isWorkspaceSettingsFile = (p: string) =>
function stripUnappliedSettingsFields(local: any, remote: any) {
delete local?.name;
delete remote?.name;
for (const key of ["operator_settings", ...SETTINGS_MANAGED_ONLY_WHEN_PRESENT]) {
if (local?.[key] === undefined) delete remote?.[key];
}
// push clears an omitted mute_critical_alerts to false
if (local?.mute_critical_alerts == null && remote?.mute_critical_alerts === false) {
delete local?.mute_critical_alerts;
delete remote.mute_critical_alerts;
}
if (local?.color == null) {
delete local?.color;
delete remote?.color;
@@ -3843,6 +3852,7 @@ export async function pull(
opts.skipDatatableMigrations,
opts.defaultTs,
opts.syncBehavior,
opts.includeDatatablePermissions,
);
const remote = ZipFSElement(
@@ -4946,6 +4956,7 @@ export async function push(
opts.skipDatatableMigrations,
opts.defaultTs,
opts.syncBehavior,
opts.includeDatatablePermissions,
))!,
!opts.json,
opts.defaultTs ?? "bun",
@@ -6049,6 +6060,7 @@ export async function push(
(opts.yes ?? false) || !process.stdin.isTTY,
skipReencrypt: opts.skipReencryptOnKeyChange,
},
includeDatatablePermissions: opts.includeDatatablePermissions,
defaultTs: opts.defaultTs,
},
);
@@ -6187,6 +6199,7 @@ export async function push(
(opts.yes ?? false) || !process.stdin.isTTY,
skipReencrypt: opts.skipReencryptOnKeyChange,
},
includeDatatablePermissions: opts.includeDatatablePermissions,
defaultTs: opts.defaultTs,
},
);
@@ -6815,6 +6828,10 @@ const command = new Command()
.option("--include-users", "Include syncing users")
.option("--include-groups", "Include syncing groups")
.option("--include-settings", "Include syncing workspace settings")
.option(
"--include-datatable-permissions",
"With --include-settings, also sync who may connect to each data table as which role",
)
.option("--include-key", "Include workspace encryption key")
.option(
"--keep-deleted",
@@ -6878,6 +6895,10 @@ const command = new Command()
.option("--include-users", "Include syncing users")
.option("--include-groups", "Include syncing groups")
.option("--include-settings", "Include syncing workspace settings")
.option(
"--include-datatable-permissions",
"With --include-settings, also sync who may connect to each data table as which role",
)
.option("--include-key", "Include workspace encryption key")
.option(
"--skip-reencrypt-on-key-change",