mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-21 16:34:21 +08:00
🐛 fix(nacos-connection): 支持受限命名空间与权限保护
- 在连接表单中独立维护 Namespace ID 与 contextPath - 为无命名空间列表权限的账号回退到已配置作用域 - 细分数据编辑、结构管理与导入保护并阻止陈旧操作 - 补齐侧边栏请求代际和连接 URI 回归测试
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
isSingleReadOnlyConnectionQuery,
|
||||
resolveConnectionProtectionConfig,
|
||||
supportsConnectionKeepAliveSQL,
|
||||
supportsConnectionReadOnlyMode,
|
||||
} from './connectionReadOnly';
|
||||
|
||||
describe('connectionReadOnly', () => {
|
||||
@@ -28,6 +29,13 @@ describe('connectionReadOnly', () => {
|
||||
expect(supportsConnectionKeepAliveSQL({ type: 'redis' } as any)).toBe(false);
|
||||
});
|
||||
|
||||
it('supports Nacos production protection without treating it as a SQL keepalive source', () => {
|
||||
const config = { type: 'nacos' } as any;
|
||||
|
||||
expect(supportsConnectionReadOnlyMode(config)).toBe(true);
|
||||
expect(supportsConnectionKeepAliveSQL(config)).toBe(false);
|
||||
});
|
||||
|
||||
it('maps legacy readOnly connections to the full production protection set', () => {
|
||||
expect(resolveConnectionProtectionConfig({
|
||||
type: 'postgres',
|
||||
|
||||
@@ -65,6 +65,11 @@ const CONNECTION_READ_ONLY_TYPES = new Set([
|
||||
"mongodb",
|
||||
]);
|
||||
|
||||
const CONNECTION_PROTECTION_TYPES = new Set([
|
||||
...CONNECTION_READ_ONLY_TYPES,
|
||||
"nacos",
|
||||
]);
|
||||
|
||||
export const MAX_CONNECTION_KEEPALIVE_SQL_LENGTH = 4096;
|
||||
|
||||
const SQL_READ_ONLY_KEYWORDS = new Set([
|
||||
@@ -446,7 +451,7 @@ export const isSingleReadOnlyConnectionQuery = (
|
||||
export const supportsConnectionReadOnlyMode = (
|
||||
config: ConnectionReadOnlyLike,
|
||||
): boolean => {
|
||||
return CONNECTION_READ_ONLY_TYPES.has(resolveConnectionReadOnlyType(config));
|
||||
return CONNECTION_PROTECTION_TYPES.has(resolveConnectionReadOnlyType(config));
|
||||
};
|
||||
|
||||
export const normalizeConnectionProtectionConfig = (
|
||||
|
||||
@@ -220,6 +220,40 @@ describe('buildRpcConnectionConfig', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves Nacos legacy readOnly for backend mutation guards', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-nacos-readonly',
|
||||
type: 'nacos',
|
||||
host: 'nacos.local',
|
||||
port: 8848,
|
||||
readOnly: true,
|
||||
} as any);
|
||||
|
||||
expect(result.readOnly).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves explicit Nacos protection flags for backend mutation guards', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-nacos-protected',
|
||||
type: 'nacos',
|
||||
host: 'nacos.local',
|
||||
port: 8848,
|
||||
protection: {
|
||||
restrictDataEdit: true,
|
||||
restrictStructureEdit: true,
|
||||
restrictDataImport: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
expect(result.readOnly).toBe(false);
|
||||
expect(result.protection).toEqual({
|
||||
restrictDataEdit: true,
|
||||
restrictStructureEdit: true,
|
||||
restrictScriptExecution: false,
|
||||
restrictDataImport: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores the legacy readOnly flag for unsupported connection types', () => {
|
||||
const result = buildRpcConnectionConfig({
|
||||
id: 'conn-redis-readonly',
|
||||
|
||||
57
frontend/src/utils/nacosConnectionScope.test.ts
Normal file
57
frontend/src/utils/nacosConnectionScope.test.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
extractNacosConnectionScope,
|
||||
resolveNacosConnectionScope,
|
||||
setNacosConnectionScope,
|
||||
} from "./nacosConnectionScope";
|
||||
|
||||
describe("nacosConnectionScope", () => {
|
||||
it("keeps an explicitly configured public namespace distinct from no scope", () => {
|
||||
expect(resolveNacosConnectionScope("contextPath=/nacos")).toEqual({
|
||||
configured: false,
|
||||
namespaceId: "",
|
||||
requestNamespaceId: "",
|
||||
});
|
||||
expect(
|
||||
resolveNacosConnectionScope(
|
||||
"contextPath=%2Fnacos&namespaceId=public",
|
||||
),
|
||||
).toEqual({
|
||||
configured: true,
|
||||
namespaceId: "public",
|
||||
requestNamespaceId: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("sets and clears a namespace without dropping other Nacos parameters", () => {
|
||||
const scoped = setNacosConnectionScope(
|
||||
"contextPath=/registry&custom=value",
|
||||
" team/dev ",
|
||||
);
|
||||
|
||||
expect(new URLSearchParams(scoped).get("contextPath")).toBe("/registry");
|
||||
expect(new URLSearchParams(scoped).get("custom")).toBe("value");
|
||||
expect(new URLSearchParams(scoped).get("namespaceId")).toBe("team/dev");
|
||||
|
||||
const cleared = setNacosConnectionScope(scoped, "");
|
||||
expect(new URLSearchParams(cleared).get("contextPath")).toBe("/registry");
|
||||
expect(new URLSearchParams(cleared).get("custom")).toBe("value");
|
||||
expect(new URLSearchParams(cleared).has("namespaceId")).toBe(false);
|
||||
});
|
||||
|
||||
it("extracts the dedicated scope from the advanced connection parameters", () => {
|
||||
expect(
|
||||
extractNacosConnectionScope(
|
||||
"contextPath=/nacos;namespaceId=dev-team\ncustom=value",
|
||||
),
|
||||
).toEqual({
|
||||
connectionParams: "contextPath=%2Fnacos&custom=value",
|
||||
scope: {
|
||||
configured: true,
|
||||
namespaceId: "dev-team",
|
||||
requestNamespaceId: "dev-team",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
73
frontend/src/utils/nacosConnectionScope.ts
Normal file
73
frontend/src/utils/nacosConnectionScope.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export const NACOS_NAMESPACE_ID_PARAM = "namespaceId";
|
||||
|
||||
export type NacosConnectionScope = {
|
||||
configured: boolean;
|
||||
namespaceId: string;
|
||||
requestNamespaceId: string;
|
||||
};
|
||||
|
||||
const normalizeNacosConnectionParamsText = (raw: unknown): string => {
|
||||
let text = String(raw ?? "").trim();
|
||||
const queryIndex = text.indexOf("?");
|
||||
if (queryIndex >= 0) {
|
||||
text = text.slice(queryIndex + 1);
|
||||
}
|
||||
const hashIndex = text.indexOf("#");
|
||||
if (hashIndex >= 0) {
|
||||
text = text.slice(0, hashIndex);
|
||||
}
|
||||
return text
|
||||
.replace(/^[?&]+/, "")
|
||||
.replace(/[;\r\n]+/g, "&")
|
||||
.trim();
|
||||
};
|
||||
|
||||
const parseNacosConnectionParams = (raw: unknown): URLSearchParams =>
|
||||
new URLSearchParams(normalizeNacosConnectionParamsText(raw));
|
||||
|
||||
export const resolveNacosConnectionScope = (
|
||||
connectionParams: unknown,
|
||||
): NacosConnectionScope => {
|
||||
const params = parseNacosConnectionParams(connectionParams);
|
||||
const namespaceId = String(
|
||||
params.get(NACOS_NAMESPACE_ID_PARAM) ?? "",
|
||||
).trim();
|
||||
const configured = namespaceId !== "";
|
||||
return {
|
||||
configured,
|
||||
namespaceId,
|
||||
requestNamespaceId:
|
||||
configured && namespaceId.toLowerCase() !== "public"
|
||||
? namespaceId
|
||||
: "",
|
||||
};
|
||||
};
|
||||
|
||||
export const setNacosConnectionScope = (
|
||||
connectionParams: unknown,
|
||||
namespaceId: unknown,
|
||||
): string => {
|
||||
const params = parseNacosConnectionParams(connectionParams);
|
||||
const normalizedNamespaceId = String(namespaceId ?? "").trim();
|
||||
if (normalizedNamespaceId) {
|
||||
params.set(NACOS_NAMESPACE_ID_PARAM, normalizedNamespaceId);
|
||||
} else {
|
||||
params.delete(NACOS_NAMESPACE_ID_PARAM);
|
||||
}
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
export const extractNacosConnectionScope = (
|
||||
connectionParams: unknown,
|
||||
): {
|
||||
connectionParams: string;
|
||||
scope: NacosConnectionScope;
|
||||
} => {
|
||||
const params = parseNacosConnectionParams(connectionParams);
|
||||
const scope = resolveNacosConnectionScope(params.toString());
|
||||
params.delete(NACOS_NAMESPACE_ID_PARAM);
|
||||
return {
|
||||
connectionParams: params.toString(),
|
||||
scope,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user