Skip to content

Commit

Permalink
feat(fhir-router/graphql): fix #4567 configurable system setting for …
Browse files Browse the repository at this point in the history
…graphql max depth
  • Loading branch information
dillonstreator committed Jun 9, 2024
1 parent 437b575 commit 5cc02f0
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 32 deletions.
1 change: 1 addition & 0 deletions packages/fhir-router/src/fhirrouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type FhirRouteHandler = (req: FhirRequest, repo: FhirRepository, router:

export interface FhirOptions {
introspectionEnabled?: boolean;
graphqlMaxDepth?: number;
}

// Execute batch
Expand Down
55 changes: 29 additions & 26 deletions packages/fhir-router/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function graphqlHandler(
}

const schema = getRootSchema();
const validationRules = [...specifiedRules, MaxDepthRule];
const validationRules = [...specifiedRules, MaxDepthRule(router.options.graphqlMaxDepth)];
const validationErrors = validate(schema, document, validationRules);
if (validationErrors.length > 0) {
return [invalidRequest(validationErrors)];
Expand Down Expand Up @@ -408,31 +408,34 @@ async function resolveByDelete(
await ctx.repo.deleteResource(resourceType, args.id);
}

const DEFAULT_MAX_DEPTH = 15;

/**
* Custom GraphQL rule that enforces max depth constraint.
* @param context - The validation context.
* @returns An ASTVisitor that validates the maximum depth rule.
* @param maxDepth - The maximum allowed depth.
* @returns A function that is an ASTVisitor that validates the maximum depth rule.
*/
const MaxDepthRule = (context: ValidationContext): ASTVisitor => ({
Field(
/** The current node being visiting. */
node: any,
/** The index or key to this node from the parent node or Array. */
_key: string | number | undefined,
/** The parent immediately above this node, which may be an Array. */
_parent: ASTNode | readonly ASTNode[] | undefined,
/** The key path to get to this node from the root node. */
path: readonly (string | number)[]
): any {
const depth = getDepth(path);
const maxDepth = 12;
if (depth > maxDepth) {
const fieldName = node.name.value;
context.reportError(
new GraphQLError(`Field "${fieldName}" exceeds max depth (depth=${depth}, max=${maxDepth})`, {
nodes: node,
})
);
}
},
});
const MaxDepthRule =
(maxDepth: number = DEFAULT_MAX_DEPTH) =>
(context: ValidationContext): ASTVisitor => ({
Field(
/** The current node being visiting. */
node: any,
/** The index or key to this node from the parent node or Array. */
_key: string | number | undefined,
/** The parent immediately above this node, which may be an Array. */
_parent: ASTNode | readonly ASTNode[] | undefined,
/** The key path to get to this node from the root node. */
path: readonly (string | number)[]
): any {
const depth = getDepth(path);
if (depth > maxDepth) {
const fieldName = node.name.value;
context.reportError(
new GraphQLError(`Field "${fieldName}" exceeds max depth (depth=${depth}, max=${maxDepth})`, {
nodes: node,
})
);
}
},
});
17 changes: 11 additions & 6 deletions packages/server/src/fhir/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ResourceType } from '@medplum/fhirtypes';
import { NextFunction, Request, Response, Router } from 'express';
import { asyncWrap } from '../async';
import { getConfig } from '../config';
import { getAuthenticatedContext } from '../context';
import { AuthenticatedRequestContext, getAuthenticatedContext } from '../context';
import { authenticateRequest } from '../oauth/middleware';
import { recordHistogramValue } from '../otel/otel';
import { bulkDataRouter } from './bulkdata';
Expand Down Expand Up @@ -122,22 +122,27 @@ protectedRoutes.use('/job', jobRouter);
/**
* Returns the internal FHIR router.
* This function will be executed on every request.
* @param ctx - The authenticated request context.
* @returns The lazy initialized internal FHIR router.
*/
function getInternalFhirRouter(): FhirRouter {
function getInternalFhirRouter(ctx: AuthenticatedRequestContext): FhirRouter {
if (!internalFhirRouter) {
internalFhirRouter = initInternalFhirRouter();
internalFhirRouter = initInternalFhirRouter(ctx);
}
return internalFhirRouter;
}

/**
* Returns a new FHIR router.
* This function should only be called once on the first request.
* @param ctx - The authenticated request context.
* @returns A new FHIR router with all the internal operations.
*/
function initInternalFhirRouter(): FhirRouter {
const router = new FhirRouter({ introspectionEnabled: getConfig().introspectionEnabled });
function initInternalFhirRouter(ctx: AuthenticatedRequestContext): FhirRouter {
const router = new FhirRouter({
introspectionEnabled: getConfig().introspectionEnabled,
graphqlMaxDepth: ctx.project.systemSetting?.find((s) => s.name === 'graphqlMaxDepth')?.valueInteger,
});

// Project $export
router.add('GET', '/$export', bulkExportHandler);
Expand Down Expand Up @@ -288,7 +293,7 @@ protectedRoutes.use(
headers: req.headers,
};

const result = await getInternalFhirRouter().handleRequest(request, ctx.repo);
const result = await getInternalFhirRouter(ctx).handleRequest(request, ctx.repo);
if (result.length === 1) {
if (!isOk(result[0])) {
throw new OperationOutcomeError(result[0]);
Expand Down

0 comments on commit 5cc02f0

Please sign in to comment.