Skip to content

Commit b3ad69d

Browse files
vikhyat187vinit717
andauthored
Discord slash command code to grant AWS access (Real-Dev-Squad#276)
* Discord slash command code to grant AWS access * added test cases for the discord command * added uuid as types * Revert "remove feature flag (Real-Dev-Squad#275)" (Real-Dev-Squad#281) This reverts commit b54d9c3. * 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<void> * 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 <[email protected]>
1 parent 79dd9ba commit b3ad69d

File tree

10 files changed

+320
-3
lines changed

10 files changed

+320
-3
lines changed

config/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export function loadEnv(env: env, fromWorkerEnv: boolean): env {
5656
IDENTITY_SERVICE_PUBLIC_KEY: fromWorkerEnv
5757
? env.IDENTITY_SERVICE_PUBLIC_KEY
5858
: process.env.IDENTITY_SERVICE_PUBLIC_KEY || "",
59+
AWS_READ_ACCESS_GROUP_ID: fromWorkerEnv
60+
? env.AWS_READ_ACCESS_GROUP_ID
61+
: process.env.AWS_READ_ACCESS_GROUP_ID || "",
5962
};
6063
return Env;
6164
}

src/constants/commands.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { config } from "dotenv";
2+
3+
config();
4+
15
export const HELLO = {
26
name: "hello",
37
description: "Replies with hello in the channel",
@@ -27,6 +31,36 @@ export const GROUP_INVITE = {
2731
},
2832
],
2933
};
34+
export const GRANT_AWS_ACCESS = {
35+
name: "grant-aws-access",
36+
description: "This command is to grant AWS access to the discord users.",
37+
options: [
38+
{
39+
name: "user-name",
40+
description: "User to be granted the AWS access",
41+
type: 6, //user Id to be grant the access
42+
required: true,
43+
},
44+
{
45+
name: "aws-group-name",
46+
description: "AWS group name",
47+
type: 3,
48+
required: true,
49+
choices: [
50+
{
51+
name: "AWS read access",
52+
value: process.env.AWS_READ_ACCESS_GROUP_ID,
53+
},
54+
],
55+
},
56+
{
57+
name: "dev",
58+
description: "Feature flag",
59+
type: 5,
60+
required: true,
61+
},
62+
],
63+
};
3064

