Class QuartzHttpApiOptions
- Namespace
- Quartz
- Assembly
- Quartz.AspNetCore.dll
How the HTTP API is served.
public sealed class QuartzHttpApiOptions
- Inheritance
-
QuartzHttpApiOptions
- Inherited Members
Remarks
There is one set of these per process, not one per scheduler. The API serves every scheduler in the
container through one set of endpoints — a request names the scheduler it is for — so what is
configured here describes the endpoints rather than any scheduler. That is why there is no
IQuartzBuilder registration: calling services.AddQuartzHttpApi(configure) twice configures
the same options twice, and the last callback registered wins for any setting both of them touch.
Properties
ApiPath
The path the API is served under. It is a property of the process, not of a scheduler: every scheduler is reached under this one path.
public string ApiPath { get; set; }
Property Value
Remarks
MapQuartzHttpApi(pattern) says the same thing where the endpoints are mapped, which is
where the rest of an application's routes are written, and a pattern given there wins over this.
EventStreamHeartbeatInterval
How long GET {ApiPath}/schedulers/{name}/events may say nothing before it emits a
Heartbeat frame. Fifteen seconds by default.
public TimeSpan EventStreamHeartbeatInterval { get; set; }
Property Value
Remarks
A heartbeat is what keeps a stream that has nothing to report alive, and it is a setting rather
than a constant because the thing it has to stay under is not Quartz's: a reverse proxy closes an
idle connection after its own read timeout — nginx's proxy_read_timeout is 60 seconds,
Azure's front doors 90, and a deployment is free to configure less. Set this below whatever sits
between the API and its readers, and a stream that would have been cut every minute stays open.
It bounds silence rather than spacing the frames: a scheduler that is producing events sends no heartbeats at all, and the interval starts again after each frame. One goes out when the stream opens whatever this says, because a response's headers are not sent until its body is written.
IncludeStackTraceInProblemDetails
Whether a failure's stack trace is included in the problem details returned to the caller.
public bool IncludeStackTraceInProblemDetails { get; set; }
Property Value
Remarks
It also puts the real message of a 500 back in the response body. Both are things to read
while developing and neither is something to ship: a fault's message routinely names the server,
the database, the login or the constraint that produced it.
IsJobTypeAllowed
Which job types a request may name. Null — the default — allows every one, which is what every earlier release did.
public Func<string, bool>? IsJobTypeAllowed { get; set; }
Property Value
Remarks
This is the narrowing for the thing SECURITY.md says is not a vulnerability: a caller who
passes authorization can schedule any type that implements IJob, and
Quartz.Jobs' NativeJob implements it and starts the executable its job data names.
An operator who knows which jobs their deployment schedules can say so here, and the surface stops
being "any IJob on the probing path".
The predicate is given the job type name, exactly as the request spelled it. Nothing
resolves it first, and that is the point: a name arriving over HTTP is data until the side that
runs the job loads it, and resolving one here to compare types would be the assembly probe this
API exists not to make. So match on the string — a set of names, a namespace prefix — and allow
for the fact that one type has more than one spelling: Acme.Jobs.Nightly, Acme.Jobs and the
same name with Version, Culture and PublicKeyToken after it are both it. A
predicate matching a bare StartsWith on the namespace covers every spelling at once.
A refused name is 403 with problem details naming the type the request asked for and
nothing else — it is the caller's own input, so it gives nothing away. The refusal is per request
rather than per job, so schedule-jobs stores none of its batch when one job in it names a
type that is not allowed.
It is one predicate for the process, like everything else here, so it cannot say that one
scheduler may run a type another may not; the request's scheduler name is not passed to it. The
dashboard's QuartzDashboardOptions.IsJobTypeAllowed is the same setting for that surface,
and the two are configured separately because a deployment can map one of them and not the other.
MaxPageSize
The most items one paged request may return: 1000 by default, and 0 for no limit.
A take naming a number above it is a 400.
public int MaxPageSize { get; set; }
Property Value
Remarks
A page size is the only thing on this API a caller can use to make the server do arbitrary work, and until 4.0.0-beta.1 nothing bounded it: one request could materialize every trigger in the store while the bulk key fetch next door refused 1001 keys. The default is that same 1000.
?take=all is bounded by this rather than refused by it. It does not name a number — it says
"as many as you will give me" — so it is answered with this many, and hasMore says whether
that was all of them. A listing whose matches fit under the cap therefore answers exactly as it
would with no cap at all, which is what keeps the 3.x-compatible listings
(GetJobKeys and its neighbours) working through HttpScheduler: they ask for
everything whether the answer is three rows or three million.
Set it to 0 where an export or a migration really has to take everything in one call, and
put the API behind something that says who may.
OperationAuthorizationPolicy
The authorization policy every operation is held to, evaluated against a SchedulerResource carrying the scheduler's name, the operation's name and the operator role it needs. Null — the default — leaves the API as it was: whoever reaches a route may call it. Added in 4.8.
public string? OperationAuthorizationPolicy { get; set; }
Property Value
Remarks
Name a policy built with RequireSchedulerRoles(…) and each route needs the role the route
table gives it: View for every read,
Operate for pausing, resuming, triggering and interrupting,
Administer for adding, deleting, rescheduling and shutting down. A
refusal is 403 with problem details naming the operation and the level, logged as event
9009.
The check runs after SchedulerAuthorizationPolicy's, before ReadOnly's
and before the handler, so no body is read for a refused call. trigger is asked again at
Administer once its body is read and carries job data. The scheduler
listing is filtered to the schedulers the caller may view.
The two policies compose: SchedulerAuthorizationPolicy decides which schedulers a caller may reach, this one what they may do there, and ReadOnly stays the ceiling for everybody.
ReadOnly
Whether the API serves reads only. false by default, which is what every earlier release did.
public bool ReadOnly { get; set; }
Property Value
Remarks
Set it and every route that changes something answers 403 with problem details saying the
API is read-only, before the handler runs and therefore before a body is read or a scheduler is
looked up. It is the whole mutating surface in one switch: an operator serving a dashboard or a
monitoring tool out of a production process wants the reads and none of the writes, and getting
that from route-level authorization means naming thirty-odd routes and keeping the list current.
What counts as mutating is a property of the route rather than of its verb. The two bulk fetches —
POST …/jobs/fetch and POST …/triggers/fetch — are reads that take a body of keys, so
they are served; everything else that is not a GET is refused. Pausing, resuming,
interrupting and resetting a trigger from its error state all count: each of them changes what the
scheduler will do next.
It is a bound on this API and on nothing else. The scheduler in the process goes on firing jobs,
and a dashboard mapped beside it has its own QuartzDashboardOptions.ReadOnly — neither
setting binds the other surface.
SchedulerAuthorizationPolicy
The authorization policy every route that names a scheduler is held to, evaluated against a
SchedulerResource carrying that name. Null — the default — leaves the API as it was:
whatever RequireAuthorization(…) the application put on the mapped group, applied uniformly
to every scheduler.
public string? SchedulerAuthorizationPolicy { get; set; }
Property Value
Remarks
Set it and each request is checked with
IAuthorizationService.AuthorizeAsync(user, new SchedulerResource(name), policy) before the
scheduler is looked up, so a caller who fails cannot tell "no such scheduler" from "not yours": a
refusal is 403 with problem details, and a 404 only ever answers a scheduler the
caller was allowed to ask about. The scheduler listing is filtered the same way.
The check is authorization, never authentication: an anonymous caller gets whatever the policy
says, which is a 403 when the policy refuses. Put RequireAuthorization() on the
mapped group as well to have an anonymous caller challenged with a 401 first.