VPP-3: Policy is the standard way to model authorization in VDL.
A policy is a normal VDL type annotated with @policy. The type declares the inputs. The annotation declares two boolean expressions: allow and deny. A compatible plugin checks them before generating code and emits a pure boolean function.
@policy({
allow "principal.role == 'Admin'"
})
type CanCreateUser {
principal User
}
VPP-3 covers Role-Based Access Control (RBAC), Attribute-Based Access Control (ABAC), and mixes of both. It is a starting base, not a full authorization platform. Complex graph traversal, database lookups, and external queries stay in application code.
Why It Exists
Authorization spreads across handlers, jobs, UI flows, and framework hooks. VPP-3 keeps the common decision in one typed place, verified once, generated per language.
- Static safety: bad fields, bad enums, bad types, and unsafe optional access fail before code is emitted.
- Runtime safety: bad runtime shapes fail closed to
false. - Simple integration: the output is a function, not a runtime to host.
Core Concepts
A policy has up to three inputs.
- Principal: who acts. User, service account, worker, API client. Always required.
- Resource: what is accessed. Optional. Omit it when not needed.
- Context: transient decision data. MFA status, tenant, time, session. Optional.
The action is the policy type name itself. There is no action variable inside expressions.
@policy({ allow "principal.id == resource.ownerId" })
type CanReadInvoice {
principal User
resource Invoice
}
The @policy Annotation
@policy goes on a type declaration. It takes one object with:
allow(string, required): non-empty boolean expression.deny(string, optional): non-empty boolean expression.version(int, optional): language version. Defaults to1. Unknown versions must be rejected.
Use allow "true" to allow everything explicitly. Use deny "true" to deny everything explicitly. Empty or whitespace-only strings are invalid.
@policy({
version 1
allow "principal.role == 'Admin' && context.mfaVerified == true"
deny "principal.disabled == true"
})
type CanRunMaintenance {
principal User
context SessionContext
}
Policy Type Shape
Only these fields are part of VPP-3 v1:
principal: required.resource: optional.context: optional.
Rules:
- All three must be required when present. Never
principal?. - All three must reference named VDL object types. No scalars, enums, arrays, maps, or inline objects. Wrap scalar input in a small type.
- Declare them in
principal,resource,contextorder. Generators emit parameters in that order, omitting missing roots.
Policy Naming
Name policies like boolean questions, normally starting with Can:
CanCreateUser
CanApproveInvoice
CanAccessAdminPanel
Generators derive function names from the type name with target casing (canCreateUser, CanCreateUser, can_create_user). The Can prefix is convention, not validity. Never reject a policy only for missing it. Report collisions instead of overwriting.
Evaluation Semantics
The generated function returns true for allow, false for deny.
- Validate runtime shapes. Mismatch returns
false. - If
denyexists and istrue, returnfalse. - Return
allow. - Default is
false.
Deny wins. No rule means deny. && and || short-circuit left to right.
Policy Language
VPP-3 expressions use a small language called the Policy Language. It lives inside allow and deny strings. It has no statements, no assignments, and no variables in v1.
Keywords
true false in has principal resource context
Everything else is an identifier, literal, operator, or punctuation. Future versions only add keywords additively. Unknown syntax must be rejected.
Lexical Rules
- Whitespace outside literals is ignored, including newlines.
- Identifiers:
[A-Za-z_][A-Za-z0-9_]*. - Strings: single quotes only, such as
'Finance'. Support only\'and\\escapes. - Numbers:
100,10.5. No hex, no exponents in v1. - Booleans:
true,false. - Arrays: comma-separated, such as
['admin', 'owner']. Unlike VDL data literals, which separate items with whitespace, Policy Language arrays require commas because items are full expressions and whitespace is insignificant.
Syntax
expr := orExpr ;
orExpr := andExpr ( "||" andExpr )* ;
andExpr := cmpExpr ( "&&" cmpExpr )* ;
cmpExpr := unary ( ( "==" | "!=" | ">" | ">=" | "<" | "<=" | "in" ) unary )? ;
unary := "!" unary | primary ;
primary := hasCall | paren | ref | lit | array ;
hasCall := "has" "(" ref ")" ;
paren := "(" expr ")" ;
ref := root ( "." ident )+ ;
root := "principal" | "resource" | "context" ;
lit := string | number | "true" | "false" ;
array := "[" [ expr ( "," expr )* ] "]" ;
Precedence, highest first: !, then == != > >= < <= in, then &&, then ||. Parentheses override.
Roots and Paths
An expression may use only roots declared by its policy type. resource and context are unavailable when omitted.
principal.id
resource.ownerId
context.mfaVerified
Paths walk named object fields with dot notation. No array indexing, no map indexing, no method calls, no dynamic access, no database lookups.
Types
No implicit conversion. 1 == '1' is invalid. int versus float is invalid in v1. Every root expression must evaluate to bool.
== !=:string,int,float,bool,datetime,enum.> >= < <=:int,float,datetime,string. Strings use UTF-8 byte order.in: scalar left, array right with compatible element type. Empty array isfalse.!:boolonly.- Objects and arrays are never directly comparable except through
in.
Arrays and Objects
Arrays hold scalars only: string, number, boolean, enum value, or reference to one of those. All items must share one element type. Nested arrays and objects inside arrays are invalid in v1.
principal.role in ['admin', 'owner']
principal.id in resource.allowedIds
There are no object literals in v1. Compare objects field by field instead of comparing them directly:
principal.tenantId == resource.tenantId
VDL uses whitespace in data arrays such as ["admin" "editor"]. The Policy Language uses commas because array items are expressions, not plain values. Generators always emit commas for target languages.
Enum Values
Compare enums by value with a string literal:
enum InvoiceStatus {
Locked
Unlocked
}
resource.status == 'Locked'
The literal must match the member value, or the member name when no explicit value exists. resource.status == 'Archived' is invalid without that member. Generators may emit native enum comparisons.
Datetime
Compare datetime fields against datetime fields only. There are no datetime literals in v1. Pass time through context, such as context.now:
resource.expiresAt > context.now
No now() calls, no duration arithmetic. Compute windows before calling the function.
Optional Fields and has
has(ref) is true when the full chain is present. has(principal.profile.department) means profile is present and department is present. Generators expand the chain per target (!= nil, !== undefined, is_some()).
One has guards the whole path. Never require has(p) && has(p.x) && has(p.x.y) by hand.
Equality is null-safe to keep the common case short:
- Missing
== valueisfalse. - Missing
!= valueistrue.
The generator still emits the presence check. You just do not write it.
Ordering and membership are strict and require an explicit guard:
has(principal.department) && principal.department == 'Finance'
has(resource.score) && resource.score > 100
has(principal.id) && principal.id in resource.allowedIds
Guard scope follows short-circuit flow. A has on the left of && guards the right side. Each || branch must be safe alone. !has(p.x) does not guard p.x.
Presence-only checks need no other condition:
has(principal.department)
Unsupported in v1
Reject arithmetic, regex, string interpolation, indexing, method calls, async, imports, custom functions, and any function other than has(...). This keeps every target emitter to a direct mapping with explicit parentheses.
Static Validation
Validate before emitting. Reject the policy when any check fails:
@policyis not on atype.principalis missing or optional.resourceorcontextis optional when present.- Any root is not a named object type.
- Extra fields beyond
principal,resource,context. allowmissing, empty, or not a string.denypresent but empty or not a string.- Unknown
version. - Unknown root, field, or enum value.
- Incompatible comparison or
inelement mismatch. - Non-
boolroot result. - Unsafe optional access.
- Unsupported syntax.
Stop generation and point diagnostics at the annotation, expression, and subexpression when possible.
Runtime Behavior
Static checks do not make runtime data trusted. Validate shapes at runtime when data crosses a dynamic boundary such as HTTP, queues, or plugin input. Mismatch returns false, or a checked wrapper reports the error while keeping deny.
Generated Code
Emit one pure boolean function per policy. Deterministic, local, no network, no database, no config files, no expression parsing at runtime.
function canReadInvoice(principal: User, resource: Invoice): boolean
Use VPP-0 models when available. Helpers, test fixtures, and explanation utilities are allowed when they preserve the canonical decision.
Complete Example
type User {
id string
name string
role string
department? string
disabled bool
}
enum InvoiceStatus {
Locked
Unlocked
}
type Invoice {
ownerId string
status InvoiceStatus
}
type SessionContext {
mfaVerified bool
now datetime
}
@policy({
version 1
allow "principal.id == resource.ownerId || has(principal.department) && principal.department == 'Finance' && context.mfaVerified == true"
deny "principal.disabled == true || resource.status == 'Locked'"
})
type CanApproveInvoice {
principal User
resource Invoice
context SessionContext
}
Finance users with MFA pass. Owners pass. Disabled users and locked invoices fail because deny runs first.
Integration
VPP-3 is transport agnostic. RPC handlers, event consumers, CLI actions, jobs, and UI actions can call the same function. Other protocols may use VPP-3 functions, but VPP-3 never depends on them. Preserve VPP-1 deprecation metadata where the target supports it.
Non-Goals
VPP-3 does not define authentication, identity providers, sessions, database loading, dynamic stores, admin UIs, audit formats, transport, role storage, graph traversal, or business workflows.
Compatibility Statement
A plugin is VPP-3 compatible only when it preserves the annotation model, type shape, naming rules, Policy Language, validation, fail-closed behavior, boolean API, and deny-first semantics defined here.