From 79dd9ba58156cbbc4503cd883371cee38043d211 Mon Sep 17 00:00:00 2001 From: Pankaj Date: Tue, 12 Nov 2024 21:28:01 +0530 Subject: [PATCH 1/4] Create an api to delete a role from the discord server (#282) * feat: add deleteGuildRole api route definition and handler * fix: fix deleteGuildRole util and tests * fix: return success and error message * fix: clear mocks before each test and refactor tests * refactor: refactor deleteGuildRole util and tests in separate file * refactor: refactor deleteGuildRoleHandler and tests * fix: import deleteGuildRoleHandler from correct file * fix: remove ternary condition * fix: change api name to /roles/:roleId * refactor: change variable name for better readability * fix: log error messages for debugging * fix: change error response and validation order --- src/constants/responses.ts | 1 + src/controllers/deleteGuildRoleHandler.ts | 44 ++++++ src/index.ts | 3 + src/utils/deleteGuildRole.ts | 30 ++++ .../handlers/deleteGuildRoleHandler.test.ts | 129 ++++++++++++++++++ tests/unit/utils/deleteGuildRole.test.ts | 56 ++++++++ 6 files changed, 263 insertions(+) create mode 100644 src/controllers/deleteGuildRoleHandler.ts create mode 100644 src/utils/deleteGuildRole.ts create mode 100644 tests/unit/handlers/deleteGuildRoleHandler.test.ts create mode 100644 tests/unit/utils/deleteGuildRole.test.ts diff --git a/src/constants/responses.ts b/src/constants/responses.ts index 92dd8e2f..91d38e67 100644 --- a/src/constants/responses.ts +++ b/src/constants/responses.ts @@ -82,3 +82,4 @@ export const INVALID_TOKEN_FORMAT = export const AUTHENTICATION_ERROR = "Invalid Authentication token"; export const TASK_UPDATE_SENT_MESSAGE = "Task update sent on Discord's tracking-updates channel."; +export const NOT_IMPLEMENTED = "Feature not implemented"; diff --git a/src/controllers/deleteGuildRoleHandler.ts b/src/controllers/deleteGuildRoleHandler.ts new file mode 100644 index 00000000..952d6f12 --- /dev/null +++ b/src/controllers/deleteGuildRoleHandler.ts @@ -0,0 +1,44 @@ +import { IRequest } from "itty-router"; +import JSONResponse from "../utils/JsonResponse"; +import { env } from "../typeDefinitions/default.types"; +import * as response from "../constants/responses"; +import { verifyNodejsBackendAuthToken } from "../utils/verifyAuthToken"; +import { deleteGuildRole } from "../utils/deleteGuildRole"; + +export async function deleteGuildRoleHandler(request: IRequest, env: env) { + const authHeader = request.headers.get("Authorization"); + const reason = request.headers.get("X-Audit-Log-Reason"); + const roleId = decodeURI(request.params?.roleId ?? ""); + const { dev } = request.query; + const devFlag = dev === "true"; + + if (!authHeader) { + return new JSONResponse(response.BAD_SIGNATURE, { status: 401 }); + } + + if (!devFlag) { + return new JSONResponse(response.NOT_IMPLEMENTED, { status: 501 }); + } + + if (!roleId) { + return new JSONResponse(response.BAD_REQUEST, { status: 400 }); + } + + try { + await verifyNodejsBackendAuthToken(authHeader, env); + const result = await deleteGuildRole(env, roleId, reason); + + if (result === response.ROLE_REMOVED) { + return new Response(null, { status: 204 }); + } else { + return new JSONResponse(response.INTERNAL_SERVER_ERROR, { + status: 500, + }); + } + } catch (err) { + console.error("An error occurred while deleting discord role:", err); + return new JSONResponse(response.INTERNAL_SERVER_ERROR, { + status: 500, + }); + } +} diff --git a/src/index.ts b/src/index.ts index 966260a9..e94c3608 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ import { sendProfileBlockedMessage } from "./controllers/profileHandler"; import { sendTaskUpdatesHandler } from "./controllers/taskUpdatesHandler"; import config, { loadEnv } from "./../config/config"; +import { deleteGuildRoleHandler } from "./controllers/deleteGuildRoleHandler"; const router = Router(); @@ -34,6 +35,8 @@ router.get("/", async () => { router.patch("/guild/member", changeNickname); +router.delete("/roles/:roleId", deleteGuildRoleHandler); + router.put("/roles/create", createGuildRoleHandler); router.put("/roles/add", addGroupRoleHandler); diff --git a/src/utils/deleteGuildRole.ts b/src/utils/deleteGuildRole.ts new file mode 100644 index 00000000..1f3daa52 --- /dev/null +++ b/src/utils/deleteGuildRole.ts @@ -0,0 +1,30 @@ +import { INTERNAL_SERVER_ERROR, ROLE_REMOVED } from "../constants/responses"; +import { DISCORD_BASE_URL } from "../constants/urls"; +import { env } from "../typeDefinitions/default.types"; +import createDiscordHeaders from "./createDiscordHeaders"; + +export async function deleteGuildRole( + env: env, + roleId: string, + reason?: string +) { + const deleteGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${env.DISCORD_GUILD_ID}/roles/${roleId}`; + const headers: HeadersInit = createDiscordHeaders({ + token: env.DISCORD_TOKEN, + reason: reason, + }); + try { + const response = await fetch(deleteGuildRoleUrl, { + method: "DELETE", + headers, + }); + if (response.ok) { + return ROLE_REMOVED; + } else { + return INTERNAL_SERVER_ERROR; + } + } catch (err) { + console.error("An error occurred while deleting discord role:", err); + return INTERNAL_SERVER_ERROR; + } +} diff --git a/tests/unit/handlers/deleteGuildRoleHandler.test.ts b/tests/unit/handlers/deleteGuildRoleHandler.test.ts new file mode 100644 index 00000000..890a924a --- /dev/null +++ b/tests/unit/handlers/deleteGuildRoleHandler.test.ts @@ -0,0 +1,129 @@ +import { generateDummyRequestObject, guildEnv } from "../../fixtures/fixture"; +import * as responseConstants from "../../../src/constants/responses"; +import * as verifyTokenUtils from "../../../src/utils/verifyAuthToken"; +import { deleteGuildRoleHandler } from "../../../src/controllers/deleteGuildRoleHandler"; +import * as deleteGuildRoleUtils from "../../../src/utils/deleteGuildRole"; + +describe("deleteGuildRoleHandler", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + const roleId = "1A32BEX04"; + it("should return NOT_IMPLEMENTED when dev is false", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "false", + }, + method: "DELETE", + headers: { Authorization: "Bearer testtoken" }, + }); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.NOT_IMPLEMENTED); + }); + it("should return BAD_REQUEST when roleId is not valid", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: "", + }, + query: { + dev: "true", + }, + method: "DELETE", + headers: { Authorization: "Bearer testtoken" }, + }); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.BAD_REQUEST); + }); + it("should return BAD_SIGNATURE when authorization header is not provided", async () => { + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + method: "DELETE", + }); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(responseConstants.BAD_SIGNATURE); + }); + it("should return INTERNAL_SERVER_ERROR when response is not ok", async () => { + const expectedResponse = responseConstants.INTERNAL_SERVER_ERROR; + const mockRequest = generateDummyRequestObject({ + url: "/roles", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + method: "DELETE", + headers: { Authorization: "Bearer testtoken" }, + }); + jest + .spyOn(deleteGuildRoleUtils, "deleteGuildRole") + .mockResolvedValue(expectedResponse); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(expectedResponse); + }); + it("should return INTERNAL_SERVER_ERROR when token is not verified", async () => { + const expectedResponse = responseConstants.INTERNAL_SERVER_ERROR; + const mockRequest = generateDummyRequestObject({ + url: "/roles", + method: "DELETE", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + headers: { Authorization: "Bearer testtoken" }, + }); + jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockRejectedValue(expectedResponse); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + const jsonResponse = await response.json(); + expect(jsonResponse).toEqual(expectedResponse); + }); + it("should return ok response", async () => { + const expectedResponse = new Response(null, { + status: 204, + }); + const mockRequest = generateDummyRequestObject({ + url: "/roles", + method: "DELETE", + params: { + roleId: roleId, + }, + query: { + dev: "true", + }, + headers: { Authorization: "Bearer testtoken" }, + }); + const verifyTokenSpy = jest + .spyOn(verifyTokenUtils, "verifyNodejsBackendAuthToken") + .mockResolvedValueOnce(); + const deleteGuildRoleSpy = jest + .spyOn(deleteGuildRoleUtils, "deleteGuildRole") + .mockResolvedValueOnce(responseConstants.ROLE_REMOVED); + const response = await deleteGuildRoleHandler(mockRequest, guildEnv); + expect(verifyTokenSpy).toHaveBeenCalledTimes(1); + expect(deleteGuildRoleSpy).toHaveBeenCalledTimes(1); + expect(response).toEqual(expectedResponse); + expect(response.status).toEqual(expectedResponse.status); + }); +}); diff --git a/tests/unit/utils/deleteGuildRole.test.ts b/tests/unit/utils/deleteGuildRole.test.ts new file mode 100644 index 00000000..98c77864 --- /dev/null +++ b/tests/unit/utils/deleteGuildRole.test.ts @@ -0,0 +1,56 @@ +import { DISCORD_BASE_URL } from "../../../src/constants/urls"; +import { deleteGuildRole } from "../../../src/utils/deleteGuildRole"; +import JSONResponse from "../../../src/utils/JsonResponse"; +import { guildEnv } from "../../fixtures/fixture"; +import * as response from "../../../src/constants/responses"; + +describe("deleteGuildRole", () => { + const roleId = "1A32BEX04"; + const deleteGuildRoleUrl = `${DISCORD_BASE_URL}/guilds/${guildEnv.DISCORD_GUILD_ID}/roles/${roleId}`; + const mockRequestInit = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${guildEnv.DISCORD_TOKEN}`, + "X-Audit-Log-Reason": "This is reason for this action", + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should pass the reason to discord as a X-Audit-Log-Reason header if provided", async () => { + jest + .spyOn(global, "fetch") + .mockImplementation((inp) => Promise.resolve(new JSONResponse(inp))); + + await deleteGuildRole(guildEnv, roleId, "This is reason for this action"); + + expect(global.fetch).toHaveBeenCalledWith( + deleteGuildRoleUrl, + mockRequestInit + ); + }); + + it("should return ROLE_REMOVED when response is ok", async () => { + const expectedResponse = new Response(null, { + status: 204, + }); + jest.spyOn(global, "fetch").mockResolvedValue(expectedResponse); + const result = await deleteGuildRole(guildEnv, roleId); + expect(result).toEqual(response.ROLE_REMOVED); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it("should return INTERNAL_SERVER_ERROR when response is not ok", async () => { + const expectedErrorResponse = new Response(response.INTERNAL_SERVER_ERROR); + jest.spyOn(global, "fetch").mockRejectedValue(expectedErrorResponse); + const result = await deleteGuildRole(guildEnv, roleId); + expect(result).toEqual(response.INTERNAL_SERVER_ERROR); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); +}); From b3ad69df1f20721c05c15e0f133b2e0fcc454a94 Mon Sep 17 00:00:00 2001 From: Vikhyat Bhatnagar <52795644+vikhyat187@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:56:28 +0530 Subject: [PATCH 2/4] Discord slash command code to grant AWS access (#276) * Discord slash command code to grant AWS access * added test cases for the discord command * added uuid as types * Revert "remove feature flag (#275)" (#281) This reverts commit b54d9c3a708b287599214925d385d00f84b59481. * removed uuid, code refactoring and fixed test cases post changes * correcting package.json file * lint fix * code refactoring to call the API outside if/else * Updated the command options to valid one * fix test cases * Fixing test case - changing the return type to Promise * lint fix * Updated the logic of signing JWT into seperate file * added documentation for website backend API * added feature flag to backend API * Updated the backend route to /aws/groups/access * Reverted the register command change * remove package lock changes * added the group id to config file and added more info to the async processing comment * remove the unused import * added feature flag * fix the register command, as the value of ENV variable was being passed as undefined, due to recent change * code refactor and made FF mandatory --------- Co-authored-by: Vinit khandal <111434418+vinit717@users.noreply.github.com> --- config/config.ts | 3 + src/constants/commands.ts | 34 ++++++ src/constants/urls.ts | 1 + src/controllers/baseHandler.ts | 15 +++ src/controllers/grantAWSAccessCommand.ts | 38 ++++++ src/register.ts | 2 + src/utils/authTokenGenerator.ts | 12 ++ src/utils/awsAccess.ts | 98 +++++++++++++++ src/utils/sendUserDiscordData.ts | 8 +- .../handlers/grantAwsAccessCommand.test.ts | 112 ++++++++++++++++++ 10 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 src/controllers/grantAWSAccessCommand.ts create mode 100644 src/utils/authTokenGenerator.ts create mode 100644 src/utils/awsAccess.ts create mode 100644 tests/unit/handlers/grantAwsAccessCommand.test.ts diff --git a/config/config.ts b/config/config.ts index e81afe71..70e8e89e 100644 --- a/config/config.ts +++ b/config/config.ts @@ -56,6 +56,9 @@ export function loadEnv(env: env, fromWorkerEnv: boolean): env { IDENTITY_SERVICE_PUBLIC_KEY: fromWorkerEnv ? env.IDENTITY_SERVICE_PUBLIC_KEY : process.env.IDENTITY_SERVICE_PUBLIC_KEY || "", + AWS_READ_ACCESS_GROUP_ID: fromWorkerEnv + ? env.AWS_READ_ACCESS_GROUP_ID + : process.env.AWS_READ_ACCESS_GROUP_ID || "", }; return Env; } diff --git a/src/constants/commands.ts b/src/constants/commands.ts index d64a965f..5d7d9e31 100644 --- a/src/constants/commands.ts +++ b/src/constants/commands.ts @@ -1,3 +1,7 @@ +import { config } from "dotenv"; + +config(); + export const HELLO = { name: "hello", description: "Replies with hello in the channel", @@ -27,6 +31,36 @@ export const GROUP_INVITE = { }, ], }; +export const GRANT_AWS_ACCESS = { + name: "grant-aws-access", + description: "This command is to grant AWS access to the discord users.", + options: [ + { + name: "user-name", + description: "User to be granted the AWS access", + type: 6, //user Id to be grant the access + required: true, + }, + { + name: "aws-group-name", + description: "AWS group name", + type: 3, + required: true, + choices: [ + { + name: "AWS read access", + value: process.env.AWS_READ_ACCESS_GROUP_ID, + }, + ], + }, + { + name: "dev", + description: "Feature flag", + type: 5, + required: true, + }, + ], +}; export const MENTION_EACH = { name: "mention-each", diff --git a/src/constants/urls.ts b/src/constants/urls.ts index a86b355f..6ebf15e2 100644 --- a/src/constants/urls.ts +++ b/src/constants/urls.ts @@ -3,6 +3,7 @@ export const RDS_BASE_STAGING_API_URL = "https://staging-api.realdevsquad.com"; export const RDS_BASE_DEVELOPMENT_API_URL = "http://localhost:3000"; // If needed, modify the URL to your local API server run through ngrok export const DISCORD_BASE_URL = "https://discord.com/api/v10"; +export const AWS_IAM_SIGNIN_URL = "https://realdevsquad.awsapps.com/start#/"; export const DISCORD_AVATAR_BASE_URL = "https://cdn.discordapp.com/avatars"; export const VERIFICATION_SITE_URL = "https://my.realdevsquad.com"; diff --git a/src/controllers/baseHandler.ts b/src/controllers/baseHandler.ts index 61c5b765..0593f3a3 100644 --- a/src/controllers/baseHandler.ts +++ b/src/controllers/baseHandler.ts @@ -29,6 +29,7 @@ import { USER, REMOVE, GROUP_INVITE, + GRANT_AWS_ACCESS, } from "../constants/commands"; import { updateNickName } from "../utils/updateNickname"; import { discordEphemeralResponse } from "../utils/discordEphemeralResponse"; @@ -44,6 +45,7 @@ import { import { DevFlag } from "../typeDefinitions/filterUsersByRole"; import { kickEachUser } from "./kickEachUser"; import { groupInvite } from "./groupInvite"; +import { grantAWSAccessCommand } from "./grantAWSAccessCommand"; export async function baseHandler( message: discordMessageRequest, @@ -82,6 +84,19 @@ export async function baseHandler( return await mentionEachUser(transformedArgument, env, ctx); } + case getCommandName(GRANT_AWS_ACCESS): { + const data = message.data?.options as Array; + const transformedArgument = { + member: message.member, + userDetails: data[0], + awsGroupDetails: data[1], + channelId: message.channel_id, + dev: data.find((item) => item.name === "dev") as unknown as DevFlag, + }; + + return await grantAWSAccessCommand(transformedArgument, env, ctx); + } + case getCommandName(REMOVE): { const data = message.data?.options as Array; const transformedArgument = { diff --git a/src/controllers/grantAWSAccessCommand.ts b/src/controllers/grantAWSAccessCommand.ts new file mode 100644 index 00000000..86fe9884 --- /dev/null +++ b/src/controllers/grantAWSAccessCommand.ts @@ -0,0 +1,38 @@ +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"; +import { DevFlag } from "../typeDefinitions/filterUsersByRole"; + +export async function grantAWSAccessCommand( + transformedArgument: { + member: messageRequestMember; + userDetails: messageRequestDataOptions; + awsGroupDetails: messageRequestDataOptions; + channelId: number; + dev?: DevFlag; + }, + env: env, + ctx: ExecutionContext +) { + const dev = transformedArgument?.dev?.value || false; + if (!dev) { + return discordTextResponse("Please enable feature flag to make this work"); + } + 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); +} diff --git a/src/register.ts b/src/register.ts index 8602d543..76786999 100644 --- a/src/register.ts +++ b/src/register.ts @@ -10,6 +10,7 @@ import { USER, REMOVE, GROUP_INVITE, + GRANT_AWS_ACCESS, } from "./constants/commands"; import { config } from "dotenv"; import { DISCORD_BASE_URL } from "./constants/urls"; @@ -42,6 +43,7 @@ async function registerGuildCommands( NOTIFY_ONBOARDING, REMOVE, GROUP_INVITE, + GRANT_AWS_ACCESS, ]; try { diff --git a/src/utils/authTokenGenerator.ts b/src/utils/authTokenGenerator.ts new file mode 100644 index 00000000..ecfd462f --- /dev/null +++ b/src/utils/authTokenGenerator.ts @@ -0,0 +1,12 @@ +import jwt from "@tsndr/cloudflare-worker-jwt"; + +export async function generateDiscordAuthToken( + name: string, + expiry: number, + privateKey: string, + algorithm: string +) { + return await jwt.sign({ name: name, exp: expiry }, privateKey, { + algorithm: algorithm, + }); +} diff --git a/src/utils/awsAccess.ts b/src/utils/awsAccess.ts new file mode 100644 index 00000000..84fb1569 --- /dev/null +++ b/src/utils/awsAccess.ts @@ -0,0 +1,98 @@ +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"; +import { generateDiscordAuthToken } from "./authTokenGenerator"; + +export async function processAWSAccessRequest( + discordUserId: string, + awsGroupId: string, + env: env, + channelId: number +): Promise { + const authToken = await generateDiscordAuthToken( + "Cloudflare Worker", + Math.floor(Date.now() / 1000) + 2, + env.BOT_PRIVATE_KEY, + "RS256" + ); + const discordReplyUrl = `${DISCORD_BASE_URL}/channels/${channelId}/messages`; + const base_url = config(env).RDS_BASE_API_URL; + const grantAWSAccessAPIUrl = `${base_url}/aws/groups/access?dev=true`; + + try { + const requestData = { + groupId: awsGroupId, + userId: discordUserId, + }; + + /** + * Grant AWS access is the API in website backend, + * which takes the discordId and AWS groupId, it fetches the + * user based on the discordId, checks if the user is part of AWS account + * if not creates a new user and adds user to the AWS group. + */ + + const response = await fetch(grantAWSAccessAPIUrl, { + 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(discordReplyUrl, { + 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(discordReplyUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${env.DISCORD_TOKEN}`, + }, + body: JSON.stringify({ + content: content, + }), + }); + } +} + +export async function grantAWSAccess( + discordUserId: string, + awsGroupId: string, + env: env, + ctx: ExecutionContext, + channelId: number +) { + // Immediately send a Discord response to acknowledge the command, as the cloudfare workers have a limit of response time equals to 3s + 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; +} diff --git a/src/utils/sendUserDiscordData.ts b/src/utils/sendUserDiscordData.ts index 29153f36..06a658c0 100644 --- a/src/utils/sendUserDiscordData.ts +++ b/src/utils/sendUserDiscordData.ts @@ -2,6 +2,7 @@ import { env } from "../typeDefinitions/default.types"; import jwt from "@tsndr/cloudflare-worker-jwt"; import { DISCORD_AVATAR_BASE_URL } from "../constants/urls"; import config from "../../config/config"; +import { generateDiscordAuthToken } from "./authTokenGenerator"; export const sendUserDiscordData = async ( token: string, @@ -12,10 +13,11 @@ export const sendUserDiscordData = async ( discordJoinedAt: string, env: env ) => { - const authToken = await jwt.sign( - { name: "Cloudflare Worker", exp: Math.floor(Date.now() / 1000) + 2 }, + const authToken = await generateDiscordAuthToken( + "Cloudflare Worker", + Math.floor(Date.now() / 1000) + 2, env.BOT_PRIVATE_KEY, - { algorithm: "RS256" } + "RS256" ); const data = { type: "discord", diff --git a/tests/unit/handlers/grantAwsAccessCommand.test.ts b/tests/unit/handlers/grantAwsAccessCommand.test.ts new file mode 100644 index 00000000..fed5ae93 --- /dev/null +++ b/tests/unit/handlers/grantAwsAccessCommand.test.ts @@ -0,0 +1,112 @@ +import { + grantAWSAccess, + processAWSAccessRequest, +} from "../../../src/utils/awsAccess"; +import { discordTextResponse } from "../../../src/utils/discordResponse"; +import jwt from "@tsndr/cloudflare-worker-jwt"; + +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/groups/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/groups/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/groups/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/groups/access"); + expect(fetchCalls[1]).toContain("/channels/123456789/messages"); + }); +}); From 54b810e9b321657bf9f7821f8e7fc1ad7a6cf04d Mon Sep 17 00:00:00 2001 From: Vikhyat Bhatnagar <52795644+vikhyat187@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:34:40 +0530 Subject: [PATCH 3/4] added envs to CI file (#291) --- .github/workflows/register-commands-production.yaml | 3 +++ .github/workflows/register-commands-staging.yaml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/register-commands-production.yaml b/.github/workflows/register-commands-production.yaml index 3fb43a32..02f8005a 100644 --- a/.github/workflows/register-commands-production.yaml +++ b/.github/workflows/register-commands-production.yaml @@ -25,6 +25,7 @@ jobs: RDS_SERVERLESS_PUBLIC_KEY: ${{secrets.RDS_SERVERLESS_PUBLIC_KEY}} CRON_JOBS_PUBLIC_KEY: ${{secrets.CRON_JOBS_PUBLIC_KEY}} IDENTITY_SERVICE_PUBLIC_KEY: ${{secrets.IDENTITY_SERVICE_PUBLIC_KEY}} + AWS_READ_ACCESS_GROUP_ID: ${{secrets.AWS_READ_ACCESS_GROUP_ID}} Register-Commands: needs: [Environment-Variables-Check] @@ -61,6 +62,7 @@ jobs: RDS_SERVERLESS_PUBLIC_KEY CRON_JOBS_PUBLIC_KEY IDENTITY_SERVICE_PUBLIC_KEY + AWS_READ_ACCESS_GROUP_ID env: CURRENT_ENVIRONMENT: production CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_API_TOKEN}} @@ -71,3 +73,4 @@ jobs: RDS_SERVERLESS_PUBLIC_KEY: ${{secrets.RDS_SERVERLESS_PUBLIC_KEY}} CRON_JOBS_PUBLIC_KEY: ${{secrets.CRON_JOBS_PUBLIC_KEY}} IDENTITY_SERVICE_PUBLIC_KEY: ${{secrets.IDENTITY_SERVICE_PUBLIC_KEY}} + AWS_READ_ACCESS_GROUP_ID: ${{secrets.AWS_READ_ACCESS_GROUP_ID}} diff --git a/.github/workflows/register-commands-staging.yaml b/.github/workflows/register-commands-staging.yaml index c8c2adc6..95ce990a 100644 --- a/.github/workflows/register-commands-staging.yaml +++ b/.github/workflows/register-commands-staging.yaml @@ -25,6 +25,7 @@ jobs: RDS_SERVERLESS_PUBLIC_KEY: ${{secrets.RDS_SERVERLESS_PUBLIC_KEY}} CRON_JOBS_PUBLIC_KEY: ${{secrets.CRON_JOBS_PUBLIC_KEY}} IDENTITY_SERVICE_PUBLIC_KEY: ${{secrets.IDENTITY_SERVICE_PUBLIC_KEY}} + AWS_READ_ACCESS_GROUP_ID: ${{secrets.AWS_READ_ACCESS_GROUP_ID}} Register-Commands: needs: [Environment-Variables-Check] @@ -61,6 +62,7 @@ jobs: RDS_SERVERLESS_PUBLIC_KEY CRON_JOBS_PUBLIC_KEY IDENTITY_SERVICE_PUBLIC_KEY + AWS_READ_ACCESS_GROUP_ID env: CURRENT_ENVIRONMENT: staging CLOUDFLARE_API_TOKEN: ${{secrets.CLOUDFLARE_API_TOKEN}} @@ -71,3 +73,4 @@ jobs: RDS_SERVERLESS_PUBLIC_KEY: ${{secrets.RDS_SERVERLESS_PUBLIC_KEY}} CRON_JOBS_PUBLIC_KEY: ${{secrets.CRON_JOBS_PUBLIC_KEY}} IDENTITY_SERVICE_PUBLIC_KEY: ${{secrets.IDENTITY_SERVICE_PUBLIC_KEY}} + AWS_READ_ACCESS_GROUP_ID: ${{secrets.AWS_READ_ACCESS_GROUP_ID}} From 0aef740ac158f678de6c8ed38b411a90472995b4 Mon Sep 17 00:00:00 2001 From: Vikhyat Bhatnagar <52795644+vikhyat187@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:59:23 +0530 Subject: [PATCH 4/4] added the key in env (#292) * added the key in env * format-fix --- .github/workflows/register-commands-production.yaml | 1 + .github/workflows/register-commands-staging.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/register-commands-production.yaml b/.github/workflows/register-commands-production.yaml index 02f8005a..d5b6449c 100644 --- a/.github/workflows/register-commands-production.yaml +++ b/.github/workflows/register-commands-production.yaml @@ -42,6 +42,7 @@ jobs: DISCORD_TOKEN: ${{secrets.DISCORD_TOKEN}} DISCORD_APPLICATION_ID: ${{secrets.DISCORD_APPLICATION_ID}} DISCORD_GUILD_ID: ${{secrets.DISCORD_GUILD_ID}} + AWS_READ_ACCESS_GROUP_ID: ${{secrets.AWS_READ_ACCESS_GROUP_ID}} Deploy-to-Cloudflare: needs: [Register-Commands] runs-on: ubuntu-latest diff --git a/.github/workflows/register-commands-staging.yaml b/.github/workflows/register-commands-staging.yaml index 95ce990a..59545705 100644 --- a/.github/workflows/register-commands-staging.yaml +++ b/.github/workflows/register-commands-staging.yaml @@ -42,6 +42,7 @@ jobs: DISCORD_TOKEN: ${{secrets.DISCORD_TOKEN}} DISCORD_APPLICATION_ID: ${{secrets.DISCORD_APPLICATION_ID}} DISCORD_GUILD_ID: ${{secrets.DISCORD_GUILD_ID}} + AWS_READ_ACCESS_GROUP_ID: ${{secrets.AWS_READ_ACCESS_GROUP_ID}} Deploy-to-Cloudflare: needs: [Register-Commands] runs-on: ubuntu-latest