Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions docs/jobs-queue/jobs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,51 @@ const createdJob = await payload.jobs.queue({
})
```

#### Access Control

By default, Payload's job operations bypass access control when used from the Local API. You can enable access control by passing `overrideAccess: false` to any job operation.

To define custom access control for jobs, add an `access` property to your Jobs Config:

```ts
import type { SanitizedConfig } from 'payload'

const config: SanitizedConfig = {
// ...
jobs: {
access: {
// Control who can queue new jobs
queue: ({ req }) => {
return req.user?.roles?.includes('admin')
},
// Control who can run jobs
run: ({ req }) => {
return req.user?.roles?.includes('admin')
},
// Control who can cancel jobs
cancel: ({ req }) => {
return req.user?.roles?.includes('admin')
},
},
},
}
```

Each access control function receives the current `req` object and should return a boolean. If no access control is defined, the default behavior allows any authenticated user to perform the operation.

To use access control in the Local API:

```ts
const req = await createLocalReq({ user }, payload)

await payload.jobs.queue({
workflow: 'createPost',
input: { title: 'My Post' },
overrideAccess: false, // Enable access control
req, // Pass the request with user context
})
```

#### Cancelling Jobs

Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks.
Expand Down
4 changes: 4 additions & 0 deletions packages/payload/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
i18n: {},
jobs: {
access: {
cancel: defaultAccess,
queue: defaultAccess,
run: defaultAccess,
},
deleteJobOnComplete: true,
Expand Down Expand Up @@ -136,6 +138,8 @@ export const addDefaultsToConfig = (config: Config): Config => {
depth: 0,
...(config.jobs || {}),
access: {
cancel: defaultAccess,
queue: defaultAccess,
run: defaultAccess,
...(config.jobs?.access || {}),
},
Expand Down
20 changes: 19 additions & 1 deletion packages/payload/src/queues/config/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ export type RunJobAccessArgs = {

export type RunJobAccess = (args: RunJobAccessArgs) => boolean | Promise<boolean>

export type QueueJobAccessArgs = {
req: PayloadRequest
}

export type CancelJobAccessArgs = {
req: PayloadRequest
}
export type CancelJobAccess = (args: CancelJobAccessArgs) => boolean | Promise<boolean>
export type QueueJobAccess = (args: QueueJobAccessArgs) => boolean | Promise<boolean>

export type SanitizedJobsConfig = {
/**
* If set to `true`, the job system is enabled and a payload-jobs collection exists.
Expand All @@ -94,7 +104,15 @@ export type JobsConfig = {
*/
access?: {
/**
* By default, all logged-in users can trigger jobs.
* By default, all logged-in users can cancel jobs.
*/
cancel?: CancelJobAccess
/**
* By default, all logged-in users can queue jobs.
*/
queue?: QueueJobAccess
/**
* By default, all logged-in users can run jobs.
*/
run?: RunJobAccess
}
Expand Down
103 changes: 96 additions & 7 deletions packages/payload/src/queues/localAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { BaseJob, RunningJobFromTask } from './config/types/workflowTypes.j

import {
createLocalReq,
Forbidden,
type Job,
type Payload,
type PayloadRequest,
Expand Down Expand Up @@ -56,16 +57,33 @@ export const getJobsLocalAPI = (payload: Payload) => ({
| {
input: TypedJobs['tasks'][TTaskOrWorkflowSlug]['input']
meta?: BaseJob['meta']
/**
* If set to false, access control as defined in jobsConfig.access.queue will be run.
* By default, this is true and no access control will be run.
* If you set this to false and do not have jobsConfig.access.queue defined, the default access control will be
* run (which is a function that returns `true` if the user is logged in).
*
* @default true
*/
overrideAccess?: boolean
queue?: string
req?: PayloadRequest
// TTaskOrWorkflowlug with keyof TypedJobs['workflows'] removed:
task: TTaskOrWorkflowSlug extends keyof TypedJobs['tasks'] ? TTaskOrWorkflowSlug : never
waitUntil?: Date
workflow?: never
}
| {
input: TypedJobs['workflows'][TTaskOrWorkflowSlug]['input']
meta?: BaseJob['meta']
/**
* If set to false, access control as defined in jobsConfig.access.queue will be run.
* By default, this is true and no access control will be run.
* If you set this to false and do not have jobsConfig.access.queue defined, the default access control will be
* run (which is a function that returns `true` if the user is logged in).
*
* @default true
*/
overrideAccess?: boolean
queue?: string
req?: PayloadRequest
task?: never
Expand All @@ -79,6 +97,20 @@ export const getJobsLocalAPI = (payload: Payload) => ({
? Job<TTaskOrWorkflowSlug>
: RunningJobFromTask<TTaskOrWorkflowSlug>
> => {
const overrideAccess = args?.overrideAccess !== false
const req: PayloadRequest = args.req ?? (await createLocalReq({}, payload))

if (!overrideAccess) {
/**
* By default, jobsConfig.access.queue will be `defaultAccess` which is a function that returns `true` if the user is logged in.
*/
const accessFn = payload.config.jobs?.access?.queue ?? (() => true)
const hasAccess = await accessFn({ req })
if (!hasAccess) {
throw new Forbidden(req.t)
}
}

let queue: string | undefined = undefined

// If user specifies queue, use that
Expand Down Expand Up @@ -123,15 +155,16 @@ export const getJobsLocalAPI = (payload: Payload) => ({
collection: jobsCollectionSlug,
data,
depth: payload.config.jobs.depth ?? 0,
req: args.req,
overrideAccess,
req,
})) as ReturnType
} else {
return jobAfterRead({
config: payload.config,
doc: await payload.db.create({
collection: jobsCollectionSlug,
data,
req: args.req,
req,
}),
}) as unknown as ReturnType
}
Expand All @@ -151,6 +184,14 @@ export const getJobsLocalAPI = (payload: Payload) => ({
* @default 10
*/
limit?: number
/**
* If set to false, access control as defined in jobsConfig.access.run will be run.
* By default, this is true and no access control will be run.
* If you set this to false and do not have jobsConfig.access.run defined, the default access control will be
* run (which is a function that returns `true` if the user is logged in).
*
* @default true
*/
overrideAccess?: boolean
/**
* Adjust the job processing order using a Payload sort string.
Expand Down Expand Up @@ -198,6 +239,14 @@ export const getJobsLocalAPI = (payload: Payload) => ({

runByID: async (args: {
id: number | string
/**
* If set to false, access control as defined in jobsConfig.access.run will be run.
* By default, this is true and no access control will be run.
* If you set this to false and do not have jobsConfig.access.run defined, the default access control will be
* run (which is a function that returns `true` if the user is logged in).
*
* @default true
*/
overrideAccess?: boolean
req?: PayloadRequest
/**
Expand All @@ -221,12 +270,32 @@ export const getJobsLocalAPI = (payload: Payload) => ({
},

cancel: async (args: {
/**
* If set to false, access control as defined in jobsConfig.access.cancel will be run.
* By default, this is true and no access control will be run.
* If you set this to false and do not have jobsConfig.access.cancel defined, the default access control will be
* run (which is a function that returns `true` if the user is logged in).
*
* @default true
*/
overrideAccess?: boolean
queue?: string
req?: PayloadRequest
where: Where
}): Promise<void> => {
const newReq: PayloadRequest = args.req ?? (await createLocalReq({}, payload))
const req: PayloadRequest = args.req ?? (await createLocalReq({}, payload))

const overrideAccess = args.overrideAccess !== false
if (!overrideAccess) {
/**
* By default, jobsConfig.access.cancel will be `defaultAccess` which is a function that returns `true` if the user is logged in.
*/
const accessFn = payload.config.jobs?.access?.cancel ?? (() => true)
const hasAccess = await accessFn({ req })
if (!hasAccess) {
throw new Forbidden(req.t)
}
}

const and: Where[] = [
args.where,
Expand Down Expand Up @@ -262,18 +331,38 @@ export const getJobsLocalAPI = (payload: Payload) => ({
},
depth: 0, // No depth, since we're not returning
disableTransaction: true,
req: newReq,
req,
returning: false,
where: { and },
})
},

cancelByID: async (args: {
id: number | string
/**
* If set to false, access control as defined in jobsConfig.access.cancel will be run.
* By default, this is true and no access control will be run.
* If you set this to false and do not have jobsConfig.access.cancel defined, the default access control will be
* run (which is a function that returns `true` if the user is logged in).
*
* @default true
*/
overrideAccess?: boolean
req?: PayloadRequest
}): Promise<void> => {
const newReq: PayloadRequest = args.req ?? (await createLocalReq({}, payload))
const req: PayloadRequest = args.req ?? (await createLocalReq({}, payload))

const overrideAccess = args.overrideAccess !== false
if (!overrideAccess) {
/**
* By default, jobsConfig.access.cancel will be `defaultAccess` which is a function that returns `true` if the user is logged in.
*/
const accessFn = payload.config.jobs?.access?.cancel ?? (() => true)
const hasAccess = await accessFn({ req })
if (!hasAccess) {
throw new Forbidden(req.t)
}
}

await updateJob({
id: args.id,
Expand All @@ -288,7 +377,7 @@ export const getJobsLocalAPI = (payload: Payload) => ({
},
depth: 0, // No depth, since we're not returning
disableTransaction: true,
req: newReq,
req,
returning: false,
})
},
Expand Down
3 changes: 3 additions & 0 deletions packages/payload/src/queues/operations/runJobs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export const runJobs = async (args: RunJobsArgs): Promise<RunJobsResult> => {
} = args

if (!overrideAccess) {
/**
* By default, jobsConfig.access.run will be `defaultAccess` which is a function that returns `true` if the user is logged in.
*/
const accessFn = jobsConfig?.access?.run ?? (() => true)
const hasAccess = await accessFn({ req })
if (!hasAccess) {
Expand Down
Loading
Loading