Skip to content

Commit

Permalink
Make operations permissions required (#7848)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Cousens <413395+dcousens@users.noreply.github.com>
Co-authored-by: Daniel Cousens <dcousens@users.noreply.github.com>
  • Loading branch information
3 people committed Aug 31, 2022
1 parent 824dafa commit 7ae3bb7
Show file tree
Hide file tree
Showing 119 changed files with 656 additions and 140 deletions.
5 changes: 5 additions & 0 deletions .changeset/few-glasses-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': major
---

Changes `.access` control on `list` to required, with new `allowAll` and `denyAll` functions for easy shorthand.
62 changes: 55 additions & 7 deletions docs/pages/docs/apis/access-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: "Complete reference docs for Keystone’s Access Control API. Confi
---

{% hint kind="warn" %}
We recently improved this API so it’s easier to program, and makes it harder to introduce security gaps in your system. If you were using it prior to September 6th 2021, [read this guide](/updates/new-access-control) for info on how to upgrade.
We recently improved this API so it’s easier to program, and makes it harder to introduce security gaps in your system. If you were using it prior to September 1st 2022, you will need to now configure access control for your operations.
{% /hint %}

The `access` property of the [list configuration](./schema) object configures who can query, create, update, and delete items in your Keystone system.
Expand All @@ -29,7 +29,6 @@ export default config({
## List Access Control

Keystone provides support for access control and filtering on a per-list basis.
By default, all lists and fields are public without additional configuration.

There are three types of access control that can be configured on the `access` object:

Expand All @@ -43,6 +42,21 @@ Each of these types of access control are applied before [hooks](./hooks).
Please note that Keystone does not automatically configure nor leverage database row security policies or row level security
{% /hint %}

If you are happy to allow all parts of a list to be accessible you can use:

```typescript
import { config, list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';

export default config({
lists: {
ListKey: list({
access: allowAll,
}),
},
});
```

{% hint kind="tip" %}
**Access denied:** **Mutations** return `null` data with an access denied error. **Queries** never return access denied errors, and instead treat items as if they didn't exist.
{% /hint %}
Expand All @@ -61,11 +75,7 @@ If access is **denied** due to any of the access control methods, the following
### Operation

Operation-level access control lets you control which operations can be accessed by a user based on the `session` and `context`.
Individual functions can be provided for each of the operations.

{% hint kind="warn" %}
Each operation type _defaults to public_ unless configured.
{% /hint %}
Individual functions can be provided for each of the operations. All operations must be configured.

{% hint kind="tip" %}
These functions must return `true` if the operation is allowed, or `false` if it is not allowed.
Expand All @@ -90,6 +100,44 @@ export default config({
});
```

If you are happy setting all of them to true, you can set it for all operations with:

```typescript
import { config, list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';

export default config({
lists: {
ListKey: list({
access: {
operation: allowAll
},
}),
},
});
```

If you want to set all but one operation to true, you can do so using `allOperations` and `denyAll` from `@keystone-6/core/access` such as:

```typescript
import { config, list } from '@keystone-6/core';
import { denyAll, allOperations } from '@keystone-6/core/access';

export default config({
lists: {
ListKey: list({
access: {
operation: {
...allOperations(denyAll)
// hint: unconditionally returning `true` is equivalent to using allowAll for this operation
query: ({ session, context, listKey, operation }) => true,
}
},
}),
},
});
```

{% hint kind="warn" %}
The `query` access control is applied only when running GraphQL query operations.
A user can still **read** fields as part of a return query when using a mutation `operation` (`create`, `update` or `delete`).
Expand Down
3 changes: 3 additions & 0 deletions examples/assets-local/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { select, relationship, text, timestamp, image, file } from '@keystone-6/core/fields';

export const lists = {
Post: list({
access: allowAll,
fields: {
title: text({ validation: { isRequired: true } }),
status: select({
Expand All @@ -20,6 +22,7 @@ export const lists = {
},
}),
Author: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ isIndexed: 'unique', validation: { isRequired: true } }),
Expand Down
3 changes: 3 additions & 0 deletions examples/assets-s3/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { select, relationship, text, timestamp, image, file } from '@keystone-6/core/fields';

export const lists = {
Post: list({
access: allowAll,
fields: {
title: text({ validation: { isRequired: true } }),
status: select({
Expand All @@ -20,6 +22,7 @@ export const lists = {
},
}),
Author: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ isIndexed: 'unique', validation: { isRequired: true } }),
Expand Down
2 changes: 2 additions & 0 deletions examples/auth/schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { list } from '@keystone-6/core';
import { allOperations, allowAll } from '@keystone-6/core/access';
import { text, checkbox, password } from '@keystone-6/core/fields';

export const lists = {
User: list({
access: {
operation: {
...allOperations(allowAll),
// Only allow admins to delete users
delete: ({ session }) => session?.data.isAdmin,
},
Expand Down
4 changes: 4 additions & 0 deletions examples/basic/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@keystone-6/core/fields';
import { document } from '@keystone-6/fields-document';
import { v4 } from 'uuid';
import { allowAll } from '@keystone-6/core/access';
import { Context, Lists } from '.keystone/types';

type AccessArgs = {
Expand All @@ -33,6 +34,7 @@ export const access = {
const randomNumber = () => Math.round(Math.random() * 10);

const User: Lists.User = list({
access: allowAll,
ui: {
listView: {
initialColumns: ['name', 'posts', 'avatar'],
Expand Down Expand Up @@ -94,6 +96,7 @@ const User: Lists.User = list({
export const lists: Lists = {
User,
PhoneNumber: list({
access: allowAll,
ui: {
isHidden: true,
// parentRelationship: 'user',
Expand Down Expand Up @@ -130,6 +133,7 @@ export const lists: Lists = {
},
}),
Post: list({
access: allowAll,
fields: {
title: text({ access: {} }),
status: select({
Expand Down
3 changes: 3 additions & 0 deletions examples/blog/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { select, relationship, text, timestamp } from '@keystone-6/core/fields';

export const lists = {
Post: list({
access: allowAll,
fields: {
title: text({ validation: { isRequired: true } }),
status: select({
Expand All @@ -18,6 +20,7 @@ export const lists = {
},
}),
Author: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ isIndexed: 'unique', validation: { isRequired: true } }),
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-admin-ui-logo/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields';
import { select } from '@keystone-6/core/fields';

export const lists = {
Task: list({
access: allowAll,
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
Expand All @@ -20,6 +22,7 @@ export const lists = {
},
}),
Person: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
tasks: relationship({ ref: 'Task.assignedTo', many: true }),
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-admin-ui-navigation/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields';
import { select } from '@keystone-6/core/fields';

export const lists = {
Task: list({
access: allowAll,
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
Expand All @@ -20,6 +22,7 @@ export const lists = {
},
}),
Person: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
tasks: relationship({ ref: 'Task.assignedTo', many: true }),
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-admin-ui-pages/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields';
import { select } from '@keystone-6/core/fields';

export const lists = {
Task: list({
access: allowAll,
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
Expand All @@ -20,6 +22,7 @@ export const lists = {
},
}),
Person: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
tasks: relationship({ ref: 'Task.assignedTo', many: true }),
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-field-view/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields';
import { json, select } from '@keystone-6/core/fields';

export const lists = {
Task: list({
access: allowAll,
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
Expand All @@ -29,6 +31,7 @@ export const lists = {
},
}),
Person: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
tasks: relationship({ ref: 'Task.assignedTo', many: true }),
Expand Down
2 changes: 2 additions & 0 deletions examples/custom-field/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { list } from '@keystone-6/core';

import { allowAll } from '@keystone-6/core/access';
import { text } from './1-text-field';
import { stars } from './2-stars-field';
import { pair } from './3-pair-field';
Expand All @@ -8,6 +9,7 @@ import { Lists } from '.keystone/types';

export const lists: Lists = {
Post: list({
access: allowAll,
fields: {
content: text({
ui: {
Expand Down
3 changes: 3 additions & 0 deletions examples/custom-session-validation/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { list } from '@keystone-6/core';
import { allowAll } from '@keystone-6/core/access';
import { checkbox, password, relationship, text, timestamp } from '@keystone-6/core/fields';
import { select } from '@keystone-6/core/fields';

export const lists = {
Task: list({
access: allowAll,
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
Expand All @@ -20,6 +22,7 @@ export const lists = {
},
}),
Person: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
// Added an email and password pair to be used with authentication
Expand Down
3 changes: 3 additions & 0 deletions examples/default-values/schema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { list } from '@keystone-6/core';
import { checkbox, relationship, text, timestamp } from '@keystone-6/core/fields';
import { select } from '@keystone-6/core/fields';
import { allowAll } from '@keystone-6/core/access';
import { Lists } from '.keystone/types';

export const lists: Lists = {
Task: list({
access: allowAll,
fields: {
label: text({ validation: { isRequired: true } }),
priority: select({
Expand Down Expand Up @@ -65,6 +67,7 @@ export const lists: Lists = {
},
}),
Person: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
tasks: relationship({ ref: 'Task.assignedTo', many: true }),
Expand Down
3 changes: 3 additions & 0 deletions examples/document-field/schema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { list } from '@keystone-6/core';
import { select, relationship, text, timestamp } from '@keystone-6/core/fields';
import { document } from '@keystone-6/fields-document';
import { allowAll } from '@keystone-6/core/access';

export const lists = {
Post: list({
access: allowAll,
fields: {
title: text({ validation: { isRequired: true } }),
slug: text({ isIndexed: 'unique', validation: { isRequired: true } }),
Expand Down Expand Up @@ -40,6 +42,7 @@ export const lists = {
},
}),
Author: list({
access: allowAll,
fields: {
name: text({ validation: { isRequired: true } }),
email: text({ isIndexed: 'unique', validation: { isRequired: true } }),
Expand Down
2 changes: 2 additions & 0 deletions examples/ecommerce/schemas/CartItem.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { integer, relationship } from '@keystone-6/core/fields';
import { list } from '@keystone-6/core';
import { allOperations, allowAll } from '@keystone-6/core/access';
import { rules, isSignedIn } from '../access';

export const CartItem = list({
access: {
operation: {
...allOperations(allowAll),
create: isSignedIn,
},
filter: {
Expand Down
1 change: 1 addition & 0 deletions examples/ecommerce/schemas/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const Order: Lists.Order = list({
create: isSignedIn,
update: () => false,
delete: () => false,
query: () => true,
},
filter: { query: rules.canOrder },
},
Expand Down
1 change: 1 addition & 0 deletions examples/ecommerce/schemas/OrderItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const OrderItem = list({
create: isSignedIn,
update: () => false,
delete: () => false,
query: () => true,
},
filter: {
query: rules.canManageOrderItems,
Expand Down
Loading

0 comments on commit 7ae3bb7

Please sign in to comment.