Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge master into 2.0.0 #364

Merged
merged 9 commits into from
Jul 28, 2021
23 changes: 23 additions & 0 deletions docs/asciidoc/auth/authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,26 @@ type Todo {

extend type Todo @auth(rules: [{ isAuthenticated: true }])
----

== `allowUnauthenticated`
In some cases, you may want to allow unauthenticated requests while also having auth-based rules. You can use the `allowUnauthenticated` parameter to avoid throwing an exception if no auth is present in the context.

In the example below, only the publisher can see his blog posts if it is not published yet. Once the blog post is published, anyone can see it.

[source, graphql]
----
type BlogPost
@auth(
rules: [
{
operations: [READ]
where: { OR: [{ publisher: "$jwt.sub" }, { published: true }] }
allowUnauthenticated: true
}
]
) {
id: ID!
publisher: String!
published: Boolean!
}
----
12 changes: 11 additions & 1 deletion packages/graphql/src/schema/get-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ import { DirectiveNode, valueFromASTUntyped } from "graphql";
import { Auth, AuthRule, AuthOperations } from "../types";

const validOperations: AuthOperations[] = ["CREATE", "READ", "UPDATE", "DELETE", "CONNECT", "DISCONNECT"];
const validFields = ["operations", "AND", "OR", "allow", "where", "bind", "isAuthenticated", "roles"];
const validFields = [
"operations",
"AND",
"OR",
"allow",
"where",
"bind",
"isAuthenticated",
"allowUnauthenticated",
"roles",
];

function getAuth(directive: DirectiveNode): Auth {
const auth: Auth = { rules: [], type: "JWT" };
Expand Down
16 changes: 13 additions & 3 deletions packages/graphql/src/schema/make-augmented-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import { mergeTypeDefs } from "@graphql-tools/merge";
import { IExecutableSchemaDefinition, makeExecutableSchema } from "@graphql-tools/schema";
import { forEachField } from "@graphql-tools/utils";
import camelCase from "camelcase";
import {
DefinitionNode,
Expand Down Expand Up @@ -50,12 +51,13 @@ import { Node, Exclude } from "../classes";
import getAuth from "./get-auth";
import { PrimitiveField, Auth, CustomEnumField, ConnectionQueryArgs } from "../types";
import {
findResolver,
countResolver,
createResolver,
deleteResolver,
cypherResolver,
defaultFieldResolver,
deleteResolver,
findResolver,
updateResolver,
countResolver,
} from "./resolvers";
import * as Scalars from "./scalars";
import parseExcludeDirective from "./parse-exclude-directive";
Expand Down Expand Up @@ -1307,6 +1309,14 @@ function makeAugmentedSchema(
resolvers: generatedResolvers,
});

// Assign a default field resolver to account for aliasing of fields
forEachField(schema, (field) => {
if (!field.resolve) {
// eslint-disable-next-line no-param-reassign
field.resolve = defaultFieldResolver;
}
});

return {
nodes,
relationships,
Expand Down
11 changes: 9 additions & 2 deletions packages/graphql/src/schema/resolvers/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

import camelCase from "camelcase";
import pluralize from "pluralize";
import { FieldNode, GraphQLResolveInfo } from "graphql";
import { execute } from "../../utils";
import { translateCreate } from "../../translate";
import { Node } from "../../classes";
import { Context } from "../../types";

export default function createResolver({ node }: { node: Node }) {
async function resolve(_root: any, _args: any, _context: unknown) {
async function resolve(_root: any, _args: any, _context: unknown, info: GraphQLResolveInfo) {
const context = _context as Context;
const [cypher, params] = translateCreate({ context, node });

Expand All @@ -36,8 +37,14 @@ export default function createResolver({ node }: { node: Node }) {
context,
});

const responseField = info.fieldNodes[0].selectionSet?.selections.find(
(selection) => selection.kind === "Field" && selection.name.value === pluralize(camelCase(node.name))
) as FieldNode; // Field exist by construction and must be selected as it is the only field.

const responseKey = responseField.alias ? responseField.alias.value : responseField.name.value;

return {
[pluralize(camelCase(node.name))]: Object.values(result[0] || {}),
[responseKey]: Object.values(result[0] || {}),
};
}

Expand Down
36 changes: 36 additions & 0 deletions packages/graphql/src/schema/resolvers/defaultField.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { GraphQLResolveInfo } from "graphql";

/**
* Based on the default field resolver used by graphql-js that accounts for aliased fields
* @link https://github.com/graphql/graphql-js/blob/main/src/execution/execute.ts#L999-L1015
*/
// eslint-disable-next-line consistent-return
export default function defaultFieldResolver(source: any, args: any, context: unknown, info: GraphQLResolveInfo) {
const responseKey = info.fieldNodes[0].alias ? info.fieldNodes[0].alias.value : info.fieldNodes[0].name.value;
if ((typeof source === "object" && source !== null) || typeof source === "function") {
const property = source[responseKey];
if (typeof property === "function") {
return source[responseKey](args, context, info);
}
return property;
}
}
7 changes: 4 additions & 3 deletions packages/graphql/src/schema/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
* limitations under the License.
*/

export { default as countResolver } from "./count";
export { default as createResolver } from "./create";
export { default as cypherResolver } from "./cypher";
export { default as defaultFieldResolver } from "./defaultField";
export { default as deleteResolver } from "./delete";
export { default as findResolver } from "./read";
export { default as updateResolver } from "./update";
export { default as deleteResolver } from "./delete";
export { default as cypherResolver } from "./cypher";
export { default as countResolver } from "./count";
17 changes: 13 additions & 4 deletions packages/graphql/src/schema/resolvers/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

import camelCase from "camelcase";
import pluralize from "pluralize";
import { FieldNode, GraphQLResolveInfo } from "graphql";
import { execute } from "../../utils";
import { translateUpdate } from "../../translate";
import { Node } from "../../classes";
import { Context } from "../../types";
import { translateUpdate } from "../../translate";
import { execute } from "../../utils";

export default function updateResolver({ node }: { node: Node }) {
async function resolve(_root: any, _args: any, _context: unknown) {
async function resolve(_root: any, _args: any, _context: unknown, info: GraphQLResolveInfo) {
const context = _context as Context;
const [cypher, params] = translateUpdate({ context, node });
const result = await execute({
Expand All @@ -35,7 +36,15 @@ export default function updateResolver({ node }: { node: Node }) {
context,
});

return { [pluralize(camelCase(node.name))]: result.map((x) => x.this) };
const responseField = info.fieldNodes[0].selectionSet?.selections.find(
(selection) => selection.kind === "Field" && selection.name.value === pluralize(camelCase(node.name))
) as FieldNode; // Field exist by construction and must be selected as it is the only field.

const responseKey = responseField.alias ? responseField.alias.value : responseField.name.value;

return {
[responseKey]: result.map((x) => x.this),
};
}

return {
Expand Down
Loading