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

Merged
merged 36 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 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
7b72896
added feature flag to backend API
vikhyat187 Nov 7, 2024
865d751
Merge branch 'develop' into grant-aws-access
vikhyat187 Nov 7, 2024
a5c84a8
Updated the backend route to /aws/groups/access
vikhyat187 Nov 8, 2024
7016a87
Reverted the register command change
vikhyat187 Nov 10, 2024
dd153f3
remove package lock changes
vikhyat187 Nov 10, 2024
7cc0e36
added the group id to config file and added more info to the async pr…
vikhyat187 Nov 11, 2024
cb6770b
remove the unused import
vikhyat187 Nov 11, 2024
b70a6b6
Merge branch 'develop' into grant-aws-access
vikhyat187 Nov 12, 2024
dab6d0f
added feature flag
vikhyat187 Nov 13, 2024
e04b977
Merge branch 'grant-aws-access' of https://github.com/Real-Dev-Squad/…
vikhyat187 Nov 13, 2024
82f1af8
fix the register command, as the value of ENV variable was being pass…
vikhyat187 Nov 13, 2024
3239471
code refactor and made FF mandatory
vikhyat187 Nov 13, 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: 3 additions & 0 deletions config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
24 changes: 24 additions & 0 deletions src/constants/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 @@ -82,6 +84,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(
samarpan1738 marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -42,6 +43,7 @@ async function registerGuildCommands(
NOTIFY_ONBOARDING,
REMOVE,
GROUP_INVITE,
GRANT_AWS_ACCESS,
];

try {
Expand Down
12 changes: 12 additions & 0 deletions src/utils/authTokenGenerator.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
98 changes: 98 additions & 0 deletions src/utils/awsAccess.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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 = "";
Copy link
Member

Choose a reason for hiding this comment

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

why we can't directly use content:= in line 50

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We had two values of content, so declared the variable before and assigning the values depending on the condition.

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,
}),
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved
});
}
}

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
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved
processAWSAccessRequest(discordUserId, awsGroupId, env, channelId)
);

// Return the immediate response within 3 seconds
vikhyat187 marked this conversation as resolved.
Show resolved Hide resolved
return initialResponse;
}
8 changes: 5 additions & 3 deletions src/utils/sendUserDiscordData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down
112 changes: 112 additions & 0 deletions tests/unit/handlers/grantAwsAccessCommand.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
samarpan1738 marked this conversation as resolved.
Show resolved Hide resolved
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");
});
});
Loading