Skip to main content

ABAC Conditions

ABAC (Attribute-Based Access Control) conditions let you add fine-grained rules to any policy. A condition compares attributes from the subject, resource, or request context at evaluation time.

A policy with conditions only grants access when all active conditions pass. If any condition fails, the policy does not match, even if the subject, action, and resource match.

Conditions shape

All conditions are stored as a single flat JSON object on the conditions field of a policy. Each key in the object is an active guardrail or attribute check:

{
"conditions": {
"subject_attrs": [
{ "key": "department", "op": "eq", "value": "engineering" }
],
"resource_attrs": [
{ "key": "classification", "op": "eq", "value": "internal" }
],
"time_window": {
"start": "09:00",
"end": "18:00",
"days": ["mon", "tue", "wed", "thu", "fri"],
"tz": "UTC"
},
"ip_allowlist": {
"cidrs": ["10.0.0.0/8", "172.16.0.0/12"]
}
}
}

Any key you omit is not evaluated. You can mix attribute checks and guardrails in the same conditions object: all present keys must pass.

Attribute conditions

Attribute checks compare a stored or request-time attribute on the subject, resource, or request context.

subject_attrs

Checks attributes on the subject (user, service, agent).

{
"conditions": {
"subject_attrs": [
{ "key": "department", "op": "eq", "value": "engineering" },
{ "key": "clearance", "op": "gte", "value": 3 }
]
}
}

Each row: { "key": "<attribute>", "op": "<operator>", "value": <rhs> }. All rows are ANDed.

No nested traversal. key is matched against the attribute map directly, so "key": "manager.region" looks for an attribute literally named manager.region: it does not descend into a nested manager object. Store what you want to match as a flat key.

resource_attrs

Checks attributes on the resource being accessed.

{
"conditions": {
"resource_attrs": [
{ "key": "status", "op": "eq", "value": "published" },
{ "key": "region", "op": "in", "value": ["us-east-1", "us-west-2"] }
]
}
}

context_attrs

Checks keys passed in context at evaluation time.

{
"conditions": {
"context_attrs": [{ "key": "env", "op": "eq", "value": "prod" }]
}
}

All three check types use the same row shape.

Operators

OperatorMeaning
eqEqual
neNot equal
gteGreater than or equal
lteLess than or equal
gtGreater than
ltLess than
inValue is contained in the given array (value must be an array)
not_inValue is NOT in the given array (value must be an array)
matchesValue matches the given regular expression

These names are the only accepted forms. Symbolic aliases such as == or >= are rejected with a 400.

Type coercion: if the stored or request attribute is a number or boolean but the policy stores the value as a string (which the dashboard does), Vengtoo coerces automatically: {"op":"gte","value":"3"} against a stored 5, and {"op":"eq","value":"true"} against a stored true, both work as expected.

Regular expressions (matches)

matches takes a regular expression, not a glob. A common mistake is a glob-style pattern like *@example.com, which is an invalid regex (* has nothing to repeat), so a policy using it will always deny. Write .*@example.com instead.

