@theguild/federation-composition
Version:
Open Source Composition library for Apollo Federation
212 lines (211 loc) • 9.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.AuthOnRequiresRule = AuthOnRequiresRule;
const graphql_1 = require("graphql");
const helpers_js_1 = require("../../../subgraph/helpers.js");
const auth_js_1 = require("../../../utils/auth.js");
function AuthOnRequiresRule(context, supergraph) {
return {
ObjectTypeField(objectTypeState, fieldState) {
for (const [graphId, fieldInGraph] of fieldState.byGraph) {
if (!fieldInGraph.requires) {
continue;
}
const selectionSet = (0, helpers_js_1.parseFields)(fieldInGraph.requires);
if (!selectionSet) {
continue;
}
const provisionedAccess = new ProvisionedAccess(objectTypeState, fieldState);
let fieldLackingAccess = ensureAccessToSelectionSet(supergraph, objectTypeState, selectionSet, provisionedAccess);
if (fieldLackingAccess) {
context.reportError(createAccessRequirementError(context.graphIdToName(graphId), `${objectTypeState.name}.${fieldState.name}`, fieldLackingAccess));
return;
}
}
},
};
}
function ensureAccessToSelectionSet(supergraph, currentType, selectionSet, provisionedAccess) {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case graphql_1.Kind.FIELD: {
if (currentType.kind === "union") {
throw new Error("Cannot select fields directly on union types.");
}
const fieldLackingAccess = ensureAccessToField(supergraph, currentType, selection, provisionedAccess);
if (fieldLackingAccess) {
return fieldLackingAccess;
}
break;
}
case graphql_1.Kind.INLINE_FRAGMENT: {
const fieldLackingAccess = ensureAccessToInlineFragment(supergraph, currentType, selection, provisionedAccess);
if (fieldLackingAccess) {
return fieldLackingAccess;
}
break;
}
case graphql_1.Kind.FRAGMENT_SPREAD: {
throw new Error("Fragment spreads are not supported in @requires.");
}
}
}
}
function ensureAccessToField(supergraph, currentType, fieldNode, provisionedAccess) {
if (fieldNode.name.value === "__typename") {
return;
}
let fieldName = fieldNode.name.value;
let fieldState = currentType.fields.get(fieldNode.name.value);
let fieldDetails = null;
if (fieldState) {
fieldDetails = {
type: fieldState.type,
scopes: fieldState.scopes,
policies: fieldState.policies,
authenticated: fieldState.authenticated,
};
}
else {
if (currentType.kind === "interface") {
throw new Error(`Field "${fieldNode.name.value}" not found on interface type "${currentType.name}".`);
}
for (const interfaceName of currentType.interfaces) {
const interfaceType = supergraph.interfaceTypes.get(interfaceName);
if (!interfaceType) {
throw new Error(`Interface "${interfaceName}" implemented by "${currentType.name}" not found in supergraph.`);
}
const interfaceFieldState = interfaceType.fields.get(fieldNode.name.value);
if (interfaceFieldState) {
if (!fieldDetails) {
fieldDetails = {
type: interfaceFieldState.type,
scopes: (0, auth_js_1.mergeScopePolicies)([], interfaceFieldState.scopes),
policies: (0, auth_js_1.mergeScopePolicies)([], interfaceFieldState.policies),
authenticated: interfaceFieldState.authenticated,
};
}
else {
fieldDetails = {
type: interfaceFieldState.type,
scopes: (0, auth_js_1.mergeScopePolicies)(fieldDetails.scopes, interfaceFieldState.scopes),
policies: (0, auth_js_1.mergeScopePolicies)(fieldDetails.policies, interfaceFieldState.policies),
authenticated: interfaceFieldState.authenticated
? true
: fieldDetails.authenticated,
};
}
}
}
}
if (fieldDetails === null) {
throw new Error(`Field "${fieldName}" not found on type "${currentType.name}".`);
}
if (!provisionedAccess.canAccess(fieldDetails)) {
return `${currentType.name}.${fieldName}`;
}
const outputTypeName = extractNamedTypeName(fieldDetails.type);
if (!outputTypeName) {
throw new Error(`Unable to extract output type name from field "${currentType.name}.${fieldName}" type "${fieldDetails.type}".`);
}
if (graphql_1.specifiedScalarTypes.some((s) => s.name === outputTypeName)) {
return;
}
const outputType = supergraph.objectTypes.get(outputTypeName) ??
supergraph.interfaceTypes.get(outputTypeName) ??
supergraph.enumTypes.get(outputTypeName) ??
supergraph.scalarTypes.get(outputTypeName) ??
supergraph.unionTypes.get(outputTypeName);
if (!outputType) {
throw new Error(`Output type "${outputTypeName}" of field "${currentType.name}.${fieldName}" not found in supergraph.`);
}
if (outputType.kind !== "union" && !provisionedAccess.canAccess(outputType)) {
return `${currentType.name}.${fieldName}`;
}
if (!fieldNode.selectionSet) {
return;
}
if (outputType.kind === "enum" || outputType.kind === "scalar") {
throw new Error(`Field "${currentType.name}.${fieldName}" of type "${outputType.name}" cannot have a selection set.`);
}
return ensureAccessToSelectionSet(supergraph, outputType, fieldNode.selectionSet, provisionedAccess);
}
function ensureAccessToInlineFragment(supergraph, currentType, inlineFragment, provisionedAccess) {
const concreteType = inlineFragment.typeCondition
? (supergraph.objectTypes.get(inlineFragment.typeCondition.name.value) ??
supergraph.interfaceTypes.get(inlineFragment.typeCondition.name.value))
: currentType;
if (!concreteType) {
throw new Error(`Type "${currentType.name}" not found in supergraph for inline fragment.`);
}
if (concreteType.kind == "union") {
throw new Error("Cannot have inline fragments on union types without type conditions.");
}
if (!provisionedAccess.canAccess(concreteType)) {
return concreteType.name;
}
return ensureAccessToSelectionSet(supergraph, concreteType, inlineFragment.selectionSet, provisionedAccess);
}
function createAccessRequirementError(graphName, fieldWithRequiresCoordinate, authCoordinate) {
const strDataRef = authCoordinate.includes(".")
? `field "${authCoordinate}"`
: `type "${authCoordinate}"`;
return new graphql_1.GraphQLError(`[${graphName}] Field "${fieldWithRequiresCoordinate}" does not specify necessary @authenticated, @requiresScopes and/or @policy auth requirements to access the transitive ${strDataRef} data from @requires selection set.`, {
extensions: {
code: "MISSING_TRANSITIVE_AUTH_REQUIREMENTS",
},
});
}
function extractNamedTypeName(typeStr) {
let typeName = typeStr;
if (!typeName)
return null;
typeName = typeName.replace(/[![\]]/g, "");
return typeName || null;
}
class ProvisionedAccess {
scopes;
policies;
authenticated;
constructor(objectTypeState, fieldState) {
this.scopes = [];
this.policies = [];
this.authenticated = false;
if (objectTypeState.authenticated) {
this.authenticated = true;
}
if (objectTypeState.scopes.length > 0) {
this.scopes = objectTypeState.scopes.slice();
}
if (objectTypeState.policies.length > 0) {
this.policies = objectTypeState.policies.slice();
}
if (fieldState.authenticated) {
this.authenticated = true;
}
if (fieldState.scopes.length > 0) {
this.scopes = (0, auth_js_1.mergeScopePolicies)(this.scopes, fieldState.scopes);
}
if (fieldState.policies.length > 0) {
this.policies = (0, auth_js_1.mergeScopePolicies)(this.policies, fieldState.policies);
}
}
canAccess(required) {
if (required.authenticated && !this.authenticated) {
return false;
}
for (const requiredScopeGroup of required.scopes) {
const satisfiedByAny = this.scopes.some((providedGroup) => requiredScopeGroup.every((scope) => providedGroup.includes(scope)));
if (!satisfiedByAny) {
return false;
}
}
for (const requiredPolicyGroup of required.policies) {
const satisfiedByAny = this.policies.some((providedGroup) => requiredPolicyGroup.every((policy) => providedGroup.includes(policy)));
if (!satisfiedByAny) {
return false;
}
}
return true;
}
}