feat: harden data access and simplify user management
This commit is contained in:
1 parent
3dd5731751
commit
adb780bc82
51 files changed
+3239
-2121
No files matched your search
@@ -14,6 +14,14 @@ const serverComposeSource = readFileSync(
|
||||
"utf8",
|
||||
);
|
||||
const dockerfileSource = readFileSync(new URL("../../Dockerfile", import.meta.url), "utf8");
|
||||
const backupScriptSource = readFileSync(
|
||||
new URL("../../scripts/backup_mysql_encrypted.sh", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const storageGuardSource = readFileSync(
|
||||
new URL("../../docker/mysql-storage-guard.sh", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const dataCenterSource = readFileSync(
|
||||
new URL("../src/panels/data-platform/DataCenterPanel.tsx", import.meta.url),
|
||||
"utf8",
|
||||
@@ -30,11 +38,11 @@ test("Interface Center is available under System", () => {
|
||||
});
|
||||
|
||||
test("Interface Center is designed for inbound server access", () => {
|
||||
for (const label of ["DBeaver 管理接入", "HTTPS API", "接入信息", "安全设置"]) {
|
||||
for (const label of ["DBeaver 管理接入", "选择服务器数据库", "连接参数", "生成密钥与数据库账号"]) {
|
||||
assert.match(panelSource, new RegExp(label));
|
||||
}
|
||||
assert.match(panelSource, /SSH 隧道 \+ 独立 MySQL 账号/);
|
||||
assert.match(panelSource, /不要在 Main 页填写服务器公网 IP/);
|
||||
assert.match(panelSource, /SSH 加密隧道/);
|
||||
assert.match(panelSource, /Main 页不要填写服务器公网 IP/);
|
||||
assert.match(panelSource, /Show all databases/);
|
||||
assert.doesNotMatch(panelSource, /JDBC URL/);
|
||||
assert.doesNotMatch(panelSource, /测试连接/);
|
||||
@@ -62,28 +70,42 @@ test("production deployment fails closed and preserves recoverability", () => {
|
||||
assert.match(serverComposeSource, /SECURITY_STRICT_MODE: "true"/);
|
||||
assert.match(serverComposeSource, /API_BIND_HOST:-127\.0\.0\.1/);
|
||||
assert.match(serverComposeSource, /必须设置至少 32 位 AUTH_SECRET/);
|
||||
assert.match(serverComposeSource, /mysql-storage-guard/);
|
||||
assert.match(serverComposeSource, /MYSQL_STORAGE_MOUNT:\?必须设置 MYSQL_STORAGE_MOUNT/);
|
||||
assert.match(serverComposeSource, /MYSQL_DATA_DIR:\?必须设置 MYSQL_DATA_DIR/);
|
||||
assert.match(serverComposeSource, /create_host_path: false/);
|
||||
assert.match(serverComposeSource, /127\.0\.0\.1:\$\{MYSQL_PORT:-3307\}:3306/);
|
||||
assert.match(storageGuardSource, /storage identity mismatch/);
|
||||
assert.match(backupScriptSource, /拒绝将生产备份写入代码仓库/);
|
||||
assert.match(backupScriptSource, /verify_mysql_backup\.sh/);
|
||||
assert.match(backupScriptSource, /DATA_BACKUP_RETENTION_DAYS/);
|
||||
assert.match(dockerfileSource, /USER appuser/);
|
||||
assert.match(dataCenterSource, /安全只读模式/);
|
||||
});
|
||||
|
||||
test("Interface Center keeps clients, credentials, policies, docs and logs", () => {
|
||||
for (const label of ["客户端与密钥", "权限策略", "接口说明", "调用日志"]) {
|
||||
test("Interface Center keeps the DBeaver flow focused", () => {
|
||||
for (const label of ["私钥不会上传", "一人一号、单库授权", "一键撤销", "复制全部连接参数"]) {
|
||||
assert.match(panelSource, new RegExp(label));
|
||||
}
|
||||
assert.match(panelSource, /完整密钥只在签发时显示一次/);
|
||||
assert.match(panelSource, /留空默认 30 天,最长 90 天/);
|
||||
assert.match(panelSource, /加密备份待部署/);
|
||||
assert.match(panelSource, /接口不接受任意 SQL/);
|
||||
for (const removedLabel of ["HTTPS API", "客户端与密钥", "权限策略", "调用日志"]) {
|
||||
assert.doesNotMatch(panelSource, new RegExp(removedLabel));
|
||||
}
|
||||
});
|
||||
|
||||
test("frontend uses the administrative Interface Center API", () => {
|
||||
test("frontend uses only the managed DBeaver endpoints on this page", () => {
|
||||
for (const path of [
|
||||
"/interface-center/summary",
|
||||
"/interface-center/clients",
|
||||
"/interface-center/credentials",
|
||||
"/interface-center/policies",
|
||||
"/interface-center/logs",
|
||||
"/interface-center/catalog",
|
||||
"/interface-center/dbeaver-access",
|
||||
]) {
|
||||
assert.match(apiSource, new RegExp(path));
|
||||
}
|
||||
for (const unusedCall of [
|
||||
"listInterfaceClients",
|
||||
"listInterfaceCredentials",
|
||||
"listInterfacePolicies",
|
||||
"listInterfaceCallLogs",
|
||||
]) {
|
||||
assert.doesNotMatch(panelSource, new RegExp(unusedCall));
|
||||
}
|
||||
});
|
||||
@@ -37,20 +37,31 @@ test("retired plaza pages are absent while the core plaza remains", () => {
|
||||
});
|
||||
|
||||
test("retired system pages and the notification bell are absent", () => {
|
||||
for (const page of ["areas", "notifications", "settings"]) {
|
||||
for (const page of ["areas", "notifications", "settings", "agents", "logs", "permissions"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(`/admin/system/${page}`));
|
||||
assert.doesNotMatch(appSource, new RegExp(`key: "/system/${page}`));
|
||||
}
|
||||
|
||||
for (const component of ["AreaManagement", "Notifications", "AgentSettings", "NotificationBell"]) {
|
||||
for (const component of [
|
||||
"AreaManagement",
|
||||
"Notifications",
|
||||
"AgentSettings",
|
||||
"AgentTasksPanel",
|
||||
"LogsPanel",
|
||||
"PermissionsAudit",
|
||||
"NotificationBell",
|
||||
]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(component));
|
||||
}
|
||||
|
||||
assert.match(appSource, /key: "\/system\/users"[\s\S]*label: "用户管理"/);
|
||||
assert.match(appSource, /path="\/admin\/system\/users"/);
|
||||
});
|
||||
|
||||
test("the data center uses its own full-width workbench surface", () => {
|
||||
assert.match(appSource, /const showDataCenterWorkbench = location\.pathname === "\/admin\/data"/);
|
||||
assert.match(appSource, /\{!showDataCenterWorkbench && \(\s*<Sider/);
|
||||
assert.match(appSource, /path="\/admin\/data" element=\{<DataCenterPanel \/>\}/);
|
||||
assert.match(appSource, /path="\/admin\/data" element=\{<DataCenterPanel canManage=\{isDataManager\} \/>\}/);
|
||||
});
|
||||
|
||||
test("the data center entry opens the standalone URL in a new browser tab", () => {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { generateDbeaverSshKeyPair } from "../src/sshKey.ts";
|
||||
|
||||
test("DBeaver SSH private key is generated locally in a compatible PEM format", async () => {
|
||||
const result = await generateDbeaverSshKeyPair("Alice Admin");
|
||||
|
||||
assert.match(result.privateKeyPem, /^-----BEGIN PRIVATE KEY-----/);
|
||||
assert.match(result.privateKeyPem, /-----END PRIVATE KEY-----\n$/);
|
||||
assert.match(result.publicKey, /^ssh-rsa [A-Za-z0-9+/]+=* nianxx-Alice_Admin$/);
|
||||
assert.match(result.privateKeyFilename, /\.pem$/);
|
||||
assert.match(result.publicKeyFilename, /\.pub$/);
|
||||
assert.equal(result.publicKey.includes("PRIVATE KEY"), false);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const appSource = readFileSync(new URL("../src/App.tsx", import.meta.url), "utf8");
|
||||
const panelSource = readFileSync(
|
||||
new URL("../src/panels/system/UserManagement.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const apiSource = readFileSync(new URL("../src/api.ts", import.meta.url), "utf8");
|
||||
const dataCenterSource = readFileSync(
|
||||
new URL("../src/panels/data-platform/DataCenterPanel.tsx", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
test("System keeps one focused user-management page", () => {
|
||||
assert.match(appSource, /key: "\/system\/users"[\s\S]*label: "用户管理"/);
|
||||
assert.match(appSource, /path="\/admin\/system\/users"/);
|
||||
for (const path of ["agents", "logs", "permissions"]) {
|
||||
assert.doesNotMatch(appSource, new RegExp(`/admin/system/${path}`));
|
||||
}
|
||||
});
|
||||
|
||||
test("User management exposes exactly two understandable data permissions", () => {
|
||||
for (const label of ["数据管理", "数据查看", "仅可查看和导出数据", "不能进行任何写入"]) {
|
||||
assert.match(panelSource, new RegExp(label));
|
||||
}
|
||||
assert.match(panelSource, /type DataPermission = "data_manage" \| "data_view"/);
|
||||
assert.doesNotMatch(panelSource, /listRoles|角色|权限矩阵|新增能力项|deleteUser/);
|
||||
});
|
||||
|
||||
test("User removal is replaced by reversible account disabling", () => {
|
||||
assert.match(panelSource, /确认停用该用户/);
|
||||
assert.match(panelSource, /立即失去登录权限/);
|
||||
assert.doesNotMatch(apiSource, /api\.delete\(`\/users/);
|
||||
});
|
||||
|
||||
test("Data-view accounts receive a read-only data-center surface", () => {
|
||||
assert.match(appSource, /<DataCenterPanel canManage=\{isDataManager\}/);
|
||||
assert.match(dataCenterSource, /!canManage[\s\S]*\? "只读权限"/);
|
||||
assert.match(dataCenterSource, /canManage && \(/);
|
||||
assert.match(dataCenterSource, /const canEdit = canManage && field\.editable/);
|
||||
});
|
||||
Reference in new issue
Block a user