The flavor is RE2 (Go's regexp engine). Two things to know:

  • RE2 does not support lookahead or backreferences (unlike PCRE), so patterns that rely on (?=...) or \1 will not compile.
  • Matching is unanchored (substring) by default. See the security warning below.
Anchor your patterns

Unanchored matching is the important one for security: .*@example\.com also matches evil@example.com.attacker.com, because that string contains @example.com. For a domain or email check, anchor the end so the value must actually end there:

{ "key": "email", "op": "matches", "value": ".*@example\\.com$" }

or pin the whole value:

{ "key": "email", "op": "matches", "value": "^[^@]+@example\\.com$" }

Escape literal dots as \., since a bare . matches any character. (In the dashboard you type a single backslash, for example .*@example\.com$; in raw JSON it is doubled, \\., because JSON escapes backslashes.) An invalid pattern fails closed and denies.

Comparing two attributes

value is a literal by default. To compare against another attribute instead, pass a reference object:

{ "key": "owner", "op": "eq", "value": { "$attr": "subject.id" } }

The path is subject.<attribute>, resource.<attribute> or context.<attribute>, and resolves against the current request regardless of which side key came from.

subject.id and resource.id always resolve to the IDs in the evaluation call, so you do not have to copy them into attributes.

This is how you express ownership: one policy covering every record, rather than one policy per record:

{
"name": "owners-can-read",
"effect": "ALLOW",
"resource_types": [{ "resource_type_id": "<document-type-id>", "actions": ["read"] }],
"conditions": {
"resource_attrs": [
{ "key": "owner", "op": "eq", "value": { "$attr": "subject.id" } }
]
}
}

Stamp each resource with attributes: { "owner": "<subject-uuid>" } at creation. Alice reading her own document is ALLOW; Alice reading Bob's is deny with CONDITION_FAILED.

References work in every check type and in expression trees. A single-key object that is not $attr is treated as a literal.

A missing attribute is not the same as a denial

If the attribute is absent, the check is false and the policy does not match. What that means depends on the effect:

  • On an ALLOW policy the grant does not apply, so the request is denied unless some other policy allows it.
  • On a DENY policy the block does not apply either. A resource missing the attribute your DENY tests for is not blocked by it.

So a DENY on {"key": "classification", "op": "eq", "value": "secret"} stops a resource tagged secret, and lets through a resource that was never tagged at all. Where the absence of a tag should itself be disqualifying, gate access with an ALLOW that requires the attribute rather than a DENY that tests for a bad value.

Pass the relevant values in subject.attributes / resource.attributes in your evaluation call.

Guardrails

Guardrails are additional gates configured via the dashboard's Active policy rules panel. They live in the same conditions object alongside attribute checks.

KeyControls
time_windowPolicy only matches within a configured time range
ip_allowlistPolicy only matches when context.ip is in an allowed CIDR
geo_restrictionPolicy only matches when context.geo is an allowed country code
mfa_requiredPolicy only matches when a specified claim is truthy on the subject or context
trust_levelPolicy only matches when the subject meets a minimum trust tier
rate_limitPolicy only matches when the subject hasn't exceeded a request rate
requires_human_approvalPolicy triggers an approval workflow before granting access

See the guardrails section of Policies for the shape of each key.

Examples

Department-scoped access

Allow users to read documents only when their department attribute matches:

{
"conditions": {
"subject_attrs": [
{ "key": "department", "op": "eq", "value": "engineering" }
],
"resource_attrs": [
{ "key": "department", "op": "eq", "value": "engineering" }
]
}
}

Both checks must pass.

Clearance-level gating

Only allow access when the subject's clearance level meets the threshold:

{
"conditions": {
"subject_attrs": [{ "key": "clearance_level", "op": "gte", "value": 3 }]
}
}

Multiple attribute checks (all must pass)

Allow admin access only for senior engineers in the platform team:

{
"conditions": {
"subject_attrs": [
{ "key": "level", "op": "gte", "value": 5 },
{ "key": "team", "op": "eq", "value": "platform" }
]
}
}

Owner-only access

Allow a subject to act only on resources stamped with their own ID:

{
"conditions": {
"resource_attrs": [
{ "key": "owner", "op": "eq", "value": { "$attr": "subject.id" } }
]
}
}

Attribute check + guardrail combined

Only allow reads by engineering subjects during business hours:

{
"conditions": {
"subject_attrs": [
{ "key": "department", "op": "eq", "value": "engineering" }
],
"time_window": {
"start": "09:00",
"end": "18:00",
"days": ["mon", "tue", "wed", "thu", "fri"],
"tz": "UTC"
}
}
}

In the evaluate request

Pass the relevant values in subject.attributes and resource.attributes. Conditions read these fields directly: use attributes, not properties, for conditions to match:

{
"subject": {
"external_id": "alice-uuid-from-your-db",
"type": "user",
"attributes": {
"department": "engineering",
"level": 5,
"team": "platform"
}
},
"resource": {
"external_id": "wiki-uuid-from-your-db",
"type": "document",
"attributes": {
"department": "engineering",
"classification": "internal"
}
},
"action": { "name": "read" }
}

Stored subject attributes (set via the Management API) are also available to conditions (see Context Trust Model for how request and stored values interact).

Where attributes come from

Resources: attribute definitions are declared on the resource type (e.g., classification: string, department: string). When you create a resource of that type, those fields appear as fillable attributes. Stored values are included in the policy bundle and available to resource_attrs conditions automatically.

Subjects: there is no owning type that enforces a schema. Subject attribute definitions are declared globally (under Schema → Subject Attributes in the dashboard), and each subject gets a freeform attributes section. Stored values are included in the bundle and available to subject_attrs conditions automatically.

Passing attributes inline: you can also pass subject.attributes and resource.attributes directly in the evaluation request. This is useful for attributes that aren't stored in Vengtoo (e.g., a session-computed risk score). Request-time values are merged with stored values, but stored values win on conflict.

Stored values win on conflict

A caller cannot override an attribute that is already set in the Vengtoo control plane. This is deliberate anti-spoofing: policies always evaluate against the authoritative stored value, never whatever the caller sends. See the Context Trust Model for the full precedence order.