Skip to content

Commit 26ffbca

Browse files
paulpopusDanRibbensjacobsfletchjmikrut
authored
feat: sanitise access endpoint (#7335)
Protects the `/api/access` endpoint behind authentication and sanitizes the result, making it more secure and significantly smaller. To do this: 1. The `permission` keyword is completely omitted from the result 2. Only _truthy_ access results are returned 3. All nested permissions are consolidated when possible --------- Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com> Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com> Co-authored-by: James <james@trbl.design>
1 parent 0b9d5a5 commit 26ffbca

File tree

72 files changed

+1000
-230
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1000
-230
lines changed

packages/next/src/elements/DocumentHeader/Tabs/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { I18n } from '@payloadcms/translations'
22
import type {
33
Payload,
4-
Permissions,
54
SanitizedCollectionConfig,
65
SanitizedGlobalConfig,
6+
SanitizedPermissions,
77
} from 'payload'
88

99
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
@@ -23,7 +23,7 @@ export const DocumentTabs: React.FC<{
2323
globalConfig: SanitizedGlobalConfig
2424
i18n: I18n
2525
payload: Payload
26-
permissions: Permissions
26+
permissions: SanitizedPermissions
2727
}> = (props) => {
2828
const { collectionConfig, globalConfig, i18n, payload, permissions } = props
2929
const { config } = payload

packages/next/src/elements/DocumentHeader/Tabs/tabs/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,8 @@ export const tabs: Record<
7272
condition: ({ collectionConfig, globalConfig, permissions }) =>
7373
Boolean(
7474
(collectionConfig?.versions &&
75-
permissions?.collections?.[collectionConfig?.slug]?.readVersions?.permission) ||
76-
(globalConfig?.versions &&
77-
permissions?.globals?.[globalConfig?.slug]?.readVersions?.permission),
75+
permissions?.collections?.[collectionConfig?.slug]?.readVersions) ||
76+
(globalConfig?.versions && permissions?.globals?.[globalConfig?.slug]?.readVersions),
7877
),
7978
href: '/versions',
8079
label: ({ t }) => t('version:versions'),

packages/next/src/elements/DocumentHeader/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { I18n } from '@payloadcms/translations'
22
import type {
33
Payload,
4-
Permissions,
54
SanitizedCollectionConfig,
65
SanitizedGlobalConfig,
6+
SanitizedPermissions,
77
} from 'payload'
88

99
import { Gutter, RenderTitle } from '@payloadcms/ui'
@@ -20,7 +20,7 @@ export const DocumentHeader: React.FC<{
2020
hideTabs?: boolean
2121
i18n: I18n
2222
payload: Payload
23-
permissions: Permissions
23+
permissions: SanitizedPermissions
2424
}> = (props) => {
2525
const { collectionConfig, globalConfig, hideTabs, i18n, payload, permissions } = props
2626

packages/next/src/utilities/initReq.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { I18n, I18nClient } from '@payloadcms/translations'
2-
import type { PayloadRequest, Permissions, SanitizedConfig, User } from 'payload'
2+
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions, User } from 'payload'
33

44
import { initI18n } from '@payloadcms/translations'
55
import { headers as getHeaders } from 'next/headers.js'
@@ -11,7 +11,7 @@ import { getRequestLanguage } from './getRequestLanguage.js'
1111

1212
type Result = {
1313
i18n: I18nClient
14-
permissions: Permissions
14+
permissions: SanitizedPermissions
1515
req: PayloadRequest
1616
user: User
1717
}

packages/next/src/views/CreateFirstUser/index.client.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import type { FormProps, UserWithToken } from '@payloadcms/ui'
33
import type {
44
ClientCollectionConfig,
5-
DocumentPermissions,
65
DocumentPreferences,
76
FormState,
87
LoginWithUsernameOptions,
8+
SanitizedDocumentPermissions,
99
} from 'payload'
1010

1111
import {
@@ -24,7 +24,7 @@ import { abortAndIgnore } from '@payloadcms/ui/shared'
2424
import React, { useEffect } from 'react'
2525

2626
export const CreateFirstUserClient: React.FC<{
27-
docPermissions: DocumentPermissions
27+
docPermissions: SanitizedDocumentPermissions
2828
docPreferences: DocumentPreferences
2929
initialState: FormState
3030
loginWithUsername?: false | LoginWithUsernameOptions
@@ -114,7 +114,7 @@ export const CreateFirstUserClient: React.FC<{
114114
parentIndexPath=""
115115
parentPath=""
116116
parentSchemaPath={userSlug}
117-
permissions={null}
117+
permissions={true}
118118
readOnly={false}
119119
/>
120120
<FormSubmit size="large">{t('general:create')}</FormSubmit>

packages/next/src/views/Dashboard/Default/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { groupNavItems } from '@payloadcms/ui/shared'
2-
import type { ClientUser, Permissions, ServerProps, VisibleEntities } from 'payload'
2+
import type { ClientUser, SanitizedPermissions, ServerProps, VisibleEntities } from 'payload'
33

44
import { getTranslation } from '@payloadcms/translations'
55
import { Button, Card, Gutter, Locked } from '@payloadcms/ui'
@@ -19,7 +19,7 @@ export type DashboardProps = {
1919
}>
2020
Link: React.ComponentType<any>
2121
navGroups?: ReturnType<typeof groupNavItems>
22-
permissions: Permissions
22+
permissions: SanitizedPermissions
2323
visibleEntities: VisibleEntities
2424
} & ServerProps
2525

@@ -94,7 +94,7 @@ export const DefaultDashboard: React.FC<DashboardProps> = (props) => {
9494
path: `/collections/${slug}/create`,
9595
})
9696

97-
hasCreatePermission = permissions?.collections?.[slug]?.create?.permission
97+
hasCreatePermission = permissions?.collections?.[slug]?.create
9898
}
9999

100100
if (type === EntityType.global) {

packages/next/src/views/Dashboard/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,13 @@ export const Dashboard: React.FC<AdminViewProps> = async ({
3535

3636
const collections = config.collections.filter(
3737
(collection) =>
38-
permissions?.collections?.[collection.slug]?.read?.permission &&
38+
permissions?.collections?.[collection.slug]?.read &&
3939
visibleEntities.collections.includes(collection.slug),
4040
)
4141

4242
const globals = config.globals.filter(
4343
(global) =>
44-
permissions?.globals?.[global.slug]?.read?.permission &&
45-
visibleEntities.globals.includes(global.slug),
44+
permissions?.globals?.[global.slug]?.read && visibleEntities.globals.includes(global.slug),
4645
)
4746

4847
// Query locked global documents only if there are globals in the config

packages/next/src/views/Document/getDocumentPermissions.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import type {
33
DocumentPermissions,
44
PayloadRequest,
55
SanitizedCollectionConfig,
6+
SanitizedDocumentPermissions,
67
SanitizedGlobalConfig,
78
} from 'payload'
89

910
import {
1011
hasSavePermission as getHasSavePermission,
1112
isEditing as getIsEditing,
1213
} from '@payloadcms/ui/shared'
13-
import { docAccessOperation, docAccessOperationGlobal } from 'payload'
14+
import { docAccessOperation, docAccessOperationGlobal, sanitizePermissions } from 'payload'
1415

1516
export const getDocumentPermissions = async (args: {
1617
collectionConfig?: SanitizedCollectionConfig
@@ -19,7 +20,7 @@ export const getDocumentPermissions = async (args: {
1920
id?: number | string
2021
req: PayloadRequest
2122
}): Promise<{
22-
docPermissions: DocumentPermissions
23+
docPermissions: SanitizedDocumentPermissions
2324
hasPublishPermission: boolean
2425
hasSavePermission: boolean
2526
}> => {
@@ -91,9 +92,13 @@ export const getDocumentPermissions = async (args: {
9192
}
9293
}
9394

95+
// TODO: do this in a better way. Only doing this bc this is how the fn was written (mutates the original object)
96+
const sanitizedDocPermissions = { ...docPermissions } as any as SanitizedDocumentPermissions
97+
sanitizePermissions(sanitizedDocPermissions)
98+
9499
const hasSavePermission = getHasSavePermission({
95100
collectionSlug: collectionConfig?.slug,
96-
docPermissions,
101+
docPermissions: sanitizedDocPermissions,
97102
globalSlug: globalConfig?.slug,
98103
isEditing: getIsEditing({
99104
id,
@@ -103,7 +108,7 @@ export const getDocumentPermissions = async (args: {
103108
})
104109

105110
return {
106-
docPermissions,
111+
docPermissions: sanitizedDocPermissions,
107112
hasPublishPermission,
108113
hasSavePermission,
109114
}

packages/next/src/views/Document/getVersions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import type {
2-
DocumentPermissions,
32
Payload,
43
SanitizedCollectionConfig,
4+
SanitizedDocumentPermissions,
55
SanitizedGlobalConfig,
66
TypedUser,
77
} from 'payload'
88

99
type Args = {
1010
collectionConfig?: SanitizedCollectionConfig
11-
docPermissions: DocumentPermissions
11+
docPermissions: SanitizedDocumentPermissions
1212
globalConfig?: SanitizedGlobalConfig
1313
id?: number | string
1414
locale?: string
@@ -43,7 +43,7 @@ export const getVersions = async ({
4343
const entityConfig = collectionConfig || globalConfig
4444
const versionsConfig = entityConfig?.versions
4545

46-
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions?.permission)
46+
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions)
4747

4848
if (!shouldFetchVersions) {
4949
const hasPublishedDoc = Boolean((collectionConfig && id) || globalConfig)

packages/next/src/views/Document/getViewsFromConfig.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type {
22
AdminViewProps,
3-
CollectionPermission,
4-
GlobalPermission,
53
PayloadComponent,
64
SanitizedCollectionConfig,
5+
SanitizedCollectionPermission,
76
SanitizedConfig,
87
SanitizedGlobalConfig,
8+
SanitizedGlobalPermission,
99
ServerSideEditViewProps,
1010
} from 'payload'
1111
import type React from 'react'
@@ -38,7 +38,7 @@ export const getViewsFromConfig = ({
3838
routeSegments: string[]
3939
} & (
4040
| {
41-
docPermissions: CollectionPermission | GlobalPermission
41+
docPermissions: SanitizedCollectionPermission | SanitizedGlobalPermission
4242
overrideDocPermissions?: false | undefined
4343
}
4444
| {
@@ -78,19 +78,15 @@ export const getViewsFromConfig = ({
7878
const [collectionEntity, collectionSlug, segment3, segment4, segment5, ...remainingSegments] =
7979
routeSegments
8080

81-
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
81+
if (!overrideDocPermissions && !docPermissions?.read) {
8282
throw new Error('not-found')
8383
} else {
8484
// `../:id`, or `../create`
8585
switch (routeSegments.length) {
8686
case 3: {
8787
switch (segment3) {
8888
case 'create': {
89-
if (
90-
!overrideDocPermissions &&
91-
'create' in docPermissions &&
92-
docPermissions?.create?.permission
93-
) {
89+
if (!overrideDocPermissions && 'create' in docPermissions && docPermissions.create) {
9490
CustomView = {
9591
ComponentConfig: getCustomViewByKey(views, 'default'),
9692
}
@@ -176,7 +172,7 @@ export const getViewsFromConfig = ({
176172
}
177173

178174
case 'versions': {
179-
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
175+
if (!overrideDocPermissions && docPermissions?.readVersions) {
180176
CustomView = {
181177
ComponentConfig: getCustomViewByKey(views, 'versions'),
182178
}
@@ -229,7 +225,7 @@ export const getViewsFromConfig = ({
229225
// `../:id/versions/:version`, etc
230226
default: {
231227
if (segment4 === 'versions') {
232-
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
228+
if (!overrideDocPermissions && docPermissions?.readVersions) {
233229
CustomView = {
234230
ComponentConfig: getCustomViewByKey(views, 'version'),
235231
}
@@ -281,7 +277,7 @@ export const getViewsFromConfig = ({
281277
if (globalConfig) {
282278
const [globalEntity, globalSlug, segment3, ...remainingSegments] = routeSegments
283279

284-
if (!overrideDocPermissions && !docPermissions?.read?.permission) {
280+
if (!overrideDocPermissions && !docPermissions?.read) {
285281
throw new Error('not-found')
286282
} else {
287283
switch (routeSegments.length) {
@@ -323,7 +319,7 @@ export const getViewsFromConfig = ({
323319
}
324320

325321
case 'versions': {
326-
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
322+
if (!overrideDocPermissions && docPermissions?.readVersions) {
327323
CustomView = {
328324
ComponentConfig: getCustomViewByKey(views, 'versions'),
329325
}
@@ -340,7 +336,7 @@ export const getViewsFromConfig = ({
340336
}
341337

342338
default: {
343-
if (!overrideDocPermissions && docPermissions?.read?.permission) {
339+
if (!overrideDocPermissions && docPermissions?.read) {
344340
const baseRoute = [adminRoute, globalEntity, globalSlug, segment3]
345341
.filter(Boolean)
346342
.join('/')
@@ -381,7 +377,7 @@ export const getViewsFromConfig = ({
381377
default: {
382378
// `../:slug/versions/:version`, etc
383379
if (segment3 === 'versions') {
384-
if (!overrideDocPermissions && docPermissions?.readVersions?.permission) {
380+
if (!overrideDocPermissions && docPermissions?.readVersions) {
385381
CustomView = {
386382
ComponentConfig: getCustomViewByKey(views, 'version'),
387383
}

0 commit comments

Comments
 (0)