Skip to content

Commit 333c36a

Browse files
authored
fix: instruct models not to generate passwords for createDBUser (#177)
1 parent 30ee1f6 commit 333c36a

File tree

5 files changed

+91
-46
lines changed

5 files changed

+91
-46
lines changed

src/common/atlas/generatePassword.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { randomBytes } from "crypto";
2+
import { promisify } from "util";
3+
4+
const randomBytesAsync = promisify(randomBytes);
5+
6+
export async function generateSecurePassword(): Promise<string> {
7+
const buf = await randomBytesAsync(16);
8+
const pass = buf.toString("base64url");
9+
return pass;
10+
}

src/tools/atlas/create/createDBUser.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
55
import { CloudDatabaseUser, DatabaseUserRole } from "../../../common/atlas/openapi.js";
6+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
67

78
export class CreateDBUserTool extends AtlasToolBase {
89
protected name = "atlas-create-db-user";
@@ -11,7 +12,16 @@ export class CreateDBUserTool extends AtlasToolBase {
1112
protected argsShape = {
1213
projectId: z.string().describe("Atlas project ID"),
1314
username: z.string().describe("Username for the new user"),
14-
password: z.string().describe("Password for the new user"),
15+
// Models will generate overly simplistic passwords like SecurePassword123 or
16+
// AtlasPassword123, which are easily guessable and exploitable. We're instructing
17+
// the model not to try and generate anything and instead leave the field unset.
18+
password: z
19+
.string()
20+
.optional()
21+
.nullable()
22+
.describe(
23+
"Password for the new user. If the user hasn't supplied an explicit password, leave it unset and under no circumstances try to generate a random one. A secure password will be generated by the MCP server if necessary."
24+
),
1525
roles: z
1626
.array(
1727
z.object({
@@ -34,6 +44,11 @@ export class CreateDBUserTool extends AtlasToolBase {
3444
roles,
3545
clusters,
3646
}: ToolArgs<typeof this.argsShape>): Promise<CallToolResult> {
47+
const shouldGeneratePassword = !password;
48+
if (shouldGeneratePassword) {
49+
password = await generateSecurePassword();
50+
}
51+
3752
const input = {
3853
groupId: projectId,
3954
awsIAMType: "NONE",
@@ -62,7 +77,12 @@ export class CreateDBUserTool extends AtlasToolBase {
6277
});
6378

6479
return {
65-
content: [{ type: "text", text: `User "${username}" created sucessfully.` }],
80+
content: [
81+
{
82+
type: "text",
83+
text: `User "${username}" created successfully${shouldGeneratePassword ? ` with password: \`${password}\`` : ""}.`,
84+
},
85+
],
6686
};
6787
}
6888
}

src/tools/atlas/metadata/connectCluster.ts

+1-11
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,14 @@ import { z } from "zod";
22
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
33
import { AtlasToolBase } from "../atlasTool.js";
44
import { ToolArgs, OperationType } from "../../tool.js";
5-
import { randomBytes } from "crypto";
6-
import { promisify } from "util";
5+
import { generateSecurePassword } from "../../../common/atlas/generatePassword.js";
76
import logger, { LogId } from "../../../logger.js";
87

98
const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours
109

11-
const randomBytesAsync = promisify(randomBytes);
12-
13-
async function generateSecurePassword(): Promise<string> {
14-
const buf = await randomBytesAsync(16);
15-
const pass = buf.toString("base64url");
16-
return pass;
17-
}
18-
1910
function sleep(ms: number): Promise<void> {
2011
return new Promise((resolve) => setTimeout(resolve, ms));
2112
}
22-
2313
export class ConnectClusterTool extends AtlasToolBase {
2414
protected name = "atlas-connect-cluster";
2515
protected description = "Connect to MongoDB Atlas cluster";

tests/integration/helpers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function getResponseElements(content: unknown | { content: unknown }): {
117117
content = (content as { content: unknown }).content;
118118
}
119119

120-
expect(Array.isArray(content)).toBe(true);
120+
expect(content).toBeArray();
121121

122122
const response = content as { type: string; text: string }[];
123123
for (const item of response) {

tests/integration/tools/atlas/dbUsers.test.ts

+57-32
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,49 @@
11
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2-
import { Session } from "../../../../src/session.js";
32
import { describeWithAtlas, withProject, randomId } from "./atlasHelpers.js";
4-
import { expectDefined } from "../../helpers.js";
3+
import { expectDefined, getResponseElements } from "../../helpers.js";
4+
import { ApiClientError } from "../../../../src/common/atlas/apiClientError.js";
55

66
describeWithAtlas("db users", (integration) => {
7-
const userName = "testuser-" + randomId;
87
withProject(integration, ({ getProjectId }) => {
9-
afterAll(async () => {
10-
const projectId = getProjectId();
8+
let userName: string;
9+
beforeEach(() => {
10+
userName = "testuser-" + randomId;
11+
});
1112

12-
const session: Session = integration.mcpServer().session;
13-
await session.apiClient.deleteDatabaseUser({
14-
params: {
15-
path: {
16-
groupId: projectId,
17-
username: userName,
18-
databaseName: "admin",
19-
},
13+
const createUserWithMCP = async (password?: string): Promise<unknown> => {
14+
return await integration.mcpClient().callTool({
15+
name: "atlas-create-db-user",
16+
arguments: {
17+
projectId: getProjectId(),
18+
username: userName,
19+
password,
20+
roles: [
21+
{
22+
roleName: "readWrite",
23+
databaseName: "admin",
24+
},
25+
],
2026
},
2127
});
28+
};
29+
30+
afterEach(async () => {
31+
try {
32+
await integration.mcpServer().session.apiClient.deleteDatabaseUser({
33+
params: {
34+
path: {
35+
groupId: getProjectId(),
36+
username: userName,
37+
databaseName: "admin",
38+
},
39+
},
40+
});
41+
} catch (error) {
42+
// Ignore 404 errors when deleting the user
43+
if (!(error instanceof ApiClientError) || error.response?.status !== 404) {
44+
throw error;
45+
}
46+
}
2247
});
2348

2449
describe("atlas-create-db-user", () => {
@@ -34,26 +59,24 @@ describeWithAtlas("db users", (integration) => {
3459
expect(createDbUser.inputSchema.properties).toHaveProperty("roles");
3560
expect(createDbUser.inputSchema.properties).toHaveProperty("clusters");
3661
});
37-
it("should create a database user", async () => {
38-
const projectId = getProjectId();
3962

40-
const response = (await integration.mcpClient().callTool({
41-
name: "atlas-create-db-user",
42-
arguments: {
43-
projectId,
44-
username: userName,
45-
password: "testpassword",
46-
roles: [
47-
{
48-
roleName: "readWrite",
49-
databaseName: "admin",
50-
},
51-
],
52-
},
53-
})) as CallToolResult;
54-
expect(response.content).toBeArray();
55-
expect(response.content).toHaveLength(1);
56-
expect(response.content[0].text).toContain("created sucessfully");
63+
it("should create a database user with supplied password", async () => {
64+
const response = await createUserWithMCP("testpassword");
65+
66+
const elements = getResponseElements(response);
67+
expect(elements).toHaveLength(1);
68+
expect(elements[0].text).toContain("created successfully");
69+
expect(elements[0].text).toContain(userName);
70+
expect(elements[0].text).not.toContain("testpassword");
71+
});
72+
73+
it("should create a database user with generated password", async () => {
74+
const response = await createUserWithMCP();
75+
const elements = getResponseElements(response);
76+
expect(elements).toHaveLength(1);
77+
expect(elements[0].text).toContain("created successfully");
78+
expect(elements[0].text).toContain(userName);
79+
expect(elements[0].text).toContain("with password: `");
5780
});
5881
});
5982
describe("atlas-list-db-users", () => {
@@ -68,6 +91,8 @@ describeWithAtlas("db users", (integration) => {
6891
it("returns database users by project", async () => {
6992
const projectId = getProjectId();
7093

94+
await createUserWithMCP();
95+
7196
const response = (await integration
7297
.mcpClient()
7398
.callTool({ name: "atlas-list-db-users", arguments: { projectId } })) as CallToolResult;

0 commit comments

Comments
 (0)