Skip to content

Commit 8fc3b14

Browse files
committed
implement reading permissions from dynamodb
1 parent f1d3906 commit 8fc3b14

File tree

6 files changed

+347
-19
lines changed

6 files changed

+347
-19
lines changed

src/api/functions/authorization.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
DynamoDBClient,
3+
GetItemCommand,
4+
QueryCommand,
5+
} from "@aws-sdk/client-dynamodb";
6+
import { unmarshall } from "@aws-sdk/util-dynamodb";
7+
import { genericConfig } from "common/config.js";
8+
import { DatabaseFetchError } from "common/errors/index.js";
9+
import { allAppRoles, AppRoles } from "common/roles.js";
10+
import { FastifyInstance } from "fastify";
11+
12+
export const AUTH_DECISION_CACHE_SECONDS = 60;
13+
14+
export async function getUserRoles(
15+
dynamoClient: DynamoDBClient,
16+
fastifyApp: FastifyInstance,
17+
userId: string,
18+
): Promise<AppRoles[]> {
19+
const cachedValue = fastifyApp.nodeCache.get(`userroles-${userId}`);
20+
if (cachedValue) {
21+
fastifyApp.log.info(`Returning cached auth decision for user ${userId}`);
22+
return cachedValue as AppRoles[];
23+
}
24+
const tableName = `${genericConfig["IAMTablePrefix"]}-userroles`;
25+
const command = new GetItemCommand({
26+
TableName: tableName,
27+
Key: {
28+
userEmail: { S: userId },
29+
},
30+
});
31+
const response = await dynamoClient.send(command);
32+
if (!response || !response.Item) {
33+
throw new DatabaseFetchError({
34+
message: "Could not get user roles",
35+
});
36+
}
37+
const items = unmarshall(response.Item) as { roles: AppRoles[] | ["all"] };
38+
if (!("roles" in items)) {
39+
return [];
40+
}
41+
if (items["roles"][0] === "all") {
42+
fastifyApp.nodeCache.set(
43+
`userroles-${userId}`,
44+
allAppRoles,
45+
AUTH_DECISION_CACHE_SECONDS,
46+
);
47+
return allAppRoles;
48+
}
49+
fastifyApp.nodeCache.set(
50+
`userroles-${userId}`,
51+
items["roles"],
52+
AUTH_DECISION_CACHE_SECONDS,
53+
);
54+
return items["roles"] as AppRoles[];
55+
}
56+
57+
export async function getGroupRoles(
58+
dynamoClient: DynamoDBClient,
59+
fastifyApp: FastifyInstance,
60+
groupId: string,
61+
) {
62+
const cachedValue = fastifyApp.nodeCache.get(`grouproles-${groupId}`);
63+
if (cachedValue) {
64+
fastifyApp.log.info(`Returning cached auth decision for group ${groupId}`);
65+
return cachedValue as AppRoles[];
66+
}
67+
const tableName = `${genericConfig["IAMTablePrefix"]}-grouproles`;
68+
const command = new GetItemCommand({
69+
TableName: tableName,
70+
Key: {
71+
groupUuid: { S: groupId },
72+
},
73+
});
74+
const response = await dynamoClient.send(command);
75+
if (!response || !response.Item) {
76+
throw new DatabaseFetchError({
77+
message: "Could not get group roles for user",
78+
});
79+
}
80+
const items = unmarshall(response.Item) as { roles: AppRoles[] | ["all"] };
81+
if (!("roles" in items)) {
82+
fastifyApp.nodeCache.set(
83+
`grouproles-${groupId}`,
84+
[],
85+
AUTH_DECISION_CACHE_SECONDS,
86+
);
87+
return [];
88+
}
89+
if (items["roles"][0] === "all") {
90+
fastifyApp.nodeCache.set(
91+
`grouproles-${groupId}`,
92+
allAppRoles,
93+
AUTH_DECISION_CACHE_SECONDS,
94+
);
95+
return allAppRoles;
96+
}
97+
fastifyApp.nodeCache.set(
98+
`grouproles-${groupId}`,
99+
items["roles"],
100+
AUTH_DECISION_CACHE_SECONDS,
101+
);
102+
return items["roles"] as AppRoles[];
103+
}

