Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/register-commands-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -41,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
Expand All @@ -61,6 +63,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}}
Expand All @@ -71,3 +74,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}}
4 changes: 4 additions & 0 deletions .github/workflows/register-commands-staging.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -41,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
Expand All @@ -61,6 +63,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}}
Expand All @@ -71,3 +74,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}}
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
34 changes: 34 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,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",
Expand Down
1 change: 1 addition & 0 deletions src/constants/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
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
15 changes: 15 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,19 @@ 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,
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<messageRequestDataOptions>;
const transformedArgument = {
Expand Down
44 changes: 44 additions & 0 deletions src/controllers/deleteGuildRoleHandler.ts
Original file line number Diff line number Diff line change
@@ -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,
});
}
}
38 changes: 38 additions & 0 deletions src/controllers/grantAWSAccessCommand.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
Expand Down
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 = "";
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;
}
Loading
Loading