Skip to content

Commit

Permalink
Update .sudo() to use the internal schema (#5084)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Mar 11, 2021
1 parent d97e0ab commit 40d4fff
Show file tree
Hide file tree
Showing 6 changed files with 39 additions and 12 deletions.
6 changes: 6 additions & 0 deletions .changeset/shiny-guests-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@keystone-next/keystone': minor
'@keystone-next/types': minor
---

Updated `context.sudo()` to provide access to all operations, including those excluded by `{ access: false }` in the public schema.
2 changes: 2 additions & 0 deletions docs-next/pages/apis/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ A static value of `false` implies that the operation can never be executed.
As such, Keystone will exclude the related operations and types from the GraphQL API.
For example, if you set `{ create: false }` then the mutations `createItem` and `createItems` will be removed from the GraphQL API.
If you want to keep the operations in the GraphQL API while preventing all access, you can use the [imperative](#imperative-list) access control rule `() => false`.
The excluded operations can still be access by using [`context.sudo()`](./context#new-context-creators).

```typescript
import { config, createSchema, list } from '@keystone-next/keystone/schema';
Expand Down Expand Up @@ -238,6 +239,7 @@ A static value of `false` implies that the operation can never be executed.
As such, Keystone will exclude the field from the related operations and types in the GraphQL API.
For example, if you set `{ update: false }` then the field would not appear in the `ItemUpdateInput` and `ItemsUpdateInputs` types of the GraphQL API.
If you want to keep the fields in the GraphQL API while preventing all access, you can use the [imperative](#imperative-list) access control rule `() => false`.
The excluded operations can still be access by using [`context.sudo()`](./context#new-context-creators).

```typescript
import { config, createSchema, list } from '@keystone-next/keystone/schema';
Expand Down
25 changes: 19 additions & 6 deletions packages-next/keystone/src/lib/createContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,45 @@ import { accessControlContext, skipAccessControlContext } from './createAccessCo

export function makeCreateContext({
graphQLSchema,
internalSchema,
keystone,
}: {
graphQLSchema: GraphQLSchema;
internalSchema: GraphQLSchema;
keystone: BaseKeystone;
}) {
// We precompute these helpers here rather than every time createContext is called
// because they require parsing the entire schema, which is potentially expensive.
const getArgsByList: Record<string, ReturnType<typeof getArgsFactory>> = {};
const publicGetArgsByList: Record<string, ReturnType<typeof getArgsFactory>> = {};
for (const [listKey, list] of Object.entries(keystone.lists)) {
getArgsByList[listKey] = getArgsFactory(list, graphQLSchema);
publicGetArgsByList[listKey] = getArgsFactory(list, graphQLSchema);
}

const internalGetArgsByList: Record<string, ReturnType<typeof getArgsFactory>> = {};
for (const [listKey, list] of Object.entries(keystone.lists)) {
internalGetArgsByList[listKey] = getArgsFactory(list, internalSchema);
}

const createContext = ({
sessionContext,
skipAccessControl = false,
req,
schemaName = 'public',
}: {
sessionContext?: SessionContext<any>;
skipAccessControl?: boolean;
req?: IncomingMessage;
schemaName?: 'public' | 'internal';
} = {}): KeystoneContext => {
const schema = schemaName === 'public' ? graphQLSchema : internalSchema;

const rawGraphQL: KeystoneGraphQLAPI<any>['raw'] = ({ query, variables }) => {
if (typeof query === 'string') {
query = parse(query);
}
return Promise.resolve(
execute({
schema: graphQLSchema,
schema,
document: query,
contextValue: contextToReturn,
variableValues: variables,
Expand All @@ -55,7 +66,7 @@ export function makeCreateContext({
};
const itemAPI: Record<string, ReturnType<typeof itemAPIForList>> = {};
const contextToReturn: KeystoneContext = {
schemaName: 'public',
schemaName,
...(skipAccessControl ? skipAccessControlContext : accessControlContext),
lists: itemAPI,
totalResults: 0,
Expand All @@ -65,9 +76,10 @@ export function makeCreateContext({
knex: keystone.adapter.knex,
mongoose: keystone.adapter.mongoose,
prisma: keystone.adapter.prisma,
graphql: { raw: rawGraphQL, run: runGraphQL, schema: graphQLSchema },
graphql: { raw: rawGraphQL, run: runGraphQL, schema },
maxTotalResults: keystone.queryLimits.maxTotalResults,
sudo: () => createContext({ sessionContext, skipAccessControl: true, req }),
sudo: () =>
createContext({ sessionContext, skipAccessControl: true, req, schemaName: 'internal' }),
exitSudo: () => createContext({ sessionContext, skipAccessControl: false, req }),
withSession: session =>
createContext({
Expand All @@ -82,6 +94,7 @@ export function makeCreateContext({
executeGraphQL: rawGraphQL,
gqlNames: (listKey: string) => keystone.lists[listKey].gqlNames,
};
const getArgsByList = schemaName === 'public' ? publicGetArgsByList : internalGetArgsByList;
for (const [listKey, list] of Object.entries(keystone.lists)) {
itemAPI[listKey] = itemAPIForList(list, contextToReturn, getArgsByList[listKey]);
}
Expand Down
10 changes: 7 additions & 3 deletions packages-next/keystone/src/lib/createGraphQLSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ import type { KeystoneConfig, BaseKeystone } from '@keystone-next/types';
import { getAdminMetaSchema } from '@keystone-next/admin-ui/system';
import { sessionSchema } from '../session';

export function createGraphQLSchema(config: KeystoneConfig, keystone: BaseKeystone) {
export function createGraphQLSchema(
config: KeystoneConfig,
keystone: BaseKeystone,
schemaName: 'public' | 'internal' = 'public'
) {
// Start with the core keystone graphQL schema
let graphQLSchema = makeExecutableSchema({
typeDefs: keystone.getTypeDefs({ schemaName: 'public' }),
resolvers: keystone.getResolvers({ schemaName: 'public' }),
typeDefs: keystone.getTypeDefs({ schemaName }),
resolvers: keystone.getResolvers({ schemaName }),
});

// Filter out the _label_ field from all lists
Expand Down
6 changes: 4 additions & 2 deletions packages-next/keystone/src/lib/createSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ export function createSystem(
) {
const keystone = createKeystone(config, dotKeystonePath, migrationMode, prismaClient);

const graphQLSchema = createGraphQLSchema(config, keystone);
const graphQLSchema = createGraphQLSchema(config, keystone, 'public');

const createContext = makeCreateContext({ keystone, graphQLSchema });
const internalSchema = createGraphQLSchema(config, keystone, 'internal');

const createContext = makeCreateContext({ keystone, graphQLSchema, internalSchema });

return { keystone, graphQLSchema, createContext };
}
2 changes: 1 addition & 1 deletion packages-next/types/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type KeystoneContext = {
withSession: (session: any) => KeystoneContext;
totalResults: number;
maxTotalResults: number;
schemaName: 'public';
schemaName: 'public' | 'internal';
/** @deprecated */
gqlNames: (listKey: string) => Record<string, string>; // TODO: actual keys
/** @deprecated */
Expand Down

1 comment on commit 40d4fff

@vercel
Copy link

@vercel vercel bot commented on 40d4fff Mar 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.