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
2 changes: 1 addition & 1 deletion src/api/functions/apiKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { genericConfig } from "common/config.js";
import { AUTH_DECISION_CACHE_SECONDS as API_KEY_DATA_CACHE_SECONDS } from "./authorization.js";
import { unmarshall } from "@aws-sdk/util-dynamodb";
import { ApiKeyMaskedEntry, DecomposedApiKey } from "common/types/apiKey.js";
import { AvailableAuthorizationPolicy } from "api/policies/definition.js";
import { AvailableAuthorizationPolicy } from "common/policies/definition.js";

export type ApiKeyDynamoEntry = ApiKeyMaskedEntry & {
keyHash: string;
Expand Down
9 changes: 1 addition & 8 deletions src/api/plugins/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
SecretsManagerClient,
GetSecretValueCommand,
} from "@aws-sdk/client-secrets-manager";
import { AppRoles } from "../../common/roles.js";
import { AppRoles } from "common/roles.js";
import {
BaseError,
InternalServerError,
Expand All @@ -15,14 +15,7 @@ import {
} from "../../common/errors/index.js";
import { genericConfig, SecretConfig } from "../../common/config.js";
import { getGroupRoles, getUserRoles } from "../functions/authorization.js";
import {
GetItemCommand,
ReplicaAlreadyExistsException,
} from "@aws-sdk/client-dynamodb";
import { getApiKeyData, getApiKeyParts } from "api/functions/apiKey.js";
import { RequestThrottled } from "@aws-sdk/client-sqs";
import { evaluatePolicy } from "api/policies/evaluator.js";
import { AuthorizationPoliciesRegistry } from "api/policies/definition.js";

export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
const _intersection = new Set<T>();
Expand Down
1 change: 0 additions & 1 deletion src/api/plugins/authorizeFromSchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FastifyPluginAsync } from "fastify";
import { AppRoles } from "common/roles.js";
import { InternalServerError } from "common/errors/index.js";
import fp from "fastify-plugin";
import { FastifyZodOpenApiSchema } from "fastify-zod-openapi";
Expand Down
6 changes: 3 additions & 3 deletions src/api/plugins/evaluatePolicies.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fp from "fastify-plugin";
import { FastifyPluginAsync, FastifyRequest } from "fastify";
import { UnauthorizedError } from "../../common/errors/index.js";
import { UnauthorizedError } from "common/errors/index.js";
import {
AuthorizationPoliciesRegistry,
AvailableAuthorizationPolicies,
} from "api/policies/definition.js";
import { evaluatePolicy } from "api/policies/evaluator.js";
} from "common/policies/definition.js";
import { evaluatePolicy } from "common/policies/evaluator.js";

