From 9e3013adf9e348596be04af9d4e308c6e52ab7a6 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Sun, 9 Aug 2026 15:51:04 +0800 Subject: [PATCH] fix(mongodb): support createUser shell commands Closes #5639 --- .../java/com/dbx/agent/AgentProtocol.java | 2 + .../src/main/resources/agent-protocol-v1.json | 2 +- .../src/main/resources/agent-protocol-v2.json | 2 +- .../com/dbx/agent/mongodb/MongoAgent.java | 27 +++++++ .../com/dbx/agent/mongodb/MongoAgentTest.java | 27 +++++++ apps/desktop/src/lib/backend/api.ts | 1 + apps/desktop/src/lib/backend/http.ts | 9 +++ apps/desktop/src/lib/backend/tauri.ts | 9 +++ .../src/lib/mongo/mongoShellCommand.ts | 34 ++++++++- apps/desktop/src/stores/queryStore.ts | 8 +- crates/dbx-core/assets/agent-protocol-v1.json | 2 +- crates/dbx-core/assets/agent-protocol-v2.json | 2 +- crates/dbx-core/src/db/agent_driver.rs | 13 +++- crates/dbx-core/src/db/mongo_driver.rs | 71 ++++++++++++++++++ crates/dbx-core/src/mongo_ops.rs | 37 ++++++++++ crates/dbx-core/src/mongo_shell.rs | 73 ++++++++++++++++++- crates/dbx-core/src/query.rs | 2 +- crates/dbx-mcp/src/backend.rs | 18 +++++ crates/dbx-web/src/main.rs | 1 + crates/dbx-web/src/routes/mongo.rs | 29 ++++++++ packages/app-tests/mongoShellCommand.test.ts | 36 +++++++++ packages/app-tests/queryStore.test.ts | 54 ++++++++++++++ packages/mongo-shell/src/aggregate.ts | 3 +- src-tauri/src/commands/mongo_cmd.rs | 30 ++++++++ src-tauri/src/lib.rs | 1 + 25 files changed, 481 insertions(+), 12 deletions(-) diff --git a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java index c0e340aa9f..541207ecec 100644 --- a/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java +++ b/agents/common/src/main/java/com/dbx/agent/AgentProtocol.java @@ -58,6 +58,7 @@ public final class AgentProtocol { public static final String MONGO_METHOD_COUNT_DOCUMENTS = "count_documents"; public static final String MONGO_METHOD_SERVER_VERSION = "server_version"; public static final String MONGO_METHOD_CREATE_INDEX = "create_index"; + public static final String MONGO_METHOD_CREATE_USER = "create_user"; public static final String MONGO_METHOD_DROP_INDEXES = "drop_indexes"; public static final String MONGO_METHOD_DROP_COLLECTION = "drop_collection"; public static final String MONGO_METHOD_DROP_DATABASE = "drop_database"; @@ -243,6 +244,7 @@ public final class AgentProtocol { MONGO_METHOD_COUNT_DOCUMENTS, MONGO_METHOD_SERVER_VERSION, MONGO_METHOD_CREATE_INDEX, + MONGO_METHOD_CREATE_USER, MONGO_METHOD_DROP_INDEXES, MONGO_METHOD_DROP_COLLECTION, MONGO_METHOD_DROP_DATABASE, diff --git a/agents/common/src/main/resources/agent-protocol-v1.json b/agents/common/src/main/resources/agent-protocol-v1.json index 999efd3e50..13077d2ee0 100644 --- a/agents/common/src/main/resources/agent-protocol-v1.json +++ b/agents/common/src/main/resources/agent-protocol-v1.json @@ -39,6 +39,6 @@ "disconnect", "shutdown" ], - "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], + "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], "kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"] } diff --git a/agents/common/src/main/resources/agent-protocol-v2.json b/agents/common/src/main/resources/agent-protocol-v2.json index 39f73d3da5..76799d87e5 100644 --- a/agents/common/src/main/resources/agent-protocol-v2.json +++ b/agents/common/src/main/resources/agent-protocol-v2.json @@ -43,7 +43,7 @@ "disconnect", "shutdown" ], - "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], + "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], "kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"], "sessionField": "agentSessionId", "cursorSessionField": "sessionId" diff --git a/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java b/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java index beb9bd015c..75da439d66 100644 --- a/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java +++ b/agents/drivers/mongodb/src/main/java/com/dbx/agent/mongodb/MongoAgent.java @@ -1003,6 +1003,32 @@ public final class MongoAgent { return Collections.singletonMap("name", name); } + private static Object createUser(JsonObject params) { + MongoClient client = requireClient(); + String database = params.get("database").getAsString(); + client.getDatabase(database).runCommand(buildCreateUserCommand(params)); + return Collections.singletonMap("affected_rows", 1); + } + + static Document buildCreateUserCommand(JsonObject params) { + Document user = requiredDocument(params, "user_json", "User document"); + Object username = user.remove("user"); + if (!(username instanceof String) || ((String) username).isBlank()) { + throw new IllegalArgumentException("MongoDB createUser requires a non-empty user name"); + } + if (user.containsKey("createUser") || user.containsKey("writeConcern")) { + throw new IllegalArgumentException("MongoDB createUser user document contains reserved command fields"); + } + + Document command = new Document("createUser", username); + command.putAll(user); + Document writeConcern = documentOrNull(params, "write_concern_json"); + if (writeConcern != null) { + command.put("writeConcern", writeConcern); + } + return command; + } + private static Document requiredDocument(JsonObject params, String key, String label) { Document document = documentOrNull(params, key); if (document == null) { @@ -1562,6 +1588,7 @@ public final class MongoAgent { case AgentProtocol.MONGO_METHOD_COUNT_DOCUMENTS -> countDocuments(params); case AgentProtocol.MONGO_METHOD_SERVER_VERSION -> serverVersion(params); case AgentProtocol.MONGO_METHOD_CREATE_INDEX -> createIndex(params); + case AgentProtocol.MONGO_METHOD_CREATE_USER -> createUser(params); case AgentProtocol.MONGO_METHOD_DROP_INDEXES -> dropIndexes(params); case AgentProtocol.MONGO_METHOD_DROP_COLLECTION -> dropCollection(params); case AgentProtocol.MONGO_METHOD_DROP_DATABASE -> dropDatabase(params); diff --git a/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java b/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java index eb4e9326fb..fef3f252a5 100644 --- a/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java +++ b/agents/drivers/mongodb/src/test/java/com/dbx/agent/mongodb/MongoAgentTest.java @@ -521,6 +521,33 @@ class MongoAgentTest { assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_CREATE_INDEX)); } + @Test + void createUserMethodIsRecognizedAndBuildsTheExpectedCommand() { + String response = MongoAgent.handleRequest( + "{\"jsonrpc\":\"2.0\",\"id\":121,\"method\":\"create_user\"," + + "\"params\":{\"database\":\"admin\"," + + "\"user_json\":\"{\\\"user\\\":\\\"test-db\\\",\\\"pwd\\\":\\\"test-password\\\",\\\"roles\\\":[{\\\"role\\\":\\\"readWrite\\\",\\\"db\\\":\\\"db1\\\"}]}\"}}" + ); + + JsonObject json = JsonParser.parseString(response).getAsJsonObject(); + assertEquals(121, json.get("id").getAsInt()); + assertEquals("Not connected", json.getAsJsonObject("error").get("message").getAsString()); + assertFalse(json.getAsJsonObject("error").get("message").getAsString().contains("Unknown method")); + assertTrue(AgentProtocol.MONGO_LEGACY_METHODS.contains(AgentProtocol.MONGO_METHOD_CREATE_USER)); + + JsonObject params = JsonParser.parseString( + "{\"database\":\"admin\"," + + "\"user_json\":\"{\\\"user\\\":\\\"test-db\\\",\\\"pwd\\\":\\\"test-password\\\",\\\"roles\\\":[{\\\"role\\\":\\\"readWrite\\\",\\\"db\\\":\\\"db1\\\"}]}\"," + + "\"write_concern_json\":\"{\\\"w\\\":\\\"majority\\\"}\"}" + ).getAsJsonObject(); + Document command = MongoAgent.buildCreateUserCommand(params); + assertEquals("createUser", command.keySet().iterator().next()); + assertEquals("test-db", command.getString("createUser")); + assertEquals("test-password", command.getString("pwd")); + assertEquals("readWrite", command.getList("roles", Document.class).get(0).getString("role")); + assertEquals("majority", command.get("writeConcern", Document.class).getString("w")); + } + @Test void defaultIndexNameMatchesNativeDriverForWholeDoubles() { assertEquals( diff --git a/apps/desktop/src/lib/backend/api.ts b/apps/desktop/src/lib/backend/api.ts index ad9c4ed008..54d7b44474 100644 --- a/apps/desktop/src/lib/backend/api.ts +++ b/apps/desktop/src/lib/backend/api.ts @@ -581,6 +581,7 @@ export const mongoAggregateDocuments = forward("mongoAggregateDocuments"); export const mongoDistinct = forward("mongoDistinct"); export const mongoCollectionStats = forward("mongoCollectionStats"); export const mongoCreateIndex = forward("mongoCreateIndex"); +export const mongoCreateUser = forward("mongoCreateUser"); export const mongoDropIndexes = forward("mongoDropIndexes"); export const documentInsertDocument = forward("documentInsertDocument"); export const mongoInsertDocument = forward("mongoInsertDocument"); diff --git a/apps/desktop/src/lib/backend/http.ts b/apps/desktop/src/lib/backend/http.ts index dfa586a2b9..912fd0e7b5 100644 --- a/apps/desktop/src/lib/backend/http.ts +++ b/apps/desktop/src/lib/backend/http.ts @@ -3202,6 +3202,15 @@ export async function mongoCreateIndex(connectionId: string, database: string, c }); } +export async function mongoCreateUser(connectionId: string, database: string, userJson: string, writeConcernJson?: string): Promise<{ affected_rows: number }> { + return post("/api/mongo/create-user", { + connectionId, + database, + userJson, + writeConcernJson, + }); +} + export async function mongoDropIndexes(connectionId: string, database: string, collection: string, indexesJson?: string, single = false): Promise { return post("/api/mongo/drop-indexes", { connectionId, diff --git a/apps/desktop/src/lib/backend/tauri.ts b/apps/desktop/src/lib/backend/tauri.ts index 633423327c..3cda6a2628 100644 --- a/apps/desktop/src/lib/backend/tauri.ts +++ b/apps/desktop/src/lib/backend/tauri.ts @@ -3136,6 +3136,15 @@ export async function mongoCreateIndex(connectionId: string, database: string, c }); } +export async function mongoCreateUser(connectionId: string, database: string, userJson: string, writeConcernJson?: string): Promise<{ affected_rows: number }> { + return invoke("mongo_create_user", { + connectionId, + database, + userJson, + writeConcernJson, + }); +} + export async function mongoDropIndexes(connectionId: string, database: string, collection: string, indexesJson?: string, single = false): Promise { return invoke("mongo_drop_indexes", { connectionId, diff --git a/apps/desktop/src/lib/mongo/mongoShellCommand.ts b/apps/desktop/src/lib/mongo/mongoShellCommand.ts index 08a5757201..90375653cd 100644 --- a/apps/desktop/src/lib/mongo/mongoShellCommand.ts +++ b/apps/desktop/src/lib/mongo/mongoShellCommand.ts @@ -9,6 +9,7 @@ import { normalizeJsonArgument, parseCollectionMethodTarget, parseMongoAggregateCommand, + parseMongoObjectArgument, quoteUnquotedObjectKeys, splitTopLevel, type MongoAggregateCommand, @@ -61,6 +62,11 @@ export interface MongoVersionCommand { kind: "version"; } +export interface MongoCreateUserCommand { + userJson: string; + writeConcernJson?: string; +} + export type MongoCollectionStatsMetric = "stats" | "dataSize" | "storageSize" | "totalIndexSize"; export interface MongoCollectionStatsCommand { @@ -75,7 +81,7 @@ export interface MongoDistinctCommand { filter?: string; } -type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete"; +type MongoWriteKind = "insert" | "update" | "delete" | "createIndex" | "createUser" | "dropIndex" | "dropIndexes" | "dropCollection" | "findOneAndUpdate" | "findOneAndReplace" | "findOneAndDelete"; export type MongoCommand = | ({ kind: "find" } & MongoFindCommand) @@ -87,6 +93,7 @@ export type MongoCommand = | ({ kind: "getIndexes" } & MongoGetIndexesCommand) | ({ kind: "collectionStats" } & MongoCollectionStatsCommand) | ({ kind: "use" } & MongoUseCommand) + | ({ kind: "createUser" } & MongoCreateUserCommand) | { kind: "insert"; collection: string; docsJson: string } | { kind: "update"; collection: string; filter: string; update: string; options?: string; many: boolean } | { kind: "delete"; collection: string; filter: string; many: boolean } @@ -471,6 +478,27 @@ export function parseMongoVersionCommand(input: string): MongoVersionCommand | n return /^db\s*\.\s*version\s*\(\s*\)$/i.test(source) ? { kind: "version" } : null; } +export function parseMongoCreateUserCommand(input: string): MongoCreateUserCommand | null { + const source = input.trim().replace(/;$/, "").trim(); + const match = /^db\s*\.\s*createUser\s*\(/i.exec(source); + if (!match) return null; + const openIndex = source.indexOf("(", match.index); + const closeIndex = findMatchingParen(source, openIndex); + if (closeIndex < 0 || source.slice(closeIndex + 1).trim()) return null; + const args = splitTopLevel(source.slice(openIndex + 1, closeIndex)); + if (args.length < 1 || args.length > 2) return null; + const userJson = parseMongoObjectArgument(args[0]); + if (!userJson) return null; + const user = JSON.parse(userJson) as Record; + if (typeof user.user !== "string" || !user.user.trim()) return null; + const writeConcernJson = args[1]?.trim() ? parseMongoObjectArgument(args[1]) : undefined; + if (args[1]?.trim() && !writeConcernJson) return null; + return { + userJson: JSON.stringify(user), + ...(writeConcernJson ? { writeConcernJson: JSON.stringify(JSON.parse(writeConcernJson)) } : {}), + }; +} + export function parseMongoWriteCommand(input: string): MongoWriteCommand | null { const source = input.trim().replace(/;$/, "").trim(); const insertOne = parseCollectionMethodTarget(source, "insertOne"); @@ -575,6 +603,10 @@ export function parseMongoCommand(input: string): ParsedMongoCommand | null { const version = parseMongoVersionCommand(source); return version ?? null; }, + (source) => { + const createUser = parseMongoCreateUserCommand(source); + return createUser ? { kind: "createUser", ...createUser } : null; + }, (source) => { // Legacy Mongo shell uses count()/find().count(); keep accepting it // while mapping to DBX's countDocuments-compatible result path. diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index de62a0fe2c..8ac825e471 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -4043,6 +4043,7 @@ export const useQueryStore = defineStore("query", () => { case "update": case "delete": case "createIndex": + case "createUser": case "dropIndex": case "dropIndexes": case "dropCollection": { @@ -4054,7 +4055,7 @@ export const useQueryStore = defineStore("query", () => { traceId, database: currentDatabase, kind: mongoCommand.kind, - collection: mongoCommand.collection, + ...(mongoCommand.kind === "createUser" ? {} : { collection: mongoCommand.collection }), }); mongoEditTarget = undefined; if (mongoCommand.kind === "insert") { @@ -4066,6 +4067,9 @@ export const useQueryStore = defineStore("query", () => { } else if (mongoCommand.kind === "createIndex") { const result = await api.mongoCreateIndex(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.keys, mongoCommand.options); allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoCreateIndexToQueryResult(result.name, performance.now() - commandStartedAt)))); + } else if (mongoCommand.kind === "createUser") { + const result = await api.mongoCreateUser(executionConnectionId, currentDatabase, mongoCommand.userJson, mongoCommand.writeConcernJson); + allResults.push(markQueryResultRowsRaw(annotateMongoResult(mongoWriteToQueryResult(result.affected_rows, performance.now() - commandStartedAt)))); } else if (mongoCommand.kind === "dropIndex" || mongoCommand.kind === "dropIndexes") { try { const result = await api.mongoDropIndexes(executionConnectionId, currentDatabase, mongoCommand.collection, mongoCommand.kind === "dropIndex" ? mongoCommand.index : mongoCommand.indexes, mongoCommand.kind === "dropIndex"); @@ -4084,7 +4088,7 @@ export const useQueryStore = defineStore("query", () => { traceId, database: currentDatabase, kind: mongoCommand.kind, - collection: mongoCommand.collection, + ...(mongoCommand.kind === "createUser" ? {} : { collection: mongoCommand.collection }), elapsed: elapsed(), }); break; diff --git a/crates/dbx-core/assets/agent-protocol-v1.json b/crates/dbx-core/assets/agent-protocol-v1.json index 999efd3e50..13077d2ee0 100644 --- a/crates/dbx-core/assets/agent-protocol-v1.json +++ b/crates/dbx-core/assets/agent-protocol-v1.json @@ -39,6 +39,6 @@ "disconnect", "shutdown" ], - "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], + "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], "kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"] } diff --git a/crates/dbx-core/assets/agent-protocol-v2.json b/crates/dbx-core/assets/agent-protocol-v2.json index 39f73d3da5..76799d87e5 100644 --- a/crates/dbx-core/assets/agent-protocol-v2.json +++ b/crates/dbx-core/assets/agent-protocol-v2.json @@ -43,7 +43,7 @@ "disconnect", "shutdown" ], - "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], + "mongoLegacyMethods": ["list_databases", "list_collections", "find_documents", "find_one", "explain_find", "aggregate_documents", "find_documents_extended_json", "count_documents", "server_version", "create_index", "create_user", "drop_indexes", "drop_collection", "drop_database", "insert_document", "update_document", "update_documents", "delete_document", "delete_documents"], "kvMethods": ["kv_list_prefix", "kv_get", "kv_put", "kv_delete", "kv_rename", "kv_history", "kv_status", "etcd_compact", "etcd_defrag", "etcd_watch_start", "etcd_watch_poll", "etcd_watch_stop", "etcd_lease_list", "etcd_lease_get", "etcd_lease_grant", "etcd_lease_keepalive_once", "etcd_lease_revoke", "etcd_auth_user_list", "etcd_auth_user_get", "etcd_auth_user_add", "etcd_auth_user_delete", "etcd_auth_user_change_password", "etcd_auth_user_grant_role", "etcd_auth_user_revoke_role", "etcd_auth_role_list", "etcd_auth_role_get", "etcd_auth_role_add", "etcd_auth_role_delete", "etcd_auth_role_grant_permission", "etcd_auth_role_revoke_permission"], "sessionField": "agentSessionId", "cursorSessionField": "sessionId" diff --git a/crates/dbx-core/src/db/agent_driver.rs b/crates/dbx-core/src/db/agent_driver.rs index 2e595a54d5..ff45c9da90 100644 --- a/crates/dbx-core/src/db/agent_driver.rs +++ b/crates/dbx-core/src/db/agent_driver.rs @@ -1279,6 +1279,7 @@ pub enum MongoAgentMethod { CountDocuments, ServerVersion, CreateIndex, + CreateUser, DropIndexes, DropCollection, DropDatabase, @@ -1290,7 +1291,7 @@ pub enum MongoAgentMethod { } impl MongoAgentMethod { - pub const ALL: [Self; 18] = [ + pub const ALL: [Self; 19] = [ Self::ListDatabases, Self::ListCollections, Self::FindDocuments, @@ -1301,6 +1302,7 @@ impl MongoAgentMethod { Self::CountDocuments, Self::ServerVersion, Self::CreateIndex, + Self::CreateUser, Self::DropIndexes, Self::DropCollection, Self::DropDatabase, @@ -1323,6 +1325,7 @@ impl MongoAgentMethod { Self::CountDocuments => "count_documents", Self::ServerVersion => "server_version", Self::CreateIndex => "create_index", + Self::CreateUser => "create_user", Self::DropIndexes => "drop_indexes", Self::DropCollection => "drop_collection", Self::DropDatabase => "drop_database", @@ -2520,6 +2523,13 @@ impl AgentDriverClient { self.call_mongo_method(MongoAgentMethod::CreateIndex, params).await } + pub async fn mongo_create_user( + &mut self, + params: Value, + ) -> Result { + self.call_mongo_method(MongoAgentMethod::CreateUser, params).await + } + pub async fn mongo_drop_indexes( &mut self, params: Value, @@ -4348,6 +4358,7 @@ for line in sys.stdin: assert_eq!(MongoAgentMethod::CountDocuments.as_str(), "count_documents"); assert_eq!(MongoAgentMethod::ServerVersion.as_str(), "server_version"); assert_eq!(MongoAgentMethod::CreateIndex.as_str(), "create_index"); + assert_eq!(MongoAgentMethod::CreateUser.as_str(), "create_user"); assert_eq!(MongoAgentMethod::DropIndexes.as_str(), "drop_indexes"); assert_eq!(MongoAgentMethod::DropCollection.as_str(), "drop_collection"); assert_eq!(MongoAgentMethod::DropDatabase.as_str(), "drop_database"); diff --git a/crates/dbx-core/src/db/mongo_driver.rs b/crates/dbx-core/src/db/mongo_driver.rs index 51e397699d..d34e9b0892 100644 --- a/crates/dbx-core/src/db/mongo_driver.rs +++ b/crates/dbx-core/src/db/mongo_driver.rs @@ -1231,6 +1231,48 @@ pub async fn create_index( Ok(name) } +pub async fn create_user( + client: &Client, + database: &str, + user_json: &str, + write_concern_json: Option<&str>, +) -> Result<(), String> { + let database = validate_mongo_namespace_name(database, "Database")?; + let command = create_user_command(user_json, write_concern_json)?; + client.database(database).run_command(command).await.map_err(|error| error.kind.to_string())?; + Ok(()) +} + +pub fn validate_create_user_request(user_json: &str, write_concern_json: Option<&str>) -> Result<(), String> { + create_user_command(user_json, write_concern_json).map(|_| ()) +} + +fn create_user_command(user_json: &str, write_concern_json: Option<&str>) -> Result { + let user_value: serde_json::Value = + serde_json::from_str(user_json).map_err(|error| format!("Invalid MongoDB user JSON: {error}"))?; + let mut user = json_object_to_document_extended_json(&user_value) + .map_err(|error| format!("Invalid MongoDB user document: {error}"))?; + let username = user + .remove("user") + .and_then(|value| value.as_str().map(str::to_string)) + .filter(|value| !value.trim().is_empty()) + .ok_or("MongoDB createUser requires a non-empty user name")?; + if user.contains_key("createUser") || user.contains_key("writeConcern") { + return Err("MongoDB createUser user document contains reserved command fields".to_string()); + } + + let mut command = doc! { "createUser": username }; + command.extend(user); + if let Some(write_concern_json) = write_concern_json.filter(|value| !value.trim().is_empty()) { + let write_concern_value: serde_json::Value = serde_json::from_str(write_concern_json) + .map_err(|error| format!("Invalid MongoDB write concern JSON: {error}"))?; + let write_concern = json_object_to_document_extended_json(&write_concern_value) + .map_err(|error| format!("Invalid MongoDB write concern: {error}"))?; + command.insert("writeConcern", write_concern); + } + Ok(command) +} + /// Validate an index request before it reaches either the native driver or the /// Legacy Agent. In particular, `key` belongs to the createIndexes command /// itself and must not be smuggled through the options document. @@ -3013,6 +3055,35 @@ mod tests { assert_eq!(error, "Collection name is required"); } + #[test] + fn create_user_command_preserves_roles_and_write_concern() { + let command = create_user_command( + r#"{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}"#, + Some(r#"{"w":"majority","wtimeout":5000}"#), + ) + .unwrap(); + + assert_eq!(command.keys().next().map(String::as_str), Some("createUser")); + assert_eq!(command.get_str("createUser").unwrap(), "test-db"); + assert_eq!(command.get_str("pwd").unwrap(), "test-password"); + let roles = command.get_array("roles").unwrap(); + assert_eq!(roles[0].as_document().unwrap(), &doc! { "role": "readWrite", "db": "db1" }); + assert_eq!(command.get_document("writeConcern").unwrap(), &doc! { "w": "majority", "wtimeout": 5000_i32 }); + } + + #[test] + fn create_user_command_rejects_missing_names_and_reserved_fields() { + for user in [ + r#"{"pwd":"secret","roles":[]}"#, + r#"{"user":"","pwd":"secret","roles":[]}"#, + r#"{"user":"app","createUser":"other","pwd":"secret","roles":[]}"#, + r#"{"user":"app","writeConcern":{"w":1},"pwd":"secret","roles":[]}"#, + ] { + assert!(create_user_command(user, None).is_err(), "{user}"); + } + assert!(create_user_command(r#"{"user":"app","pwd":"secret","roles":[]}"#, Some("true")).is_err()); + } + #[test] fn create_indexes_command_keeps_raw_options_and_generates_a_name() { let (command, name) = create_indexes_command( diff --git a/crates/dbx-core/src/mongo_ops.rs b/crates/dbx-core/src/mongo_ops.rs index e231818517..4e2c04be96 100644 --- a/crates/dbx-core/src/mongo_ops.rs +++ b/crates/dbx-core/src/mongo_ops.rs @@ -466,6 +466,37 @@ pub async fn mongo_create_index_core( } } +pub async fn mongo_create_user_core( + state: &AppState, + connection_id: &str, + database: &str, + user_json: &str, + write_concern_json: Option<&str>, +) -> Result { + mongo_driver::validate_mongo_namespace_name(database, "Database")?; + mongo_driver::validate_create_user_request(user_json, write_concern_json)?; + ensure_document_pool(state, connection_id).await?; + let connections = state.connections.read().await; + match connections.get(connection_id).ok_or("Not found")? { + PoolKind::MongoDb(client) => { + mongo_driver::create_user(client, database, user_json, write_concern_json).await?; + Ok(1) + } + PoolKind::Agent(client) => { + let mut client = client.lock().await; + let result: serde_json::Value = client + .mongo_create_user(serde_json::json!({ + "database": database, + "user_json": user_json, + "write_concern_json": write_concern_json, + })) + .await?; + Ok(result.get("affected_rows").and_then(serde_json::Value::as_u64).unwrap_or(1)) + } + _ => Err("Not a MongoDB connection".to_string()), + } +} + pub async fn mongo_drop_indexes_core( state: &AppState, connection_id: &str, @@ -875,6 +906,12 @@ pub async fn execute_mongo_command_core( mongo_create_index_core(state, connection_id, database, collection, keys, options.as_deref()).await?; Ok(scalar_query_result("name", Value::String(name))) } + MongoCommand::CreateUser { user_json, write_concern_json } => { + let affected = + mongo_create_user_core(state, connection_id, database, user_json, write_concern_json.as_deref()) + .await?; + Ok(affected_query_result(affected)) + } MongoCommand::DropIndexes { collection, indexes, single } => { let result = mongo_drop_indexes_core(state, connection_id, database, collection, indexes.as_deref(), *single) diff --git a/crates/dbx-core/src/mongo_shell.rs b/crates/dbx-core/src/mongo_shell.rs index 8bbcccb3bf..8dde2346dc 100644 --- a/crates/dbx-core/src/mongo_shell.rs +++ b/crates/dbx-core/src/mongo_shell.rs @@ -8,6 +8,13 @@ pub enum MongoCommand { Version, #[serde(rename = "use")] Use { database: String }, + #[serde(rename = "createUser")] + CreateUser { + #[serde(rename = "userJson")] + user_json: String, + #[serde(rename = "writeConcernJson")] + write_concern_json: Option, + }, #[serde(rename = "find")] Find { collection: String, @@ -77,7 +84,8 @@ impl MongoCommand { pub fn is_mutating(&self) -> bool { matches!( self, - Self::Insert { .. } + Self::CreateUser { .. } + | Self::Insert { .. } | Self::Update { .. } | Self::Delete { .. } | Self::CreateIndex { .. } @@ -90,7 +98,7 @@ impl MongoCommand { } pub fn is_dangerous(&self) -> bool { - matches!(self, Self::DropCollection { .. }) + matches!(self, Self::CreateUser { .. } | Self::DropCollection { .. }) || matches!(self, Self::DropIndexes { indexes: None, single: false, .. }) || matches!(self, Self::Aggregate { pipeline, .. } if aggregate_writes(pipeline)) } @@ -348,6 +356,23 @@ pub fn parse(input: &str) -> Result { if let Some(database) = parse_use_database(source) { return Ok(MongoCommand::Use { database }); } + if let Some((args, tail)) = database_method_call(source, "createUser") { + if !tail.is_empty() || !(1..=2).contains(&args.len()) { + return Err("MongoDB createUser() requires a user document and optional write concern.".to_string()); + } + let user_json = normalized_json(&args[0])?; + let user = parse_json_value(&user_json) + .and_then(|value| value.as_object().cloned()) + .ok_or("MongoDB createUser() requires a user document.")?; + if user.get("user").and_then(Value::as_str).is_none_or(|user| user.trim().is_empty()) { + return Err("MongoDB createUser() requires a non-empty user name.".to_string()); + } + let write_concern_json = optional_json_argument(args.get(1))?; + if write_concern_json.as_deref().and_then(parse_json_value).is_some_and(|value| !value.is_object()) { + return Err("MongoDB createUser() write concern must be a document.".to_string()); + } + return Ok(MongoCommand::CreateUser { user_json, write_concern_json }); + } let (collection, prefix_end) = parse_collection_prefix(source)?; if let Some((args, tail)) = method_call(source, prefix_end, "find") { @@ -651,6 +676,24 @@ fn method_call(source: &str, prefix_end: usize, method: &str) -> Option<(Vec Option<(Vec, String)> { + if !source.get(..2).is_some_and(|prefix| prefix.eq_ignore_ascii_case("db")) { + return None; + } + let after_db = source.get(2..)?.trim_start(); + let after_dot = after_db.strip_prefix('.')?.trim_start(); + if !after_dot.get(..method.len()).is_some_and(|name| name.eq_ignore_ascii_case(method)) { + return None; + } + let after_method = after_dot.get(method.len()..)?.trim_start(); + if !after_method.starts_with('(') { + return None; + } + let open = source.len() - after_method.len(); + let close = matching_paren(source, open)?; + Some((split_top_level(&source[open + 1..close]), source[close + 1..].trim().to_string())) +} + fn chained_calls(chain: &str) -> Result)>, String> { let mut rest = chain.trim(); let mut calls = Vec::new(); @@ -957,6 +1000,28 @@ mod tests { assert_eq!(validate_safety(&legacy_update, true, false, false), Err(MongoSafetyError::EmptyFilter)); } + #[test] + fn parses_create_user_as_a_dangerous_write() { + let command = parse( + r#"db . createUser({user: "test-db", pwd: "test-password", roles: [{role: "readWrite", db: "db1"}]}, {w: "majority"})"#, + ) + .unwrap(); + assert_eq!( + command, + MongoCommand::CreateUser { + user_json: r#"{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}"# + .to_string(), + write_concern_json: Some(r#"{"w":"majority"}"#.to_string()), + } + ); + assert!(command.is_mutating()); + assert!(command.is_dangerous()); + assert_eq!(validate_safety(&command, true, false, false), Err(MongoSafetyError::Dangerous)); + assert_eq!(validate_safety(&command, true, true, false), Ok(())); + assert!(parse(r#"db.createUser({pwd: "missing-user", roles: []})"#).is_err()); + assert!(parse(r#"db.createUser({user: "test"}, "majority")"#).is_err()); + } + #[test] fn treats_effectively_unbounded_write_filters_as_dangerous() { for command in [ @@ -1102,6 +1167,10 @@ mod tests { let count = serde_json::to_value(parse("db.items.count({})").unwrap()).unwrap(); assert_eq!(count["kind"], "countDocuments"); assert_eq!(count["accurate"], false); + let create_user = + serde_json::to_value(parse(r#"db.createUser({user: "app", pwd: "secret", roles: []})"#).unwrap()).unwrap(); + assert_eq!(create_user["kind"], "createUser"); + assert_eq!(create_user["userJson"], r#"{"user":"app","pwd":"secret","roles":[]}"#); } #[test] diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index e147a17105..7770bf7397 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -34,7 +34,7 @@ pub const QUERY_CANCELED: &str = "Query canceled"; /// Fallback when a Mongo connection hits the generic SQL executor instead of the shell path. /// Wording must match packages/mongo-shell `MONGO_SHELL_COMMAND_HINT` /// (desktop/CLI diagnose first; this is only the Rust SQL-executor backstop). -const MONGO_SHELL_COMMAND_HINT: &str = "Use MongoDB shell-style commands, for example: db.collection.find({}).limit(100), db.collection.aggregate([]), db.collection.aggregate([], { explain: true }), db.version(), db.collection.countDocuments({}), db.collection.distinct(\"field\"), db.collection.getIndexes(), db.collection.createIndex({...}), or db.collection.insertOne({...})."; +const MONGO_SHELL_COMMAND_HINT: &str = "Use MongoDB shell-style commands, for example: db.collection.find({}).limit(100), db.collection.aggregate([]), db.collection.aggregate([], { explain: true }), db.version(), db.collection.countDocuments({}), db.collection.distinct(\"field\"), db.collection.getIndexes(), db.collection.createIndex({...}), db.createUser({...}), or db.collection.insertOne({...})."; const SQL_OMITTED_ERROR_CONTEXT: &str = "SQL text omitted from user-facing error; enable debug SQL diagnostics to inspect the original statement."; diff --git a/crates/dbx-mcp/src/backend.rs b/crates/dbx-mcp/src/backend.rs index a3b7dea95b..9db1877986 100644 --- a/crates/dbx-mcp/src/backend.rs +++ b/crates/dbx-mcp/src/backend.rs @@ -1138,6 +1138,24 @@ impl DbxBackend for WebBackend { Value::String(value.get("name").and_then(Value::as_str).unwrap_or("").to_string()), )) } + MongoCommand::CreateUser { user_json, write_concern_json } => { + let value: Value = self + .request( + reqwest::Method::POST, + "/api/mongo/create-user", + Some(json!({ + "connectionId": connection_id, + "database": database, + "userJson": user_json, + "writeConcernJson": write_concern_json, + })), + ) + .await? + .json() + .await + .map_err(|error| format!("Invalid MongoDB create user response: {error}"))?; + Ok(affected_query_result(affected_rows_from_value(&value))) + } MongoCommand::DropIndexes { collection, indexes, single } => { let value: Value = self .request( diff --git a/crates/dbx-web/src/main.rs b/crates/dbx-web/src/main.rs index c9e70406fe..15fc074fe2 100644 --- a/crates/dbx-web/src/main.rs +++ b/crates/dbx-web/src/main.rs @@ -632,6 +632,7 @@ async fn main() { .route("/mongo/aggregate-documents", post(routes::mongo::aggregate_documents)) .route("/mongo/distinct", post(routes::mongo::distinct)) .route("/mongo/create-index", post(routes::mongo::create_index)) + .route("/mongo/create-user", post(routes::mongo::create_user)) .route("/mongo/drop-indexes", post(routes::mongo::drop_indexes)) .route("/mongo/insert-document", post(routes::mongo::insert_document)) .route("/mongo/insert-documents", post(routes::mongo::insert_documents)) diff --git a/crates/dbx-web/src/routes/mongo.rs b/crates/dbx-web/src/routes/mongo.rs index b509dafb7a..30dd64cd10 100644 --- a/crates/dbx-web/src/routes/mongo.rs +++ b/crates/dbx-web/src/routes/mongo.rs @@ -191,6 +191,15 @@ pub struct MongoCreateIndexRequest { pub options_json: Option, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MongoCreateUserRequest { + pub connection_id: String, + pub database: String, + pub user_json: String, + pub write_concern_json: Option, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct MongoDropIndexesRequest { @@ -616,6 +625,26 @@ pub async fn create_index( Ok(Json(serde_json::json!({ "name": name }))) } +pub async fn create_user( + State(state): State>, + headers: HeaderMap, + Json(req): Json, +) -> Result, AppError> { + super::mcp_policy::ensure_dangerous_write(&state, &headers, &req.connection_id, &req.database, "Create user") + .await?; + ensure_writable(&state.app, &req.connection_id, "Create user").await?; + let affected_rows = dbx_core::mongo_ops::mongo_create_user_core( + &state.app, + &req.connection_id, + &req.database, + &req.user_json, + req.write_concern_json.as_deref(), + ) + .await + .map_err(AppError::from)?; + Ok(Json(serde_json::json!({ "affected_rows": affected_rows }))) +} + pub async fn drop_indexes( State(state): State>, headers: HeaderMap, diff --git a/packages/app-tests/mongoShellCommand.test.ts b/packages/app-tests/mongoShellCommand.test.ts index 752e15a072..2aab1cd90a 100644 --- a/packages/app-tests/mongoShellCommand.test.ts +++ b/packages/app-tests/mongoShellCommand.test.ts @@ -16,6 +16,7 @@ import { parseMongoAggregateCommand, parseMongoCollectionStatsCommand, parseMongoCommand, + parseMongoCreateUserCommand, parseMongoCountDocumentsCommand, parseMongoDistinctCommand, parseMongoFindCommand, @@ -49,6 +50,41 @@ test("normalizeRustMongoCommand preserves the desktop command contract", () => { assert.deepEqual(normalizeRustMongoCommand({ kind: "findOne", collection: "users", filter: "{}", projection: null, options: null }), { kind: "findOne", collection: "users", filter: "{}" }); }); +test("parseMongoCreateUserCommand normalizes user and write concern documents", () => { + assert.deepEqual( + parseMongoCreateUserCommand(`db.createUser({ + user: "test-db", + pwd: "test-password", + roles: [{ role: "readWrite", db: "db1" }] + }, { w: "majority", wtimeout: 5000 })`), + { + userJson: '{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}', + writeConcernJson: '{"w":"majority","wtimeout":5000}', + }, + ); + assert.equal(parseMongoCreateUserCommand('db.createUser({pwd: "missing-user", roles: []})'), null); + assert.equal(parseMongoCreateUserCommand('db.createUser({user: "test"}, "majority")'), null); +}); + +test("splitMongoCommands executes use before createUser", () => { + const commands = splitMongoCommands(`use admin + +db.createUser({ + user: "test-db", + pwd: "test-password", + roles: [{ role: "readWrite", db: "db1" }] +})`); + + assert.deepEqual(commands.map(({ command }) => command.kind), ["use", "createUser"]); + const createUser = commands[1]?.command; + assert.equal(createUser?.kind, "createUser"); + if (createUser?.kind === "createUser") { + assert.equal(JSON.parse(createUser.userJson).user, "test-db"); + assert.match(evaluateMongoWriteSafety(createUser, { allowWrites: true }).reason || "", /high-risk operations/i); + assert.equal(evaluateMongoWriteSafety(createUser, { allowWrites: true, allowDangerous: true }).allowed, true); + } +}); + test("parseMongoFindCommand parses getCollection find with chained sort skip and limit", () => { assert.deepEqual(parseMongoFindCommand('db.getCollection("audit.logs").find({"level":"warn"}).sort({"createdAt":-1}).skip(20).limit(10)'), { collection: "audit.logs", diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 2b7ea5a5d1..ec7f5b0cf6 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -4547,6 +4547,60 @@ test("mongo createIndex execution uses the dedicated create-index endpoint", asy } }); +test("mongo createUser execution follows a preceding use command", async () => { + const restoreStorage = installMemoryStorage(); + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + const store = useQueryStore(); + const originalFetch = globalThis.fetch; + let createUserBody: any; + + connectionStore.addEphemeralConnection({ + ...conn("mongo-1"), + db_type: "mongodb", + port: 27017, + }); + + globalThis.fetch = withConnectionHealthMock(async (input, init) => { + if (String(input) === "/api/mongo/create-user") { + createUserBody = JSON.parse(String(init?.body ?? "{}")); + return Response.json({ affected_rows: 1 }); + } + return new Response("unexpected request", { status: 500 }); + }); + + try { + const tabId = store.createTab("mongo-1", "accounting", "Query", "query", ""); + await store.executeTabSql( + tabId, + `use admin + +db.createUser({ + user: "test-db", + pwd: "test-password", + roles: [{ role: "readWrite", db: "db1" }] +})`, + ); + const tab = store.tabs.find((item) => item.id === tabId); + + assert.deepEqual(createUserBody, { + connectionId: "mongo-1", + database: "admin", + userJson: '{"user":"test-db","pwd":"test-password","roles":[{"role":"readWrite","db":"db1"}]}', + }); + assert.equal(tab?.database, "admin"); + assert.equal(tab?.results?.length, 2); + assert.deepEqual(tab?.results?.[0]?.rows, [["switched to db admin"]]); + assert.deepEqual(tab?.results?.[1]?.columns, []); + assert.deepEqual(tab?.results?.[1]?.rows, []); + assert.equal(tab?.results?.[1]?.affected_rows, 1); + assert.equal(tab?.results?.[1]?.sourceLabel, undefined); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("mongo dropIndex execution uses the dedicated drop-indexes endpoint", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); diff --git a/packages/mongo-shell/src/aggregate.ts b/packages/mongo-shell/src/aggregate.ts index 2d1982b61d..69a8f03c67 100644 --- a/packages/mongo-shell/src/aggregate.ts +++ b/packages/mongo-shell/src/aggregate.ts @@ -25,7 +25,8 @@ export const MONGO_SHELL_COMMAND_HINT = "Use MongoDB shell-style commands, for example: db.collection.find({}).limit(100), " + "db.collection.aggregate([]), db.collection.aggregate([], { explain: true }), " + 'db.version(), db.collection.countDocuments({}), db.collection.distinct("field"), ' + - "db.collection.getIndexes(), db.collection.createIndex({...}), or db.collection.insertOne({...})."; + "db.collection.getIndexes(), db.collection.createIndex({...}), db.createUser({...}), " + + "or db.collection.insertOne({...})."; const PIPELINE_MUST_BE_ARRAY = "MongoDB aggregate pipeline must be a JSON array (for example [{ $match: {} }])."; diff --git a/src-tauri/src/commands/mongo_cmd.rs b/src-tauri/src/commands/mongo_cmd.rs index 43e9d37491..cee476cbbf 100644 --- a/src-tauri/src/commands/mongo_cmd.rs +++ b/src-tauri/src/commands/mongo_cmd.rs @@ -334,6 +334,36 @@ pub async fn mongo_create_index( Ok(serde_json::json!({ "name": name })) } +#[tauri::command] +pub async fn mongo_create_user( + state: State<'_, Arc>, + connection_id: String, + database: String, + user_json: String, + write_concern_json: Option, + mcp_request: Option, +) -> Result { + if mcp_request == Some(true) { + crate::commands::mcp_bridge::ensure_mcp_dangerous_write_allowed_by_id( + state.inner(), + &connection_id, + &database, + "Create user", + ) + .await?; + } + ensure_connection_writable(&state, &connection_id, "Create user").await?; + let affected_rows = dbx_core::mongo_ops::mongo_create_user_core( + &state, + &connection_id, + &database, + &user_json, + write_concern_json.as_deref(), + ) + .await?; + Ok(serde_json::json!({ "affected_rows": affected_rows })) +} + #[tauri::command] pub async fn mongo_drop_indexes( state: State<'_, Arc>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 25917c6fec..60e5f61dfd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1894,6 +1894,7 @@ pub fn run() { commands::mongo_cmd::mongo_aggregate_documents, commands::mongo_cmd::mongo_distinct, commands::mongo_cmd::mongo_create_index, + commands::mongo_cmd::mongo_create_user, commands::mongo_cmd::mongo_drop_indexes, commands::document_cmd::document_insert_document, commands::mongo_cmd::mongo_insert_document,