fix(#183): remove hardcoded workspace ID from Discord service
Remove critical security vulnerability where Discord service used hardcoded "default-workspace" ID, bypassing Row-Level Security policies and creating potential for cross-tenant data leakage. Changes: - Add DISCORD_WORKSPACE_ID environment variable requirement - Add validation in connect() to require workspace configuration - Replace hardcoded workspace ID with configured value - Add 3 new tests for workspace configuration - Update .env.example with security documentation Security Impact: - Multi-tenant isolation now properly enforced - Each Discord bot instance must be configured for specific workspace - Service fails fast if workspace ID not configured Breaking Change: - Existing deployments must set DISCORD_WORKSPACE_ID environment variable Tests: All 21 Discord service tests passing (100%) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,7 @@ describe("DiscordService", () => {
|
||||
process.env.DISCORD_BOT_TOKEN = "test-token";
|
||||
process.env.DISCORD_GUILD_ID = "test-guild-id";
|
||||
process.env.DISCORD_CONTROL_CHANNEL_ID = "test-channel-id";
|
||||
process.env.DISCORD_WORKSPACE_ID = "test-workspace-id";
|
||||
|
||||
// Clear ready callbacks
|
||||
mockReadyCallbacks.length = 0;
|
||||
@@ -389,7 +390,7 @@ describe("DiscordService", () => {
|
||||
});
|
||||
|
||||
expect(stitcherService.dispatchJob).toHaveBeenCalledWith({
|
||||
workspaceId: "default-workspace",
|
||||
workspaceId: "test-workspace-id",
|
||||
type: "code-task",
|
||||
priority: 10,
|
||||
metadata: {
|
||||
@@ -452,10 +453,84 @@ describe("DiscordService", () => {
|
||||
process.env.DISCORD_BOT_TOKEN = "test-token";
|
||||
});
|
||||
|
||||
it("should use default workspace if not configured", async () => {
|
||||
// This is tested through the handleCommand test above
|
||||
// which verifies workspaceId: 'default-workspace'
|
||||
expect(true).toBe(true);
|
||||
it("should throw error if DISCORD_WORKSPACE_ID is not set", async () => {
|
||||
delete process.env.DISCORD_WORKSPACE_ID;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DiscordService,
|
||||
{
|
||||
provide: StitcherService,
|
||||
useValue: mockStitcherService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const newService = module.get<DiscordService>(DiscordService);
|
||||
|
||||
await expect(newService.connect()).rejects.toThrow("DISCORD_WORKSPACE_ID is required");
|
||||
|
||||
// Restore for other tests
|
||||
process.env.DISCORD_WORKSPACE_ID = "test-workspace-id";
|
||||
});
|
||||
|
||||
it("should use configured workspace ID from environment", async () => {
|
||||
const testWorkspaceId = "configured-workspace-123";
|
||||
process.env.DISCORD_WORKSPACE_ID = testWorkspaceId;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DiscordService,
|
||||
{
|
||||
provide: StitcherService,
|
||||
useValue: mockStitcherService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
const newService = module.get<DiscordService>(DiscordService);
|
||||
|
||||
const message: ChatMessage = {
|
||||
id: "msg-1",
|
||||
channelId: "test-channel-id",
|
||||
authorId: "user-1",
|
||||
authorName: "TestUser",
|
||||
content: "@mosaic fix 42",
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
const mockThread = {
|
||||
id: "thread-123",
|
||||
send: vi.fn(),
|
||||
isThread: () => true,
|
||||
};
|
||||
|
||||
const mockChannel = {
|
||||
isTextBased: () => true,
|
||||
threads: {
|
||||
create: vi.fn().mockResolvedValue(mockThread),
|
||||
},
|
||||
};
|
||||
|
||||
(mockClient.channels.fetch as any)
|
||||
.mockResolvedValueOnce(mockChannel)
|
||||
.mockResolvedValueOnce(mockThread);
|
||||
|
||||
await newService.connect();
|
||||
await newService.handleCommand({
|
||||
command: "fix",
|
||||
args: ["42"],
|
||||
message,
|
||||
});
|
||||
|
||||
expect(mockStitcherService.dispatchJob).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceId: testWorkspaceId,
|
||||
})
|
||||
);
|
||||
|
||||
// Restore for other tests
|
||||
process.env.DISCORD_WORKSPACE_ID = "test-workspace-id";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,10 +26,12 @@ export class DiscordService implements IChatProvider {
|
||||
private connected = false;
|
||||
private readonly botToken: string;
|
||||
private readonly controlChannelId: string;
|
||||
private readonly workspaceId: string;
|
||||
|
||||
constructor(private readonly stitcherService: StitcherService) {
|
||||
this.botToken = process.env.DISCORD_BOT_TOKEN ?? "";
|
||||
this.controlChannelId = process.env.DISCORD_CONTROL_CHANNEL_ID ?? "";
|
||||
this.workspaceId = process.env.DISCORD_WORKSPACE_ID ?? "";
|
||||
|
||||
// Initialize Discord client with required intents
|
||||
this.client = new Client({
|
||||
@@ -91,6 +93,10 @@ export class DiscordService implements IChatProvider {
|
||||
throw new Error("DISCORD_BOT_TOKEN is required");
|
||||
}
|
||||
|
||||
if (!this.workspaceId) {
|
||||
throw new Error("DISCORD_WORKSPACE_ID is required");
|
||||
}
|
||||
|
||||
this.logger.log("Connecting to Discord...");
|
||||
await this.client.login(this.botToken);
|
||||
}
|
||||
@@ -280,7 +286,7 @@ export class DiscordService implements IChatProvider {
|
||||
|
||||
// Dispatch job to stitcher
|
||||
const result = await this.stitcherService.dispatchJob({
|
||||
workspaceId: "default-workspace", // TODO: Get from configuration
|
||||
workspaceId: this.workspaceId,
|
||||
type: "code-task",
|
||||
priority: 10,
|
||||
metadata: {
|
||||
|
||||
Reference in New Issue
Block a user