Skip to content
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

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fff804c
Discord slash command code to grant AWS access
vikhyat187 Oct 19, 2024
0421825
Merge branch 'develop' into grant-aws-access
vikhyat187 Oct 19, 2024
f3d33e3
added test cases for the discord command
vikhyat187 Oct 19, 2024
50579bf
Merge branch 'Real-Dev-Squad:grant-aws-access' into grant-aws-access
vikhyat187 Oct 19, 2024
87a08d7
Merge pull request #277 from vikhyat187/grant-aws-access
vikhyat187 Oct 19, 2024
b9e8d03
Merge branch 'develop' into grant-aws-access
vikhyat187 Oct 19, 2024
9a958f6
added uuid as types
vikhyat187 Oct 19, 2024
7433c40
Merge branch 'Real-Dev-Squad:grant-aws-access' into grant-aws-access
vikhyat187 Oct 19, 2024
90f46f4
Merge pull request #278 from vikhyat187/grant-aws-access
vikhyat187 Oct 19, 2024
4b218f0
Merge branch 'develop' into grant-aws-access
vikhyat187 Oct 23, 2024
725e491
Revert "remove feature flag (#275)" (#281)
vinit717 Oct 20, 2024
0d5a3fd
removed uuid, code refactoring and fixed test cases post changes
vikhyat187 Oct 25, 2024
7130af6
Merge branch 'Real-Dev-Squad:grant-aws-access' into grant-aws-access
vikhyat187 Oct 25, 2024
49f45f0
Merge pull request #283 from vikhyat187/grant-aws-access
vikhyat187 Oct 25, 2024
75875f6
correcting package.json file
vikhyat187 Oct 25, 2024
fa37aa3
Merge pull request #284 from vikhyat187/grant-aws-access
vikhyat187 Oct 25, 2024
fb649cf
lint fix
vikhyat187 Oct 25, 2024
fd71a85
code refactoring to call the API outside if/else
vikhyat187 Oct 26, 2024
89a8350
Updated the command options to valid one
vikhyat187 Oct 29, 2024
fddc63a
fix test cases
vikhyat187 Oct 29, 2024
2bc33d0
Fixing test case - changing the return type to Promise<void>
vikhyat187 Oct 29, 2024
a9f8156
lint fix
vikhyat187 Oct 29, 2024
ef03bd0
Updated the logic of signing JWT into seperate file
vikhyat187 Nov 1, 2024
1ebb5f2
added documentation for website backend API
vikhyat187 Nov 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"format-fix": "prettier --write .",
"fix": "npm run lint-fix && npm run format-fix",
"ngrok": "ngrok http 8787",
"register": "ts-node-esm src/register.ts"
"register": "npx ts-node src/register.ts"
Copy link
Author

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.

},
"keywords": [],
"author": "",
Expand Down
28 changes: 28 additions & 0 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { config } from "dotenv";

config();

export const HELLO = {
name: "hello",
description: "Replies with hello in the channel",
Expand Down Expand Up @@ -27,6 +31,30 @@ 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,
},
],
},
],
};

export const MENTION_EACH = {
name: "mention-each",
Expand Down
1 change: 1 addition & 0 deletions src/constants/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
14 changes: 14 additions & 0 deletions src/controllers/baseHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down Expand Up @@ -79,6 +81,18 @@ export async function baseHandler(
return await mentionEachUser(transformedArgument, env, ctx);
}

case getCommandName(GRANT_AWS_ACCESS): {
const data = message.data?.options as Array<messageRequestDataOptions>;
const transformedArgument = {
member: message.member,
userDetails: data[0],
awsGroupDetails: data[1],
channelId: message.channel_id,
};

return await grantAWSAccessCommand(transformedArgument, env, ctx);
}

case getCommandName(REMOVE): {
const data = message.data?.options as Array<messageRequestDataOptions>;
const transformedArgument = {
Expand Down
32 changes: 32 additions & 0 deletions src/controllers/grantAWSAccessCommand.ts
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);
}
2 changes: 2 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -41,6 +42,7 @@ async function registerGuildCommands(
NOTIFY_ONBOARDING,
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
];

try {
Expand Down
89 changes: 89 additions & 0 deletions src/utils/awsAccess.ts
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we already have this? why write it again?

Copy link
Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The 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 },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the name here?

Copy link
Author

@vikhyat187 vikhyat187 Oct 31, 2024

Choose a reason for hiding this comment

The 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.

Ref : https://github.com/Real-Dev-Squad/website-backend/blob/f8d76e1936647759ef7fdfd513f7ff03de226346/middlewares/authorizeBot.js#L18

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is a string helping in validating the right source?

Copy link
Author

Choose a reason for hiding this comment

The 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.
@prakashchoudhary07

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to discuss on this one

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the URL to a separate constant sir

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please name it more appropriately

Copy link
Author

Choose a reason for hiding this comment

The 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,
}),
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Author

Choose a reason for hiding this comment

The 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;
}
113 changes: 113 additions & 0 deletions tests/unit/handlers/grantAwsAccessCommand.test.ts
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");
});
});
Loading