3165
export const MENTION_EACH = {
3266
name: "mention-each",

src/constants/urls.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const RDS_BASE_STAGING_API_URL = "https://staging-api.realdevsquad.com";
33
export const RDS_BASE_DEVELOPMENT_API_URL = "http://localhost:3000"; // If needed, modify the URL to your local API server run through ngrok
44

55
export const DISCORD_BASE_URL = "https://discord.com/api/v10";
6+
export const AWS_IAM_SIGNIN_URL = "https://realdevsquad.awsapps.com/start#/";
67
export const DISCORD_AVATAR_BASE_URL = "https://cdn.discordapp.com/avatars";
78

89
export const VERIFICATION_SITE_URL = "https://my.realdevsquad.com";

src/controllers/baseHandler.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
USER,
3030
REMOVE,
3131
GROUP_INVITE,
32+
GRANT_AWS_ACCESS,
3233
} from "../constants/commands";
3334
import { updateNickName } from "../utils/updateNickname";
3435
import { discordEphemeralResponse } from "../utils/discordEphemeralResponse";
@@ -44,6 +45,7 @@ import {
4445
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
4546
import { kickEachUser } from "./kickEachUser";
4647
import { groupInvite } from "./groupInvite";
48+
import { grantAWSAccessCommand } from "./grantAWSAccessCommand";
4749

4850
export async function baseHandler(
4951
message: discordMessageRequest,
@@ -82,6 +84,19 @@ export async function baseHandler(
8284
return await mentionEachUser(transformedArgument, env, ctx);
8385
}
8486

87+
case getCommandName(GRANT_AWS_ACCESS): {
88+
const data = message.data?.options as Array<messageRequestDataOptions>;
89+
const transformedArgument = {
90+
member: message.member,
91+
userDetails: data[0],
92+
awsGroupDetails: data[1],
93+
channelId: message.channel_id,
94+
dev: data.find((item) => item.name === "dev") as unknown as DevFlag,
95+
};
96+
97+
return await grantAWSAccessCommand(transformedArgument, env, ctx);
98+
}
99+
85100
case getCommandName(REMOVE): {
86101
const data = message.data?.options as Array<messageRequestDataOptions>;
87102
const transformedArgument = {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { discordTextResponse } from "../utils/discordResponse";
2+
import { SUPER_USER_ONE, SUPER_USER_TWO } from "../constants/variables";
3+
import { env } from "../typeDefinitions/default.types";
4+
import {
5+
messageRequestMember,
6+
messageRequestDataOptions,
7+
} from "../typeDefinitions/discordMessage.types";
8+
import { grantAWSAccess } from "../utils/awsAccess";
9+
import { DevFlag } from "../typeDefinitions/filterUsersByRole";
10+
11+
export async function grantAWSAccessCommand(
12+
transformedArgument: {
13+
member: messageRequestMember;
14+
userDetails: messageRequestDataOptions;
15+
awsGroupDetails: messageRequestDataOptions;
16+
channelId: number;
17+
dev?: DevFlag;
18+
},
19+
env: env,
20+
ctx: ExecutionContext
21+
) {
22+
const dev = transformedArgument?.dev?.value || false;
23+
if (!dev) {
24+
return discordTextResponse("Please enable feature flag to make this work");
25+
}
26+
const isUserSuperUser = [SUPER_USER_ONE, SUPER_USER_TWO].includes(
27+
transformedArgument.member.user.id.toString()
28+
);
29+
if (!isUserSuperUser) {
30+
const responseText = `You're not authorized to make this request.`;
31+
return discordTextResponse(responseText);
32+
}
33+
const roleId = transformedArgument.userDetails.value;
34+
const groupId = transformedArgument.awsGroupDetails.value;
35+
const channelId = transformedArgument.channelId;
36+
37+
return grantAWSAccess(roleId, groupId, env, ctx, channelId);
38+
}

src/register.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
USER,
1111
REMOVE,
1212
GROUP_INVITE,
13+
GRANT_AWS_ACCESS,
1314
} from "./constants/commands";
1415
import { config } from "dotenv";
1516
import { DISCORD_BASE_URL } from "./constants/urls";
@@ -42,6 +43,7 @@ async function registerGuildCommands(
4243
NOTIFY_ONBOARDING,
4344
REMOVE,
4445
GROUP_INVITE,
46+
GRANT_AWS_ACCESS,
4547
];
4648

4749
try {

src/utils/authTokenGenerator.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import jwt from "@tsndr/cloudflare-worker-jwt";
2+
3+
export async function generateDiscordAuthToken(
4+
name: string,
5+
expiry: number,
6+
privateKey: string,
7+
algorithm: string
8+
) {
9+
return await jwt.sign({ name: name, exp: expiry }, privateKey, {
10+
algorithm: algorithm,
11+
});
12+
}

src/utils/awsAccess.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import jwt from "@tsndr/cloudflare-worker-jwt";
2+
import { env } from "../typeDefinitions/default.types";
3+
import config from "../../config/config";
4+
import { discordTextResponse } from "./discordResponse";
5+
import { DISCORD_BASE_URL, AWS_IAM_SIGNIN_URL } from "../constants/urls";
6+
import { generateDiscordAuthToken } from "./authTokenGenerator";
7+
8+
export async function processAWSAccessRequest(
9+
discordUserId: string,
10+
awsGroupId: string,
11+
env: env,
12+
channelId: number
13+
): Promise<void> {
14+
const authToken = await generateDiscordAuthToken(
15+
"Cloudflare Worker",
16+
Math.floor(Date.now() / 1000) + 2,
17+
env.BOT_PRIVATE_KEY,
18+
"RS256"
19+
);
20+
const discordReplyUrl = `${DISCORD_BASE_URL}/channels/${channelId}/messages`;
21+
const base_url = config(env).RDS_BASE_API_URL;
22+
const grantAWSAccessAPIUrl = `${base_url}/aws/groups/access?dev=true`;
23+
24+
try {
25+
const requestData = {
26+
groupId: awsGroupId,
27+
userId: discordUserId,
28+
};
29+
30+
/**
31+
* Grant AWS access is the API in website backend,
32+
* which takes the discordId and AWS groupId, it fetches the
33+
* user based on the discordId, checks if the user is part of AWS account
34+
* if not creates a new user and adds user to the AWS group.
35+
*/
36+
37+
const response = await fetch(grantAWSAccessAPIUrl, {
38+
method: "POST",
39+
headers: {
40+
"Content-Type": "application/json",
41+
Authorization: `Bearer ${authToken}`,
42+
},
43+
body: JSON.stringify(requestData),
44+
});
45+
46+
let content = "";
47+
if (!response.ok) {
48+
const responseText = await response.text();
49+
const errorData = JSON.parse(responseText);
50+
content = `<@${discordUserId}> Error occurred while granting AWS access: ${errorData.error}`;
51+
} else {
52+
content = `AWS access granted successfully <@${discordUserId}>! Please head over to AWS - ${AWS_IAM_SIGNIN_URL}.`;
53+
}
54+
await fetch(discordReplyUrl, {
55+
method: "POST",
56+
headers: {
57+
"Content-Type": "application/json",
58+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
59+
},
60+
body: JSON.stringify({
61+
content: content,
62+
}),
63+
});
64+
} catch (err) {
65+
const content = `<@${discordUserId}> Error occurred while granting AWS access.`;
66+
await fetch(discordReplyUrl, {
67+
method: "POST",
68+
headers: {
69+
"Content-Type": "application/json",
70+
Authorization: `Bot ${env.DISCORD_TOKEN}`,
71+
},
72+
body: JSON.stringify({
73+
content: content,
74+
}),
75+
});
76+
}
77+
}
78+
79+
export async function grantAWSAccess(
80+
discordUserId: string,
81+
awsGroupId: string,
82+
env: env,
83+
ctx: ExecutionContext,
84+
channelId: number
85+
) {
86+
// Immediately send a Discord response to acknowledge the command, as the cloudfare workers have a limit of response time equals to 3s
87+
const initialResponse = discordTextResponse(
88+
`<@${discordUserId}> Processing your request to grant AWS access.`
89+
);
90+
91+
ctx.waitUntil(
92+
// Asynchronously call the function to grant AWS access
93+
processAWSAccessRequest(discordUserId, awsGroupId, env, channelId)
94+
);
95+
96+
// Return the immediate response within 3 seconds
97+
return initialResponse;
98+
}

src/utils/sendUserDiscordData.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { env } from "../typeDefinitions/default.types";
22
import jwt from "@tsndr/cloudflare-worker-jwt";
33
import { DISCORD_AVATAR_BASE_URL } from "../constants/urls";
44
import config from "../../config/config";
5+
import { generateDiscordAuthToken } from "./authTokenGenerator";
56

67
export const sendUserDiscordData = async (
78
token: string,
@@ -12,10 +13,11 @@ export const sendUserDiscordData = async (
1213
discordJoinedAt: string,
1314
env: env
1415
) => {
15-
const authToken = await jwt.sign(
16-
{ name: "Cloudflare Worker", exp: Math.floor(Date.now() / 1000) + 2 },
16+
const authToken = await generateDiscordAuthToken(
17+
"Cloudflare Worker",
18+
Math.floor(Date.now() / 1000) + 2,
1719
env.BOT_PRIVATE_KEY,
18-
{ algorithm: "RS256" }
20+
"RS256"
1921
);
2022
const data = {
2123
type: "discord",

0 commit comments

Comments
 (0)