-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Discord slash command code to grant AWS access #276
base: develop
Are you sure you want to change the base?
Changes from 22 commits
fff804c
0421825
f3d33e3
50579bf
87a08d7
b9e8d03
9a958f6
7433c40
90f46f4
4b218f0
725e491
0d5a3fd
7130af6
49f45f0
75875f6
fa37aa3
fb649cf
fd71a85
89a8350
fddc63a
2bc33d0
a9f8156
ef03bd0
1ebb5f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { discordTextResponse } from "../utils/discordResponse"; | ||
import { SUPER_USER_ONE, SUPER_USER_TWO } from "../constants/variables"; | ||
import { env } from "../typeDefinitions/default.types"; | ||
import { | ||
messageRequestMember, | ||
messageRequestDataOptions, | ||
} from "../typeDefinitions/discordMessage.types"; | ||
import { grantAWSAccess } from "../utils/awsAccess"; | ||
|
||
export async function grantAWSAccessCommand( | ||
transformedArgument: { | ||
member: messageRequestMember; | ||
userDetails: messageRequestDataOptions; | ||
awsGroupDetails: messageRequestDataOptions; | ||
channelId: number; | ||
}, | ||
env: env, | ||
ctx: ExecutionContext | ||
) { | ||
const isUserSuperUser = [SUPER_USER_ONE, SUPER_USER_TWO].includes( | ||
transformedArgument.member.user.id.toString() | ||
); | ||
if (!isUserSuperUser) { | ||
const responseText = `You're not authorized to make this request.`; | ||
return discordTextResponse(responseText); | ||
} | ||
const roleId = transformedArgument.userDetails.value; | ||
const groupId = transformedArgument.awsGroupDetails.value; | ||
const channelId = transformedArgument.channelId; | ||
|
||
return grantAWSAccess(roleId, groupId, env, ctx, channelId); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import jwt from "@tsndr/cloudflare-worker-jwt"; | ||
import { env } from "../typeDefinitions/default.types"; | ||
import config from "../../config/config"; | ||
import { discordTextResponse } from "./discordResponse"; | ||
import { DISCORD_BASE_URL, AWS_IAM_SIGNIN_URL } from "../constants/urls"; | ||
|
||
export async function processAWSAccessRequest( | ||
discordUserId: string, | ||
awsGroupId: string, | ||
env: env, | ||
channelId: number | ||
): Promise<void> { | ||
const authToken = await jwt.sign( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't we already have this? why write it again? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. didn't get this comment, we are using this in similar manner in one of the other API too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So at every place we are manually signing, So don't already have a common thing to do this, if not then please create one? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have created a common utility for this. |
||
{ name: "Cloudflare Worker", exp: Math.floor(Date.now() / 1000) + 2 }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the name here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in the website backend, we check if the auth token has the name "cloudfare workers" to validate the token coming from right source. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how is a string helping in validating the right source? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So we create a token in the discord slash commands with name present in payload, this is signed by using the private key and in website backend we validate if the same text is being received post validating the token. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to discuss on this one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure @prakashchoudhary07 |
||
env.BOT_PRIVATE_KEY, | ||
{ algorithm: "RS256" } | ||
); | ||
|
||
try { | ||
const base_url = config(env).RDS_BASE_API_URL; | ||
const requestData = { | ||
groupId: awsGroupId, | ||
userId: discordUserId, | ||
}; | ||
|
||
const url = `${base_url}/aws-access`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move the URL to a separate constant sir There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also please name it more appropriately There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have created as a constant in the same file as we can use the base URL depending on the env as per config |
||
|
||
const response = await fetch(url, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Bearer ${authToken}`, | ||
}, | ||
body: JSON.stringify(requestData), | ||
}); | ||
|
||
let content = ""; | ||
if (!response.ok) { | ||
const responseText = await response.text(); | ||
const errorData = JSON.parse(responseText); | ||
content = `<@${discordUserId}> Error occurred while granting AWS access: ${errorData.error}`; | ||
} else { | ||
content = `AWS access granted successfully <@${discordUserId}>! Please head over to AWS - ${AWS_IAM_SIGNIN_URL}.`; | ||
} | ||
await fetch(`${DISCORD_BASE_URL}/channels/${channelId}/messages`, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Bot ${env.DISCORD_TOKEN}`, | ||
}, | ||
body: JSON.stringify({ | ||
content: content, | ||
}), | ||
}); | ||
} catch (err) { | ||
const content = `<@${discordUserId}> Error occurred while granting AWS access.`; | ||
await fetch(`${DISCORD_BASE_URL}/channels/${channelId}/messages`, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: `Bot ${env.DISCORD_TOKEN}`, | ||
}, | ||
body: JSON.stringify({ | ||
content: content, | ||
}), | ||
}); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RE: as same, please refactor this. Also please maintain a separate constant for URL's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done updated |
||
|
||
export async function grantAWSAccess( | ||
discordUserId: string, | ||
awsGroupId: string, | ||
env: env, | ||
ctx: ExecutionContext, | ||
channelId: number | ||
) { | ||
// Immediately send a Discord response to acknowledge the command | ||
const initialResponse = discordTextResponse( | ||
`<@${discordUserId}> Processing your request to grant AWS access.` | ||
); | ||
|
||
ctx.waitUntil( | ||
// Asynchronously call the function to grant AWS access | ||
processAWSAccessRequest(discordUserId, awsGroupId, env, channelId) | ||
); | ||
|
||
// Return the immediate response within 3 seconds | ||
return initialResponse; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import { | ||
grantAWSAccess, | ||
processAWSAccessRequest, | ||
} from "../../../src/utils/awsAccess"; | ||
import { discordTextResponse } from "../../../src/utils/discordResponse"; | ||
import jwt from "@tsndr/cloudflare-worker-jwt"; | ||
import { AWS_IAM_SIGNIN_URL } from "../../../src/constants/urls"; | ||
|
||
jest.mock("node-fetch"); | ||
jest.mock("@tsndr/cloudflare-worker-jwt"); | ||
jest.mock("../../../src/utils/discordResponse", () => ({ | ||
discordTextResponse: jest.fn(), | ||
})); | ||
|
||
const discordUserId = "test-user"; | ||
const awsGroupId = "test-group"; | ||
const env = { | ||
BOT_PRIVATE_KEY: "mock-bot-private-key", | ||
DISCORD_TOKEN: "mock-discord-token", | ||
RDS_BASE_API_URL: "https://mock-api-url.com", | ||
}; | ||
const channelId = 123456789; | ||
const ctx = { | ||
waitUntil: jest.fn(), | ||
passThroughOnException: jest.fn(), | ||
}; | ||
let fetchSpy: jest.SpyInstance; | ||
|
||
beforeEach(() => { | ||
fetchSpy = jest.spyOn(global, "fetch"); | ||
jest.spyOn(jwt, "sign").mockResolvedValue("mockJwtToken"); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe("ProcessAWSAccessRequest", () => { | ||
it("Should be a JSON response", async () => { | ||
const mockResponse = { content: "Processing your request..." }; | ||
(discordTextResponse as jest.Mock).mockReturnValue(mockResponse); | ||
const response = await grantAWSAccess( | ||
discordUserId, | ||
awsGroupId, | ||
env, | ||
ctx, | ||
channelId | ||
); | ||
expect(discordTextResponse).toHaveBeenCalledWith( | ||
`<@${discordUserId}> Processing your request to grant AWS access.` | ||
); | ||
|
||
// Ensure the function returns the mocked response | ||
expect(response).toEqual(mockResponse); | ||
expect(ctx.waitUntil).toHaveBeenCalled(); // Ensure waitUntil is called | ||
}); | ||
|
||
it("should handle succesful API call and grant access", async () => { | ||
const fetchCalls: string[] = []; | ||
fetchSpy.mockImplementation((url, options) => { | ||
fetchCalls.push(`Fetch call to: ${url}`); | ||
if (url.includes("/aws-access")) { | ||
return Promise.resolve({ ok: true } as Response); | ||
} else if (url.includes("/channels/123456789/messages")) { | ||
return Promise.resolve({ ok: true } as Response); | ||
} | ||
return Promise.reject(new Error("Unexpected URL")); | ||
}); | ||
|
||
await processAWSAccessRequest( | ||
discordUserId, | ||
awsGroupId, | ||
env as any, | ||
channelId | ||
); | ||
|
||
expect(fetchSpy).toHaveBeenCalledTimes(2); | ||
expect(fetchCalls).toHaveLength(2); | ||
|
||
expect(fetchCalls[0]).toContain("/aws-access"); | ||
expect(fetchCalls[1]).toContain("/channels/123456789/messages"); | ||
}); | ||
|
||
it("should handle API error", async () => { | ||
const fetchCalls: string[] = []; | ||
fetchSpy.mockImplementation((url, options) => { | ||
fetchCalls.push(`Fetch call to: ${url}`); | ||
if (url.includes("/aws-access")) { | ||
return Promise.resolve({ | ||
ok: false, | ||
status: 500, | ||
statusText: "Internal Server Error", | ||
} as Response); | ||
} else if (url.includes(`/channels/123456789/messages`)) { | ||
return Promise.resolve({ ok: true } as Response); | ||
} | ||
return Promise.reject(new Error("Unexpected URL")); | ||
}); | ||
|
||
await processAWSAccessRequest( | ||
discordUserId, | ||
awsGroupId, | ||
env as any, | ||
channelId | ||
); | ||
|
||
expect(fetchSpy).toHaveBeenCalledTimes(2); | ||
expect(fetchCalls).toHaveLength(2); | ||
|
||
expect(fetchCalls[0]).toContain("/aws-access"); | ||
expect(fetchCalls[1]).toContain("/channels/123456789/messages"); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed this command as this is causing problem in local development.