/**
* Evaluates all policy restrictions for a request
Expand Down
30 changes: 21 additions & 9 deletions src/api/routes/events.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import "zod-openapi/extend";
import { FastifyPluginAsync, FastifyRequest } from "fastify";

Check warning on line 2 in src/api/routes/events.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'FastifyRequest' is defined but never used. Allowed unused vars must match /^_/u
import { AppRoles } from "../../common/roles.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

Check warning on line 5 in src/api/routes/events.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'zodToJsonSchema' is defined but never used. Allowed unused vars must match /^_/u
import { OrganizationList } from "../../common/orgs.js";
import {
DeleteItemCommand,
Expand Down Expand Up @@ -39,13 +39,10 @@
FastifyPluginAsyncZodOpenApi,
FastifyZodOpenApiSchema,
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { ts, withRoles, withTags } from "api/components/index.js";
import { MAX_METADATA_KEYS, metadataSchema } from "common/types/events.js";
import { metadataSchema } from "common/types/events.js";
import { evaluateAllRequestPolicies } from "api/plugins/evaluatePolicies.js";
import { request } from "http";

const createProjectionParams = (includeMetadata: boolean = false) => {
// Object mapping attribute names to their expression aliases
Expand Down Expand Up @@ -79,7 +76,7 @@
expressionAttributeNames,
projectionExpression,
// Return function to destructure results if needed
getAttributes: <T>(item: any): T => item as T,

Check warning on line 79 in src/api/routes/events.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

Unexpected any. Specify a different type
};
};

Expand All @@ -97,12 +94,27 @@
const baseSchema = z.object({
title: z.string().min(1),
description: z.string().min(1),
start: z.string(),
end: z.optional(z.string()),
location: z.string(),
locationLink: z.optional(z.string().url()),
start: z.string().openapi({
description: "Timestamp in the America/Chicago timezone.",
example: "2024-08-27T19:00:00",
}),
end: z.optional(z.string()).openapi({
description: "Timestamp in the America/Chicago timezone.",
example: "2024-08-27T20:00:00",
}),
location: z.string().openapi({
description: "Human-friendly location name.",
example: "Siebel Center for Computer Science",
}),
locationLink: z.optional(z.string().url()).openapi({
description: "Google Maps link for easy navigation to the event location.",
example: "https://maps.app.goo.gl/dwbBBBkfjkgj8gvA8",
}),
host: z.enum(OrganizationList as [string, ...string[]]),
featured: z.boolean().default(false),
featured: z.boolean().default(false).openapi({
description:
"Whether or not the event should be shown on the ACM @ UIUC website home page (and added to Discord, as available).",
}),
paidEventId: z.optional(z.string().min(1)),
metadata: metadataSchema,
});
Expand Down
2 changes: 0 additions & 2 deletions src/api/routes/ics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import { getCacheCounter } from "api/functions/cache.js";
import {
FastifyZodOpenApiSchema,
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { withTags } from "api/components/index.js";
import { z } from "zod";
Expand Down
6 changes: 1 addition & 5 deletions src/api/routes/linkry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
UnauthorizedError,
ValidationError,
} from "../../common/errors/index.js";
import { NoDataRequest } from "../types.js";

Check warning on line 13 in src/api/routes/linkry.ts

View workflow job for this annotation

GitHub Actions / Run Unit Tests

'NoDataRequest' is defined but never used. Allowed unused vars must match /^_/u
import {
QueryCommand,
TransactWriteItemsCommand,
Expand Down Expand Up @@ -39,11 +39,7 @@
import { intersection } from "api/plugins/auth.js";
import { createAuditLogEntry } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import {
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { withRoles, withTags } from "api/components/index.js";

type OwnerRecord = {
Expand Down
2 changes: 0 additions & 2 deletions src/api/routes/roomRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@ import {
roomRequestStatusUpdateRequest,
} from "common/types/roomRequest.js";
import { AppRoles } from "common/roles.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import {
BaseError,
DatabaseFetchError,
DatabaseInsertError,
InternalServerError,
} from "common/errors/index.js";
import {
PutItemCommand,
QueryCommand,
TransactWriteItemsCommand,
} from "@aws-sdk/client-dynamodb";
Expand Down
8 changes: 1 addition & 7 deletions src/api/routes/stripe.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import {
PutItemCommand,
QueryCommand,
ScanCommand,
TransactWriteItemsCommand,
TransactWriteItemsCommandInput,
} from "@aws-sdk/client-dynamodb";
import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
import { withRoles, withTags } from "api/components/index.js";
Expand Down Expand Up @@ -33,11 +31,7 @@ import {
invoiceLinkGetResponseSchema,
} from "common/types/stripe.js";
import { FastifyPluginAsync } from "fastify";
import {
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";

const stripeRoutes: FastifyPluginAsync = async (fastify, _options) => {
fastify.withTypeProvider<FastifyZodOpenApiTypeProvider>().get(
Expand Down
8 changes: 1 addition & 7 deletions src/api/routes/tickets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,8 @@ import { zodToJsonSchema } from "zod-to-json-schema";
import { ItemPostData, postMetadataSchema } from "common/types/tickets.js";
import { createAuditLogEntry } from "api/functions/auditLog.js";
import { Modules } from "common/modules.js";
import {
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { withRoles, withTags } from "api/components/index.js";
import { request } from "http";
import authorizeFromSchemaPlugin from "api/plugins/authorizeFromSchema.js";

const postMerchSchema = z.object({
type: z.literal("merch"),
Expand Down
6 changes: 1 addition & 5 deletions src/api/routes/vending.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { withTags } from "api/components/index.js";
import { FastifyPluginAsync } from "fastify";
import {
FastifyZodOpenApiTypeProvider,
serializerCompiler,
validatorCompiler,
} from "fastify-zod-openapi";
import { FastifyZodOpenApiTypeProvider } from "fastify-zod-openapi";
import { z } from "zod";

const postSchema = z.object({
Expand Down
2 changes: 1 addition & 1 deletion src/api/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { SQSClient } from "@aws-sdk/client-sqs";
import { CloudFrontKeyValueStoreClient } from "@aws-sdk/client-cloudfront-keyvaluestore";
import { AvailableAuthorizationPolicy } from "./policies/definition";
import { AvailableAuthorizationPolicy } from "common/policies/definition.js";

declare module "fastify" {
interface FastifyInstance {
Expand Down
File renamed without changes.
File renamed without changes.
5 changes: 2 additions & 3 deletions src/api/policies/events.ts → src/common/policies/events.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { z } from "zod";
import { createPolicy } from "./evaluator.js";
import { OrganizationList } from "common/orgs.js";
import { OrganizationList } from "../orgs.js";
import { FastifyRequest } from "fastify";
import { EventPostRequest } from "api/routes/events.js";

export const hostRestrictionPolicy = createPolicy(
"EventsHostRestrictionPolicy",
Expand All @@ -15,7 +14,7 @@ export const hostRestrictionPolicy = createPolicy(
cacheKey: null,
};
}
const typedBody = request.body as EventPostRequest;
const typedBody = request.body as { host: string };
if (!typedBody || !typedBody["host"]) {
return {
allowed: true,
Expand Down
56 changes: 47 additions & 9 deletions src/common/types/apiKey.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { AuthorizationPoliciesRegistry, AvailableAuthorizationPolicy } from "../policies/definition.js";
import { AppRoles } from "../roles.js";
import { z } from "zod"

import { z } from "zod";
import { InternalServerError } from "../errors/index.js";
export type ApiKeyMaskedEntry = {
keyId: string;
roles: AppRoles[];
owner: string;
description: string;
createdAt: number;
expiresAt?: number;
restrictions?: Record<string, any>;
restrictions?: AvailableAuthorizationPolicy[];
}
export type ApiKeyDynamoEntry = ApiKeyMaskedEntry & {
keyHash: string;
};

export type DecomposedApiKey = {
prefix: string;
Expand All @@ -18,18 +22,52 @@ export type DecomposedApiKey = {
checksum: string;
};

export const apiKeyAllowedRoles = [AppRoles.EVENTS_MANAGER, AppRoles.TICKETS_MANAGER, AppRoles.TICKETS_SCANNER, AppRoles.ROOM_REQUEST_CREATE, AppRoles.STRIPE_LINK_CREATOR, AppRoles.LINKS_MANAGER]
const policySchemas = Object.entries(AuthorizationPoliciesRegistry).map(
([key, policy]) =>
z.object({
name: z.literal(key),
params: policy.paramsSchema,
})
);

if (policySchemas.length === 0) {
throw new InternalServerError({
message: "No authorization policies are defined in AuthorizationPoliciesRegistry. 'restrictions' will be an empty schema."
})
}

const policyUnion = policySchemas.length > 0
? z.discriminatedUnion("name", policySchemas as [typeof policySchemas[0], ...typeof policySchemas])
: z.never();

export const apiKeyAllowedRoles = [
AppRoles.EVENTS_MANAGER,
AppRoles.TICKETS_MANAGER,
AppRoles.TICKETS_SCANNER,
AppRoles.ROOM_REQUEST_CREATE,
AppRoles.STRIPE_LINK_CREATOR,
AppRoles.LINKS_MANAGER,
];

export const apiKeyPostBody = z.object({
roles: z.array(z.enum(apiKeyAllowedRoles as [AppRoles, ...AppRoles[]]))
.min(1)
.refine((items) => new Set(items).size === items.length, {
message: "All roles must be unique, no duplicate values allowed",
}).openapi({ description: `Roles granted to the API key. These roles are a subset of the overall application roles.` }),
description: z.string().min(1).openapi({ description: "Description of the key's use.", example: "Publish events to ACM Calendar as part of the CI process." }),
expiresAt: z.optional(z.number()).refine((val) => val === undefined || val > Date.now() / 1000, {
}).openapi({
description: `Roles granted to the API key. These roles are a subset of the overall application roles.`,
}),
description: z.string().min(1).openapi({
description: "Description of the key's use.",
example: "Publish events to ACM Calendar as part of the CI process.",
}),
expiresAt: z.optional(z.number().refine((val) => val === undefined || val > Date.now() / 1000, {
message: "expiresAt must be a future epoch time.",
}).openapi({ description: "Epoch timestamp of when the key expires.", example: 1745362658 })
})
})).openapi({
description: "Epoch timestamp of when the key expires.",
example: 1745362658,
}),
restrictions: z.optional(z.array(policyUnion)).openapi({ description: "Policy restrictions applied to the API key." }),
});

export type ApiKeyPostBody = z.infer<typeof apiKeyPostBody>;
25 changes: 25 additions & 0 deletions src/ui/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { type FastifyRequest, type FastifyInstance, type FastifyReply } from 'fastify';
import { type AppRoles, type RunEnvironment } from '@common/roles.js';
import type NodeCache from 'node-cache';
import { type DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { type SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { type SQSClient } from '@aws-sdk/client-sqs';
import { type CloudFrontKeyValueStoreClient } from '@aws-sdk/client-cloudfront-keyvaluestore';
import { type AvailableAuthorizationPolicy } from '@common/policies/definition.js';

declare module 'fastify' {
interface FastifyRequest {
startTime: number;
username?: string;
userRoles?: Set<AppRoles>;
tokenPayload?: AadToken;
policyRestrictions?: AvailableAuthorizationPolicy[];
}
}

export type NoDataRequest = {
Params: undefined;
Querystring: undefined;
Body: undefined;
};
Loading