Policy targets
Pre-filter a policy by action, resource, or role with target(); what NotApplicable means, how target patterns match, and the unreachable-target error
Targets scope a whole policy to specific actions, resources, or roles. A request that misses the targets never sees the policy's rules, and the policy contributes nothing to the final decision.
What a target miss means
A policy whose targets do not match the request is NotApplicable: the engine skips it in the cross-policy combine instead of counting it as a defaultEffect vote (since 2.0.0). Three worked cases:
SKIP1 and SKIP2 are skipped by every policyCombine mode; only EVAL runs the combining algorithm. The IDecision for a skipped policy carries applicable: false and the reason Policy "<id>" targets do not match. Not applicable.
Setting targets
import { definePolicy } from '@gentleduck/iam'
const adminPolicy = definePolicy('admin-only')
.name('Admin-Only Policy')
.target({ roles: ['admin', 'super-admin'] })
.algorithm('deny-overrides')
.rule('allow-admin-all', (r) => r.allow().on('*').of('*'))
.build()
// Evaluated only for subjects whose effective roles include admin or super-admin.
const writePolicy = definePolicy('write-restrictions')
.name('Write Restrictions')
.target({
actions: ['create', 'update', 'delete'],
resources: ['post', 'comment'],
})
.algorithm('deny-overrides')
.rule('business-hours', (r) =>
r
.deny()
.on('*')
.of('*')
.when((w) => w.or((o) => o.env('hour', 'lt', 9).env('hour', 'gte', 17))),
)
.rule('allow-otherwise', (r) => r.allow().on('*').of('*'))
.build()
// Applies only to writes on posts and comments; reads and other resources are untouched.
The allow-otherwise rule matters: without it, a write inside business hours would match the targets, match no rule, and fold defaultEffect (deny) into the decision. See "Target matched, no rule matched" below.
Target fields
| Field | Type | Match rule |
|---|---|---|
actions | readonly (TAction | '*')[] | At least one entry matches request.action via the action matcher ('*', exact, or prefix:*) |
resources | readonly (TResource | '*')[] | At least one entry matches request.resource.type via matchesResource ('*', exact, prefix:*, prefix.*) |
roles | readonly TRole[] | At least one entry is present in subject.roles (exact string membership, no wildcard) |
Every field is optional; an omitted or empty field constrains nothing. Set fields combine with AND. target() replaces the whole object on each call.
How the applicability decision is made
The engine runs two checks before any rule condition is evaluated. Both can make the policy NotApplicable.
ACT,RES,ROLEare the target dimensions (policyAppliesinsrc/core/evaluate/evaluate.libs.ts).SHAPEis the rule-shape check: a policy aboutupdatehas nothing to say aboutreadeven when its targets are silent, so it abstains instead of votingdefaultEffect(evaluatePolicyinsrc/core/evaluate/evaluate.ts). The reason string isPolicy "<id>" has no rule for this action/resource. Not applicable.subject.rolesatROLEare the effective roles after inheritance and scoped-role enrichment - see scoped roles and evaluation pipeline.
Target patterns and hierarchy
targets.resources goes through the same matchesResource function as rule resources, so suffix wildcards work and bare literals stay literal. Pinned by the evaluate tests:
| Target | Request resource | Applicable? |
|---|---|---|
'dashboard.*' | dashboard.users | yes |
'dashboard.*' | dashboard | no |
'dashboard' | dashboard.users | no |
'org:*' | org:project | yes |
The only difference from rule resources is that a dotted request resource does not switch targets to the dot-only hierarchical matcher; both :* and .* are always honoured on targets. Details on rule matching.
Target matched, no rule matched
When targets match (or are absent), at least one rule has a matching action/resource shape, but no rule's conditions hold, the combining algorithm returns defaultEffect and the policy does vote. With the default defaultEffect: 'deny' and policyCombine: 'and', that vote is a deny.
This is why a target can widen what a policy refuses: adding a resource to target({ resources }) without an allow rule that covers it denies every caller for that resource. Since 5.4.0, PolicyBuilder.build() rejects that shape with UNREACHABLE_TARGET (an error; it was a warning in 5.3.0):
[@gentleduck/iam:builder] PolicyBuilder.build("p") rejected by validator - UNREACHABLE_TARGET at "targets": Target admits "delete" but no allow rule covers it, so every request matching it is denied by this policy. Add a rule that allows it, or narrow the target.
The check fires only when the policy has at least one allow rule; a purely restrictive (deny-only) policy is exempt because denying everything it targets is the point. Only dimensions the target names are checked - an omitted dimension is unconstrained, so a target naming only impersonate is satisfied by an allow rule on .of('users'). Targets with more than 1000 action x resource pairs skip the check with a warning.
To keep a restriction policy from denying by default:
- Add a catch-all allow rule (as
allow-otherwiseabove) underdeny-overrides, or a trailing allow underfirst-match. - Or set the engine
defaultEffect: 'allow'and treat policies purely as exceptions (requiresallowFailOpeninproductionmode; see engine modes).
When to use targets versus rule conditions
| Situation | Use |
|---|---|
| Policy applies to specific roles only | target({ roles }) |
| Policy only matters for write operations | target({ actions }) |
| Policy is resource-type specific | target({ resources }) |
| Filtering depends on attributes or environment | Rule conditions |
| Policy must abstain (not vote) for unrelated requests | Targets, or rules whose actions/resources do not cover the request |
Targets are a fast pre-filter: the engine skips the whole policy before touching rules. In production mode the compiled table uses targets.actions / targets.resources at compile time to decide which cells a policy's rules belong in; see compiled table.
API reference
target(t: NonNullable<AccessControl.IPolicy<TAction, TResource, TRole>['targets']>): this
// AccessControl.IPolicy['targets']
readonly targets?: {
readonly actions?: readonly (TAction | '*')[]
readonly resources?: readonly (TResource | '*')[]
readonly roles?: readonly TRole[]
}
IDecision.applicable is false for a NotApplicable policy decision and omitted otherwise.
Gotchas
rolesis exact membership againstsubject.roles;'*'is not special there.- A policy with targets but zero rules is NotApplicable for everything:
SHAPEhas nothing to match. A policy with a'*'/'*'deny rule and matching targets denies everything the target admits. - In
productionmodepolicyCombine: 'first-applicable'is refused by the engine constructor; the other modes honour NotApplicable identically.
See also
- Rule matching - action and resource pattern semantics
- Cross-policy combining - how NotApplicable is skipped in each mode
- Combining algorithms - what happens after the targets match
- Validation -
UNREACHABLE_TARGETand other codes