Skip to content

Commit

Permalink
op-reg: Follow-up changes to #246. (#251)
Browse files Browse the repository at this point in the history
op-reg: Follow-up changes to #246.
  • Loading branch information
abernix authored Dec 11, 2019
2 parents 71e671a + b11d1b5 commit d892eb5
Show file tree
Hide file tree
Showing 9 changed files with 506 additions and 687 deletions.
4 changes: 2 additions & 2 deletions packages/apollo-server-plugin-operation-registry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

### vNEXT (Currently `alpha` tag)

- Add lifecycle hooks: `willUpdateManifest`, `onUnregisteredOperation`, and `onForbiddenOperation`. [PR #158](https://github.com/apollographql/apollo-platform-commercial/pull/158)
- Add lifecycle hooks: `onUnregisteredOperation`, and `onForbiddenOperation`. [PR #158](https://github.com/apollographql/apollo-platform-commercial/pull/158) [PR #251](https://github.com/apollographql/apollo-platform-commercial/pull/251) [PR #TODO](https://github.com/apollographql/apollo-platform-commercial/pull/TODO)
- Prevent the polling timer from keeping the event loop active [PR #223](https://github.com/apollographql/apollo-platform-commercial/pull/223)
- Update error message for operations that are not in the operation registry [PR #170](https://github.com/apollographql/apollo-platform-commercial/pull/170)
- Update error message for operations that are not in the operation registry. [PR #170](https://github.com/apollographql/apollo-platform-commercial/pull/170)

### 0.2.2

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apollo-server-plugin-operation-registry",
"version": "0.2.3-alpha.0",
"version": "0.3.0-alpha.0",
"description": "Apollo Server operation registry",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,40 +1,63 @@
import * as assert from 'assert';
import { pluginName, getStoreKey, hashForLogging } from './common';
import { pluginName, getStoreKey, signatureForLogging } from './common';
import {
ApolloServerPlugin,
GraphQLServiceContext,
GraphQLRequestListener,
GraphQLRequestContext,
} from 'apollo-server-plugin-base';
import {
operationHash,
defaultOperationRegistrySignature,
/**
* We alias these to different names entirely since the user-facing values
* which are present in their manifest (signature and document) are probably
* the most important concepts to rally around right now, in terms of
* approachability to the implementor. A future version of the
* `apollo-graphql` package should rename them to make this more clear.
*/
operationHash as operationSignature,
defaultOperationRegistrySignature as defaultOperationRegistryNormalization,
} from 'apollo-graphql';
import { ForbiddenError, ApolloError } from 'apollo-server-errors';
import Agent, { OperationManifest } from './agent';
import Agent from './agent';
import { GraphQLSchema } from 'graphql/type';
import { InMemoryLRUCache } from 'apollo-server-caching';
import loglevel from 'loglevel';
import loglevelDebug from 'loglevel-debug';
import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';

type ForbidUnregisteredOperationsPredicate = (
requestContext: GraphQLRequestContext,
) => boolean;

interface Options {
export interface OperationRegistryRequestContext {
signature: string;
normalizedDocument: string;
}

export interface Operation {
signature: string;
document: string;
}

export interface OperationManifest {
version: number;
operations: Array<Operation>;
}

export interface Options {
debug?: boolean;
forbidUnregisteredOperations?:
| boolean
| ForbidUnregisteredOperationsPredicate;
dryRun?: boolean;
schemaTag?: string;
onUnregisteredOperation?: (requestContext: GraphQLRequestContext) => void;
onForbiddenOperation?: (requestContext: GraphQLRequestContext) => void;
willUpdateManifest?: (
newManifest?: OperationManifest,
oldManifest?: OperationManifest,
) => PromiseOrValue<OperationManifest>;
onUnregisteredOperation?: (
requestContext: GraphQLRequestContext,
operationRegistryRequestContext: OperationRegistryRequestContext,
) => void;
onForbiddenOperation?: (
requestContext: GraphQLRequestContext,
operationRegistryRequestContext: OperationRegistryRequestContext,
) => void;
}

export default function plugin(options: Options = Object.create(null)) {
Expand Down Expand Up @@ -106,7 +129,6 @@ for observability purposes, but all operations will be permitted.`,
engine,
store,
logger,
willUpdateManifest: options.willUpdateManifest,
});

await agent.start();
Expand All @@ -115,50 +137,52 @@ for observability purposes, but all operations will be permitted.`,
requestDidStart(): GraphQLRequestListener<any> {
return {
async didResolveOperation(requestContext) {
const document = requestContext.document;
const documentFromRequestContext = requestContext.document;
// This shouldn't happen under normal operation since `store` will be
// set in `serverWillStart` and `requestDidStart` (this) comes after.
if (!store) {
throw new Error('Unable to access store.');
}

const hash = operationHash(
defaultOperationRegistrySignature(
document,

// XXX The `operationName` is set from the AST, not from the
// request `operationName`. If `operationName` is `null`,
// then the operation is anonymous. However, it's not possible
// to register anonymous operations from the `apollo` CLI.
// We could fail early, however, we still want to abide by the
// desires of `forbidUnregisteredOperations`, so we'll allow
// this hash be generated anyway. The hash cannot be in the
// manifest, so this would be okay and allow this code to remain
// less conditional-y, eventually forbidding the operation when
// the hash is not found and `forbidUnregisteredOperations` is on.
requestContext.operationName || '',
),
const normalizedDocument = defaultOperationRegistryNormalization(
documentFromRequestContext,

// XXX The `operationName` is set from the AST, not from the
// request `operationName`. If `operationName` is `null`,
// then the operation is anonymous. However, it's not possible
// to register anonymous operations from the `apollo` CLI.
// We could fail early, however, we still want to abide by the
// desires of `forbidUnregisteredOperations`, so we'll allow
// this signature to be generated anyway. It could not be in the
// manifest, so this would be okay and allow this code to remain
// less conditional-y, eventually forbidding the operation when
// the signature is absent and `forbidUnregisteredOperations` is on.
requestContext.operationName || '',
);

if (!hash) {
const signature = operationSignature(normalizedDocument);

if (!signature) {
throw new ApolloError('No document.');
}

// The hashes are quite long and it seems we can get by with a substr.
const logHash = hashForLogging(hash);
// The signatures are quite long so we truncate to a prefix of it.
const logSignature = signatureForLogging(signature);

logger.debug(`${logHash}: Looking up operation in local registry.`);
logger.debug(
`${logSignature}: Looking up operation in local registry.`,
);

// Try to fetch the operation from the store of operations we're
// currently aware of, which has been populated by the operation
// registry.
const storeFetch = await store.get(getStoreKey(hash));
const storeFetch = await store.get(getStoreKey(signature));

// If we have a hit, we'll return immediately, signaling that we're
// not intending to block this request.
if (storeFetch) {
logger.debug(
`${logHash}: Permitting operation found in local registry.`,
`${logSignature}: Permitting operation found in local registry.`,
);
requestContext.metrics.registeredOperation = true;
return;
Expand All @@ -167,7 +191,10 @@ for observability purposes, but all operations will be permitted.`,
if (typeof options.onUnregisteredOperation === 'function') {
const onUnregisteredOperation = options.onUnregisteredOperation;
Promise.resolve().then(() => {
onUnregisteredOperation(requestContext);
onUnregisteredOperation(requestContext, {
signature,
normalizedDocument,
});
});
}
}
Expand All @@ -190,7 +217,7 @@ for observability purposes, but all operations will be permitted.`,

if (typeof options.forbidUnregisteredOperations === 'function') {
logger.debug(
`${logHash}: Calling 'forbidUnregisteredOperations' predicate function with requestContext...`,
`${logSignature}: Calling 'forbidUnregisteredOperations' predicate function with requestContext...`,
);

try {
Expand All @@ -199,7 +226,7 @@ for observability purposes, but all operations will be permitted.`,
);

logger.debug(
`${logHash}: The 'forbidUnregisteredOperations' predicate function returned ${predicateResult}`,
`${logSignature}: The 'forbidUnregisteredOperations' predicate function returned ${predicateResult}`,
);

// If we've received a boolean back from the predicate function,
Expand All @@ -211,24 +238,36 @@ for observability purposes, but all operations will be permitted.`,
shouldForbidOperation = predicateResult;
} else {
logger.warn(
`${logHash} Predicate function did not return a boolean response. Got ${predicateResult}`,
`${logSignature} Predicate function did not return a boolean response. Got ${predicateResult}`,
);
}
} catch (err) {
// If an error occurs within the forbidUnregisteredOperations
// predicate function, we should assume that the implementor
// had a security-wise intention and remain in enforcement mode.
logger.error(
`${logHash}: An error occurred within the 'forbidUnregisteredOperations' predicate function: ${err}`,
`${logSignature}: An error occurred within the 'forbidUnregisteredOperations' predicate function: ${err}`,
);
}
}

// If the user explicitly set forbidUnregisteredOperations to either `true` or a function, and the operation
// should be forbidden, we report it within metrics as forbidden, even though we may be running in dryRun mode.
if (shouldForbidOperation && options.forbidUnregisteredOperations) {
// Whether we're in dryRun mode or not, the decision as to whether
// or not we'll be forbidding execution has already been decided.
// Therefore, we'll return early and avoid nesting this entire
// remaining 30+ line block in a `if (shouldForbidOperation)` fork.
if (!shouldForbidOperation) {
return;
}

// If the user explicitly set `forbidUnregisteredOperations` to either
// `true` or a (predicate) function which returns `true` we'll
// report it within metrics as forbidden, even though we may be
// running in `dryRun` mode. This allows the user to incrementally
// go through their code-base and ensure that they've reached
// an "inbox zero" - so to speak - of operations needing registration.
if (options.forbidUnregisteredOperations) {
logger.debug(
`${logHash} Reporting operation as forbidden to Apollo trace warehouse.`,
`${logSignature} Reporting operation as forbidden to Apollo trace warehouse.`,
);
requestContext.metrics.forbiddenOperation = true;
}
Expand All @@ -238,29 +277,34 @@ for observability purposes, but all operations will be permitted.`,
if (typeof options.onForbiddenOperation === 'function') {
const onForbiddenOperation = options.onForbiddenOperation;
Promise.resolve().then(() => {
onForbiddenOperation(requestContext);
onForbiddenOperation(requestContext, {
signature,
normalizedDocument,
});
});
}
if (!options.dryRun) {
logger.debug(
`${logHash}: Execution denied because 'forbidUnregisteredOperations' was enabled for this request and the operation was not found in the local operation registry.`,
);
const error = new ForbiddenError(
'Execution forbidden: Operation not found in operation registry',
);
Object.assign(error.extensions, {
operationHash: hash,
exception: {
message: `Please register your operation with \`npx apollo client:push --tag="${schemaTag}"\`. See https://www.apollographql.com/docs/platform/operation-registry/ for more details.`,
},
});
throw error;
} else {
logger.debug(
`${dryRunPrefix} ${logHash}: Operation ${requestContext.operationName} would have been forbidden.`,
);
}
}

if (options.dryRun) {
logger.debug(
`${dryRunPrefix} ${logSignature}: Operation ${requestContext.operationName} would have been forbidden.`,
);
return;
}

logger.debug(
`${logSignature}: Execution denied because 'forbidUnregisteredOperations' was enabled for this request and the operation was not found in the local operation registry.`,
);
const error = new ForbiddenError(
'Execution forbidden: Operation not found in operation registry',
);
Object.assign(error.extensions, {
operationSignature: signature,
exception: {
message: `Please register your operation with \`npx apollo client:push --tag="${schemaTag}"\`. See https://www.apollographql.com/docs/platform/operation-registry/ for more details.`,
},
});
throw error;
},
};
},
Expand Down
Loading

0 comments on commit d892eb5

Please sign in to comment.