diff --git a/src/api/functions/apiKey.ts b/src/api/functions/apiKey.ts index a458e197..6979053f 100644 --- a/src/api/functions/apiKey.ts +++ b/src/api/functions/apiKey.ts @@ -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; diff --git a/src/api/plugins/auth.ts b/src/api/plugins/auth.ts index 3e6aa88f..f7e75604 100644 --- a/src/api/plugins/auth.ts +++ b/src/api/plugins/auth.ts @@ -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, @@ -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(setA: Set, setB: Set): Set { const _intersection = new Set(); diff --git a/src/api/plugins/authorizeFromSchema.ts b/src/api/plugins/authorizeFromSchema.ts index f4d0d756..7c6c6b60 100644 --- a/src/api/plugins/authorizeFromSchema.ts +++ b/src/api/plugins/authorizeFromSchema.ts @@ -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"; diff --git a/src/api/plugins/evaluatePolicies.ts b/src/api/plugins/evaluatePolicies.ts index 9325f262..cceb4606 100644 --- a/src/api/plugins/evaluatePolicies.ts +++ b/src/api/plugins/evaluatePolicies.ts @@ -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 diff --git a/src/api/routes/events.ts b/src/api/routes/events.ts index 44297805..cc874a43 100644 --- a/src/api/routes/events.ts +++ b/src/api/routes/events.ts @@ -39,13 +39,10 @@ import { 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 @@ -97,12 +94,27 @@ export type EventRepeatOptions = (typeof repeatOptions)[number]; 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, }); diff --git a/src/api/routes/ics.ts b/src/api/routes/ics.ts index 549003f7..ef3a5eaa 100644 --- a/src/api/routes/ics.ts +++ b/src/api/routes/ics.ts @@ -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"; diff --git a/src/api/routes/linkry.ts b/src/api/routes/linkry.ts index dd3d6ac8..ad45721f 100644 --- a/src/api/routes/linkry.ts +++ b/src/api/routes/linkry.ts @@ -39,11 +39,7 @@ import { 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 = { diff --git a/src/api/routes/roomRequests.ts b/src/api/routes/roomRequests.ts index 500c02a7..cb821625 100644 --- a/src/api/routes/roomRequests.ts +++ b/src/api/routes/roomRequests.ts @@ -11,7 +11,6 @@ import { roomRequestStatusUpdateRequest, } from "common/types/roomRequest.js"; import { AppRoles } from "common/roles.js"; -import { zodToJsonSchema } from "zod-to-json-schema"; import { BaseError, DatabaseFetchError, @@ -19,7 +18,6 @@ import { InternalServerError, } from "common/errors/index.js"; import { - PutItemCommand, QueryCommand, TransactWriteItemsCommand, } from "@aws-sdk/client-dynamodb"; diff --git a/src/api/routes/stripe.ts b/src/api/routes/stripe.ts index 2bac6250..acb56490 100644 --- a/src/api/routes/stripe.ts +++ b/src/api/routes/stripe.ts @@ -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"; @@ -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().get( diff --git a/src/api/routes/tickets.ts b/src/api/routes/tickets.ts index 4c57a2a8..6129eaa3 100644 --- a/src/api/routes/tickets.ts +++ b/src/api/routes/tickets.ts @@ -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"), diff --git a/src/api/routes/vending.ts b/src/api/routes/vending.ts index c5d67f01..da2764d2 100644 --- a/src/api/routes/vending.ts +++ b/src/api/routes/vending.ts @@ -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({ diff --git a/src/api/types.d.ts b/src/api/types.d.ts index 1afd4215..34ab418c 100644 --- a/src/api/types.d.ts +++ b/src/api/types.d.ts @@ -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 { diff --git a/src/api/policies/definition.ts b/src/common/policies/definition.ts similarity index 100% rename from src/api/policies/definition.ts rename to src/common/policies/definition.ts diff --git a/src/api/policies/evaluator.ts b/src/common/policies/evaluator.ts similarity index 100% rename from src/api/policies/evaluator.ts rename to src/common/policies/evaluator.ts diff --git a/src/api/policies/events.ts b/src/common/policies/events.ts similarity index 86% rename from src/api/policies/events.ts rename to src/common/policies/events.ts index 74f41c42..9894f63a 100644 --- a/src/api/policies/events.ts +++ b/src/common/policies/events.ts @@ -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", @@ -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, diff --git a/src/common/types/apiKey.ts b/src/common/types/apiKey.ts index b419c840..d31030b8 100644 --- a/src/common/types/apiKey.ts +++ b/src/common/types/apiKey.ts @@ -1,6 +1,7 @@ +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[]; @@ -8,8 +9,11 @@ export type ApiKeyMaskedEntry = { description: string; createdAt: number; expiresAt?: number; - restrictions?: Record; + restrictions?: AvailableAuthorizationPolicy[]; } +export type ApiKeyDynamoEntry = ApiKeyMaskedEntry & { + keyHash: string; +}; export type DecomposedApiKey = { prefix: string; @@ -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; diff --git a/src/ui/types.d.ts b/src/ui/types.d.ts new file mode 100644 index 00000000..fac6d444 --- /dev/null +++ b/src/ui/types.d.ts @@ -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; + tokenPayload?: AadToken; + policyRestrictions?: AvailableAuthorizationPolicy[]; + } +} + +export type NoDataRequest = { + Params: undefined; + Querystring: undefined; + Body: undefined; +};