diff --git a/src/common/atlas/roles.ts b/src/common/atlas/roles.ts new file mode 100644 index 000000000..5d776c754 --- /dev/null +++ b/src/common/atlas/roles.ts @@ -0,0 +1,33 @@ +import type { UserConfig } from "../config.js"; +import type { DatabaseUserRole } from "./openapi.js"; + +const readWriteRole: DatabaseUserRole = { + roleName: "readWriteAnyDatabase", + databaseName: "admin", +}; + +const readOnlyRole: DatabaseUserRole = { + roleName: "readAnyDatabase", + databaseName: "admin", +}; + +/** + * Get the default role name for the database user based on the Atlas Admin API + * https://www.mongodb.com/docs/atlas/mongodb-users-roles-and-privileges/ + */ +export function getDefaultRoleFromConfig(config: UserConfig): DatabaseUserRole { + if (config.readOnly) { + return readOnlyRole; + } + + // If any of the write tools are enabled, use readWriteAnyDatabase + if ( + !config.disabledTools.includes("create") || + !config.disabledTools.includes("update") || + !config.disabledTools.includes("delete") + ) { + return readWriteRole; + } + + return readOnlyRole; +} diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index ed509d02d..368303d47 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -7,6 +7,7 @@ import { LogId } from "../../../common/logger.js"; import { inspectCluster } from "../../../common/atlas/cluster.js"; import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js"; import type { AtlasClusterConnectionInfo } from "../../../common/connectionManager.js"; +import { getDefaultRoleFromConfig } from "../../../common/atlas/roles.js"; const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours @@ -72,16 +73,7 @@ export class ConnectClusterTool extends AtlasToolBase { const password = await generateSecurePassword(); const expiryDate = new Date(Date.now() + EXPIRY_MS); - - const readOnly = - this.config.readOnly || - (this.config.disabledTools?.includes("create") && - this.config.disabledTools?.includes("update") && - this.config.disabledTools?.includes("delete") && - !this.config.disabledTools?.includes("read") && - !this.config.disabledTools?.includes("metadata")); - - const roleName = readOnly ? "readAnyDatabase" : "readWriteAnyDatabase"; + const role = getDefaultRoleFromConfig(this.config); await this.session.apiClient.createDatabaseUser({ params: { @@ -92,12 +84,7 @@ export class ConnectClusterTool extends AtlasToolBase { body: { databaseName: "admin", groupId: projectId, - roles: [ - { - roleName, - databaseName: "admin", - }, - ], + roles: [role], scopes: [{ type: "CLUSTER", name: clusterName }], username, password, @@ -106,6 +93,7 @@ export class ConnectClusterTool extends AtlasToolBase { oidcAuthType: "NONE", x509Type: "NONE", deleteAfterDate: expiryDate.toISOString(), + description: "This temporary user is created by the MongoDB MCP Server to connect to the cluster.", }, }); diff --git a/tests/unit/common/roles.test.ts b/tests/unit/common/roles.test.ts new file mode 100644 index 000000000..058e605ab --- /dev/null +++ b/tests/unit/common/roles.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect } from "vitest"; +import { getDefaultRoleFromConfig } from "../../../src/common/atlas/roles.js"; +import { defaultUserConfig, type UserConfig } from "../../../src/common/config.js"; + +describe("getDefaultRoleFromConfig", () => { + const defaultConfig: UserConfig = { + ...defaultUserConfig, + }; + + const readOnlyConfig: UserConfig = { + ...defaultConfig, + readOnly: true, + }; + + const readWriteConfig: UserConfig = { + ...defaultConfig, + readOnly: false, + disabledTools: [], + }; + + const readWriteConfigWithDeleteDisabled: UserConfig = { + ...defaultConfig, + readOnly: false, + disabledTools: ["delete"], + }; + + const readWriteConfigWithCreateDisabled: UserConfig = { + ...defaultConfig, + readOnly: false, + disabledTools: ["create"], + }; + + const readWriteConfigWithUpdateDisabled: UserConfig = { + ...defaultConfig, + readOnly: false, + disabledTools: ["update"], + }; + + const readWriteConfigWithAllToolsDisabled: UserConfig = { + ...defaultConfig, + readOnly: false, + disabledTools: ["create", "update", "delete"], + }; + + it("should return the correct role for a read-only config", () => { + const role = getDefaultRoleFromConfig(readOnlyConfig); + expect(role).toEqual({ + roleName: "readAnyDatabase", + databaseName: "admin", + }); + }); + + it("should return the correct role for a read-write config", () => { + const role = getDefaultRoleFromConfig(readWriteConfig); + expect(role).toEqual({ + roleName: "readWriteAnyDatabase", + databaseName: "admin", + }); + }); + + it("should return the correct role for a read-write config with all tools enabled", () => { + const role = getDefaultRoleFromConfig(readWriteConfig); + expect(role).toEqual({ + roleName: "readWriteAnyDatabase", + databaseName: "admin", + }); + }); + + it("should return the correct role for a read-write config with delete disabled", () => { + const role = getDefaultRoleFromConfig(readWriteConfigWithDeleteDisabled); + expect(role).toEqual({ + roleName: "readWriteAnyDatabase", + databaseName: "admin", + }); + }); + + it("should return the correct role for a read-write config with create disabled", () => { + const role = getDefaultRoleFromConfig(readWriteConfigWithCreateDisabled); + expect(role).toEqual({ + roleName: "readWriteAnyDatabase", + databaseName: "admin", + }); + }); + + it("should return the correct role for a read-write config with update disabled", () => { + const role = getDefaultRoleFromConfig(readWriteConfigWithUpdateDisabled); + expect(role).toEqual({ + roleName: "readWriteAnyDatabase", + databaseName: "admin", + }); + }); + + it("should return the correct role for a read-write config with all tools disabled", () => { + const role = getDefaultRoleFromConfig(readWriteConfigWithAllToolsDisabled); + expect(role).toEqual({ + roleName: "readAnyDatabase", + databaseName: "admin", + }); + }); +});