src/api/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import * as dotenv from "dotenv";
1818
import iamRoutes from "./routes/iam.js";
1919
import ticketsPlugin from "./routes/tickets.js";
2020
import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts";
21+
import NodeCache from "node-cache";
2122

2223
dotenv.config();
2324

@@ -68,6 +69,7 @@ async function init() {
6869
app.runEnvironment = process.env.RunEnvironment as RunEnvironment;
6970
app.environmentConfig =
7071
environmentConfig[app.runEnvironment as RunEnvironment];
72+
app.nodeCache = new NodeCache({ checkperiod: 30 });
7173
app.addHook("onRequest", (req, _, done) => {
7274
req.startTime = now();
7375
const hostname = req.hostname;

src/api/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"prettier:write": "prettier --write *.ts **/*.ts"
1616
},
1717
"dependencies": {
18-
"@aws-sdk/client-sts": "^3.726.0"
18+
"@aws-sdk/client-sts": "^3.726.0",
19+
"node-cache": "^5.1.2"
1920
}
2021
}

src/api/plugins/auth.ts

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
UnauthorizedError,
1515
} from "../../common/errors/index.js";
1616
import { genericConfig, SecretConfig } from "../../common/config.js";
17+
import { getGroupRoles, getUserRoles } from "api/functions/authorization.js";
18+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
1719

1820
function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
1921
const _intersection = new Set<T>();
@@ -55,6 +57,10 @@ const smClient = new SecretsManagerClient({
5557
region: genericConfig.AwsRegion,
5658
});
5759

60+
const dynamoClient = new DynamoDBClient({
61+
region: genericConfig.AwsRegion,
62+
});
63+
5864
export const getSecretValue = async (
5965
secretId: string,
6066
): Promise<Record<string, string | number | boolean> | null | SecretConfig> => {
@@ -163,13 +169,18 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
163169
verifiedTokenData.groups &&
164170
fastify.environmentConfig.GroupRoleMapping
165171
) {
166-
for (const group of verifiedTokenData.groups) {
167-
if (fastify.environmentConfig["GroupRoleMapping"][group]) {
168-
for (const role of fastify.environmentConfig["GroupRoleMapping"][
169-
group
170-
]) {
172+
const groupRoles = await Promise.allSettled(
173+
verifiedTokenData.groups.map((x) =>
174+
getGroupRoles(dynamoClient, fastify, x),
175+
),
176+
);
177+
for (const result of groupRoles) {
178+
if (result.status === "fulfilled") {
179+
for (const role of result.value) {
171180
userRoles.add(role);
172181
}
182+
} else {
183+
request.log.warn(`Failed to get group roles: ${result.reason}`);
173184
}
174185
}
175186
} else {
@@ -188,13 +199,24 @@ const authPlugin: FastifyPluginAsync = async (fastify, _options) => {
188199
}
189200
}
190201
}
202+
191203
// add user-specific role overrides
192204
if (request.username && fastify.environmentConfig.UserRoleMapping) {
193205
if (fastify.environmentConfig["UserRoleMapping"][request.username]) {
194-
for (const role of fastify.environmentConfig["UserRoleMapping"][
195-
request.username
196-
]) {
197-
userRoles.add(role);
206+
try {
207+
const userAuth = await getUserRoles(
208+
dynamoClient,
209+
fastify,
210+
request.username,
211+
);
212+
for (const role of userAuth) {
213+
userRoles.add(role);
214+
}
215+
} catch (e) {
216+
request.log.warn(
217+
`Failed to get user role mapping for ${request.username}`,
218+
e,
219+
);
198220
}
199221
}
200222
}

src/api/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FastifyRequest, FastifyInstance, FastifyReply } from "fastify";
22
import { AppRoles, RunEnvironment } from "../common/roles.js";
33
import { AadToken } from "./plugins/auth.js";
44
import { ConfigType } from "../common/config.js";
5+
import NodeCache from "node-cache";
56
declare module "fastify" {
67
interface FastifyInstance {
78
authenticate: (
@@ -20,6 +21,7 @@ declare module "fastify" {
2021
) => Promise<void>;
2122
runEnvironment: RunEnvironment;
2223
environmentConfig: ConfigType;
24+
nodeCache: NodeCache;
2325
}
2426
interface FastifyRequest {
2527
startTime: number;

0 commit comments

Comments
 (0)