diff --git a/src/api/routes/iam.ts b/src/api/routes/iam.ts index fc14eb38..c7d0c119 100644 --- a/src/api/routes/iam.ts +++ b/src/api/routes/iam.ts @@ -17,9 +17,13 @@ import { InternalServerError, NotFoundError, } from "../../common/errors/index.js"; -import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb"; +import { + DynamoDBClient, + PutItemCommand, + ScanCommand, +} from "@aws-sdk/client-dynamodb"; import { genericConfig, roleArns } from "../../common/config.js"; -import { marshall } from "@aws-sdk/util-dynamodb"; +import { marshall, unmarshall } from "@aws-sdk/util-dynamodb"; import { invitePostRequestSchema, groupMappingCreatePostSchema, @@ -28,11 +32,9 @@ import { EntraGroupActions, entraGroupMembershipListResponse, entraProfilePatchRequest, + iamGetAllAssignedRolesResponse, } from "../../common/types/iam.js"; -import { - AUTH_DECISION_CACHE_SECONDS, - getGroupRoles, -} from "../functions/authorization.js"; +import { AUTH_DECISION_CACHE_SECONDS } from "../functions/authorization.js"; import { getRoleCredentials } from "api/functions/sts.js"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; import { createAuditLogEntry } from "api/functions/auditLog.js"; @@ -110,32 +112,55 @@ const iamRoutes: FastifyPluginAsync = async (fastify, _options) => { }, ); fastify.withTypeProvider().get( - "/groups/:groupId/roles", + "/assignments", { schema: withRoles( [AppRoles.IAM_ADMIN], withTags(["IAM"], { - params: z.object({ - groupId, - }), - summary: "Get a group's application role mappings.", + response: { + 200: iamGetAllAssignedRolesResponse, + }, + summary: "Get all user and group role assignments.", }), ), - onRequest: fastify.authorizeFromSchema, }, async (request, reply) => { + const allUserRoleAssignments = fastify.dynamoClient.send( + new ScanCommand({ + TableName: `${genericConfig.IAMTablePrefix}-userroles`, + }), + ); + const allGroupRoleAssignments = fastify.dynamoClient.send( + new ScanCommand({ + TableName: `${genericConfig.IAMTablePrefix}-grouproles`, + }), + ); try { - const groupId = request.params.groupId; - const roles = await getGroupRoles(fastify.dynamoClient, groupId); - return reply.send(roles); - } catch (e: unknown) { + const [userResponse, groupResponse] = await Promise.all([ + allUserRoleAssignments, + allGroupRoleAssignments, + ]); + const userUnmarshalled = userResponse.Items?.map((x) => unmarshall(x)); + const groupUnmarshalled = groupResponse.Items?.map((x) => + unmarshall(x), + ); + return reply.send({ + user: + (userUnmarshalled as z.infer< + typeof iamGetAllAssignedRolesResponse + >["user"]) || [], + group: + (groupUnmarshalled as z.infer< + typeof iamGetAllAssignedRolesResponse + >["group"]) || [], + }); + } catch (e) { if (e instanceof BaseError) { throw e; } - request.log.error(e); throw new DatabaseFetchError({ - message: "An error occurred finding the group role mapping.", + message: "Failed to get application mappings.", }); } }, diff --git a/src/common/types/iam.ts b/src/common/types/iam.ts index 331ffa9e..ff3af99f 100644 --- a/src/common/types/iam.ts +++ b/src/common/types/iam.ts @@ -81,3 +81,16 @@ export const entraProfilePatchRequest = z.object({ }); export type ProfilePatchRequest = z.infer; + +export const iamGetAllAssignedRolesResponse = z.object({ + user: z.array(z.object({ + userEmail: z.string(), + createdAt: z.string().datetime(), + roles: z.array(z.string()) + })), + group: z.array(z.object({ + groupUuid: z.string(), + createdAt: z.string().datetime(), + roles: z.array(z.string()) + })) +}) diff --git a/tests/live/iam.test.ts b/tests/live/iam.test.ts index b9bde3f3..a4a67bd7 100644 --- a/tests/live/iam.test.ts +++ b/tests/live/iam.test.ts @@ -3,11 +3,32 @@ import { createJwt } from "./utils.js"; import { EntraActionResponse, GroupMemberGetResponse, + iamGetAllAssignedRolesResponse, } from "../../src/common/types/iam.js"; import { allAppRoles, AppRoles } from "../../src/common/roles.js"; import { getBaseEndpoint } from "./utils.js"; +import { z } from "zod"; const baseEndpoint = getBaseEndpoint(); +test("getting all role assignments", async () => { + const token = await createJwt(); + const response = await fetch(`${baseEndpoint}/api/v1/iam/assignments`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + expect(response.status).toBe(200); + const responseJson = (await response.json()) as z.infer< + typeof iamGetAllAssignedRolesResponse + >; + expect(responseJson["user"]).toBeDefined(); + expect(responseJson["group"]).toBeDefined(); + expect(responseJson["user"].length).greaterThan(0); + expect(responseJson["group"].length).greaterThan(0); +}); + test("getting members of a group", async () => { const token = await createJwt(); const response = await fetch(