feat(connection): 新增生产连接只读保护

This commit is contained in:
Syngnat
2026-06-23 15:33:11 +08:00
parent 3205d131c9
commit b0a9a995fb
30 changed files with 776 additions and 19 deletions

View File

@@ -1387,6 +1387,7 @@ const ConnectionModal: React.FC<{
user: config.user,
password: config.password,
database: config.database,
readOnly: config.readOnly === true,
uri: config.uri || "",
connectionParams:
config.connectionParams ||

View File

@@ -539,7 +539,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const supportsCopyInsert = dataSourceCaps.supportsCopyInsert;
const supportsSqlQueryExport = dataSourceCaps.supportsSqlQueryExport;
const isQueryResultExport = exportScope === 'queryResult';
const canImport = exportScope === 'table' && !!tableName;
const canImport = exportScope === 'table' && !!tableName && !readOnly;
const canExport = !!connectionId && (isQueryResultExport || !!tableName);
const canViewDdl = exportScope === 'table' && !!connectionId && !!tableName;
const canOpenObjectDesigner = exportScope === 'table' && objectType === 'table' && !!connectionId && !!tableName;

View File

@@ -8,6 +8,7 @@ import { TabData, ColumnDefinition } from '../types';
import { useStore } from '../store';
import { DBQuery, DBQueryWithCancel, DBQueryMulti, DBQueryMultiTransactional, DBGetTables, DBGetAllColumns, DBGetDatabases, DBGetColumns, CancelQuery, GenerateQueryID, WriteSQLFile, ExportSQLFile } from '../../wailsjs/go/app/App';
import { GONAVI_ROW_KEY } from './DataGrid';
import { findConnectionMutatingStatements } from '../utils/connectionReadOnly';
import { getDataSourceCapabilities, shouldShowOceanBaseRowNumberColumn } from '../utils/dataSourceCapabilities';
import { applyMongoQueryAutoLimit, convertMongoShellToJsonCommand } from "../utils/mongodb";
import { getShortcutDisplayLabel, getShortcutPlatform, getShortcutPrimaryModifierDisplayLabel, isEditableElement, isImeComposingKeyEvent, isShortcutMatch, comboToMonacoKeyBinding, resolveShortcutBinding } from "../utils/shortcuts";
@@ -3003,13 +3004,18 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
return;
}
const connCaps = getDataSourceCapabilities(conn.config);
if (!connCaps.supportsQueryEditor) {
message.error(translate('query_editor.message.unsupported_source'));
if (runSeqRef.current === runSeq) setLoading(false);
return;
}
if (!connCaps.supportsQueryEditor) {
message.error(translate('query_editor.message.unsupported_source'));
if (runSeqRef.current === runSeq) setLoading(false);
return;
}
if (findConnectionMutatingStatements(conn.config, executableSQL).length > 0) {
message.warning(translate('query_editor.message.connection_readonly_blocked'));
if (runSeqRef.current === runSeq) setLoading(false);
return;
}
const config = {
const config = {
...conn.config,
port: Number(conn.config.port),
password: conn.config.password || "",

View File

@@ -37,6 +37,7 @@ import {
import ConnectionModalMongoSections from "../ConnectionModalMongoSections";
import ConnectionModalRedisSections from "../ConnectionModalRedisSections";
import { t } from "../../i18n";
import { supportsConnectionReadOnlyMode } from "../../utils/connectionReadOnly";
import {
getConnectionConfigLayoutKindLabel,
getStoredSecretPlaceholder,
@@ -192,6 +193,11 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
} = props;
const renderStep2 = () => {
const showConnectionReadOnlyField = supportsConnectionReadOnlyMode({
type: dbType,
driver: form.getFieldValue("driver"),
oceanBaseProtocol,
});
const baseInfoSection = (
<div style={modalInnerSectionStyle}>
<div
@@ -1339,6 +1345,27 @@ const renderStep2 = () => {
),
})}
{showConnectionReadOnlyField &&
renderConfigSectionCard({
sectionKey: "readOnly",
icon: <SafetyCertificateOutlined />,
children: (
<Form.Item
name="readOnly"
label={t("connection.modal.field.readOnly.label")}
help={t("connection.modal.field.readOnly.help")}
valuePropName="checked"
style={{ marginBottom: 0 }}
>
<Checkbox
onChange={() => clearConnectionTestResultForChoice()}
>
{t("connection.modal.field.readOnly.checkbox")}
</Checkbox>
</Form.Item>
),
})}
{isMySQLLike &&
renderConfigSectionCard({
sectionKey: "connectionMode",
@@ -2306,6 +2333,7 @@ const renderStep2 = () => {
keepAliveIntervalMinutes: 240,
uri: "",
connectionParams: "",
readOnly: false,
oceanBaseProtocol: "mysql",
mysqlTopology: "single",
rocketmqTopology: "single",

View File

@@ -97,4 +97,29 @@ describe("connectionModalConfig keepalive", () => {
expect(config.keepAliveEnabled).toBe(false);
expect(config.keepAliveIntervalMinutes).toBe(15);
});
it("persists readOnly only for datasource types that support production guard", async () => {
const sqlConfig = await buildConnectionConfig({
values: {
...buildBaseValues(),
type: "postgres",
readOnly: true,
},
forPersist: true,
translate,
});
const redisConfig = await buildConnectionConfig({
values: {
...buildBaseValues(),
type: "redis",
port: 6379,
readOnly: true,
},
forPersist: true,
translate,
});
expect(sqlConfig.readOnly).toBe(true);
expect(redisConfig.readOnly).toBe(false);
});
});

View File

@@ -1,4 +1,5 @@
import type { ConnectionConfig, SavedConnection } from "../../types";
import { supportsConnectionReadOnlyMode } from "../../utils/connectionReadOnly";
import { resolveConnectionSecretDraft } from "../../utils/connectionSecretDraft";
import {
getConnectionTypeDefaultPort as getDefaultPortByType,
@@ -805,6 +806,12 @@ export const buildConnectionConfig = async ({
password: keepPassword ? mergedValues.password || "" : "",
savePassword: savePassword,
database: mergedValues.database || "",
readOnly:
supportsConnectionReadOnlyMode({
type,
driver: mergedValues.driver,
oceanBaseProtocol: selectedOceanBaseProtocol,
}) && mergedValues.readOnly === true,
useSSL: effectiveUseSSL,
sslMode: effectiveUseSSL ? sslMode : "disable",
sslCAPath: sslCAPath,