Advanced Authorization Policies
The Advanced Authorization Policy extends the basic policy with a powerful expression language for fine-grained authorization decisions. It enables security posture checks, claims inspection, and complex conditional logic.
Advanced authorization policies require the BSL-licensed @naylence/advanced-security package
for TypeScript or naylence-advanced-security for Python. Contact us for licensing details.
Overview
Advanced policies add these capabilities:
| Feature | Description |
|---|---|
| Frame type filtering | Gate rules by envelope frame type (Data, SecureOpen, etc.) |
| Expression conditions | Evaluate complex when expressions |
| Security posture checks | Verify signing and encryption status |
| Claims inspection | Access JWT/OAuth2 claims in expressions |
| Envelope metadata | Access envelope fields (id, to, frame type, etc.) |
| Node context | Access node information (id, path, etc.) |
Quick Start
version: '1'
type: AdvancedAuthorizationPolicy
rules:
# Require signed and encrypted Data frames
- id: secure-data-delivery
frame_type: Data
action: DeliverLocal
effect: allow
when: is_signed() && is_encrypted()
# Check claims and encryption level
- id: admin-api-access
address: admin.**
effect: allow
when: |
has_scope("admin") &&
is_encrypted_at_least("sealed") &&
claims.role == "administrator"Frame Type Filtering
Advanced policies can gate rules by the envelope’s frame type:
| Frame Type | Description |
|---|---|
Data | Application data messages |
DeliveryAck | Delivery acknowledgments |
NodeAttach | Node attachment request |
NodeHello | Node hello handshake |
NodeWelcome | Node welcome response |
NodeAttachAck | Node attachment acknowledgment |
AddressBind | Address binding request |
AddressUnbind | Address unbinding request |
CapabilityAdvertise | Capability advertisement |
CapabilityWithdraw | Capability withdrawal |
NodeHeartbeat | Node heartbeat |
NodeHeartbeatAck | Heartbeat acknowledgment |
CreditUpdate | Flow control credit update |
KeyAnnounce | Public key announcement |
KeyRequest | Public key request |
SecureOpen | Secure channel open request |
SecureAccept | Secure channel accept |
SecureClose | Secure channel close |
Examples
# Only allow Data frames to this address
- id: data-only
frame_type: Data
address: api.**
effect: allow
# Allow key exchange frames
- id: key-exchange
frame_type:
- KeyRequest
- KeyAnnounce
effect: allow
when: is_signed()
# Block all non-Data frames to sensitive endpoints
- id: block-control-frames
frame_type:
- NodeAttach
- AddressBind
address: sensitive.**
effect: denyThe when Expression Language
The when clause accepts expressions that evaluate to a boolean. The expression language is:
- Deterministic: Same inputs always produce same outputs
- Side-effect free: No mutations or I/O operations
- Null-safe: Missing values evaluate to
null, not errors
Syntax Overview
// Literals
true, false, null
42, 3.14, -1
"hello", 'world'
[1, 2, 3]
// Operators
a && b, a || b, !a
a == b, a != b
a < b, a <= b, a > b, a >= b
a + b, a - b, a * b, a / b, a % b
x in [1, 2, 3], x not in [1, 2, 3]
a ? b : c // ternary
// Member access
claims.sub
envelope.frame.type
node.id
// Index access
array[0]
claims["custom-claim"]
// Function calls
has_scope("admin")
starts_with(envelope.to, "api.")Operator Precedence
From lowest to highest:
- Ternary:
? : - Logical OR:
|| - Logical AND:
&& - Membership:
in,not in - Equality:
==,!= - Comparison:
<,<=,>,>= - Additive:
+,- - Multiplicative:
*,/,% - Unary:
!,- - Postfix:
.,[],()
Available Bindings
Expressions have access to these context bindings:
claims — JWT/OAuth2 Claims
Access token claims from the authorization context:
when: claims.sub == "user-123"
when: claims.aud == "/" + node.id
when: claims.role == "admin"
when: claims.exp > time.now_ms / 1000Common claims:
claims.sub— Subject (user/client ID)claims.aud— Audienceclaims.iss— Issuerclaims.exp— Expiration timestampclaims.iat— Issued at timestampclaims.scope— Space-separated scopes (string)claims.roles— Role array (Azure AD)
envelope — Envelope Metadata
Access envelope fields (excluding sensitive security values):
when: envelope.id != null
when: envelope.to == "math@fame.fabric"
when: envelope.frame.type == "Data"Available fields:
envelope.id— Envelope identifierenvelope.sid— Session identifierenvelope.traceId— Trace identifierenvelope.corrId— Correlation identifierenvelope.flowId— Flow identifierenvelope.to— Destination addressenvelope.frame.type— Frame type stringenvelope.sec.sig.present— Whether signature is presentenvelope.sec.sig.kid— Signature key IDenvelope.sec.enc.present— Whether encryption is presentenvelope.sec.enc.alg— Encryption algorithmenvelope.sec.enc.kid— Encryption key IDenvelope.sec.enc.level— Encryption level
For security, raw signature and encryption values (sig.val, enc.val) are NOT exposed
to expressions. Use the security builtin functions instead.
delivery — Delivery Context
Access delivery routing information:
when: delivery.origin_type == "downstream"
when: delivery.routing_action == "DeliverLocal"Available fields:
delivery.origin_type— Message origin (downstream,upstream,peer,local)delivery.routing_action— Current routing action
node — Node Context
Access information about the evaluating node:
when: node.id == "sentinel-1"
when: node.hasParent == true
when: starts_with(node.physicalPath, "/region-us/")Available fields:
node.id— Node identifiernode.sid— Node session IDnode.provisionalId— Provisional ID (before confirmed)node.physicalPath— Node’s physical path in fabricnode.hasParent— Whether node has a parentnode.publicUrl— Node’s public URL (if any)
time — Time Context
Access current time for temporal conditions:
when: claims.exp > time.now_ms / 1000
when: claims.nbf <= time.now_ms / 1000Available fields:
time.now_ms— Current time in milliseconds since epochtime.now_iso— Current time as ISO 8601 string
Built-in Functions
Scope Functions
Check granted OAuth2/OIDC scopes:
| Function | Description |
|---|---|
has_scope(scope) | Returns true if scope is granted |
has_any_scope(scopes) | Returns true if any scope is granted |
has_all_scopes(scopes) | Returns true if all scopes are granted |
# Single scope check
when: has_scope("api.read")
# Any of multiple scopes
when: has_any_scope(["admin", "superuser", "operator"])
# All scopes required
when: has_all_scopes(["api.read", "api.write"])Security Posture Functions
Check envelope security status:
| Function | Description |
|---|---|
is_signed() | Returns true if envelope has a valid signature |
is_encrypted() | Returns true if envelope is encrypted (any level) |
encryption_level() | Returns encryption level: "plaintext", "channel", "sealed", "unknown" |
is_encrypted_at_least(level) | Returns true if encryption meets or exceeds level |
# Require signature
when: is_signed()
# Require any encryption
when: is_encrypted()
# Require minimum encryption level
when: is_encrypted_at_least("channel")
# Require sealed encryption
when: is_encrypted_at_least("sealed")
# Check specific level
when: encryption_level() == "sealed"Encryption Level Ordering
plaintext < channel < sealed- plaintext: No encryption
- channel: Session-based encryption (ChaCha20-Poly1305-channel)
- sealed: Per-message encryption (ECDH-ES+A256GCM)
is_encrypted_at_least("plaintext") always returns true.
is_encrypted_at_least("sealed") only returns true for sealed encryption.
String Functions
| Function | Description |
|---|---|
lower(s) | Convert to lowercase |
upper(s) | Convert to uppercase |
starts_with(s, prefix) | Check if string starts with prefix |
ends_with(s, suffix) | Check if string ends with suffix |
contains(s, substring) | Check if string contains substring |
split(s, separator) | Split string into array |
trim(s) | Remove leading/trailing whitespace |
len(s) | Get string length |
# Check address prefix
when: starts_with(envelope.to, "api.")
# Case-insensitive comparison
when: lower(claims.role) == "admin"
# Check for substring
when: contains(node.physicalPath, "/secure/")Pattern Functions
| Function | Description |
|---|---|
glob_match(value, pattern) | Match using glob pattern |
regex_match(value, pattern) | Match using regex pattern |
# Glob matching
when: glob_match(envelope.to, "api.**.users")
# Regex matching
when: regex_match(claims.sub, "^user-[0-9]+$")Regex patterns are validated for safety. Patterns with excessive backtracking potential (e.g., nested quantifiers) are rejected.
Utility Functions
| Function | Description |
|---|---|
exists(x) | Returns true if value is not null |
coalesce(a, b) | Returns a if not null, otherwise b |
secure_hash(s, len) | Generate deterministic hash of string |
# Check if optional field exists
when: exists(claims.org_id)
# Default value
when: coalesce(claims.tier, "free") == "premium"
# Generate secure identifier
when: secure_hash(claims.sub, 8) == "A1b2C3d4"Null Handling
The expression language uses null-safe semantics:
- Missing bindings evaluate to
null - Missing properties evaluate to
null - Predicate functions return
falsewhen passednull - Operators on
nullfollow SQL-like semantics
# These are safe even if claims.custom is missing
when: claims.custom == null # true if missing
when: exists(claims.custom) # false if missing
when: coalesce(claims.custom, "default") == "default"
# Predicates return false for null
when: starts_with(claims.custom, "prefix") # false if null
when: has_scope(claims.scope) # false if nullExpression Limits
To prevent resource exhaustion, expressions have configurable limits:
| Limit | Default | Description |
|---|---|---|
maxExpressionLength | 4096 | Maximum expression string length |
maxAstDepth | 32 | Maximum nesting depth |
maxAstNodes | 256 | Maximum AST nodes |
maxRegexPatternLength | 256 | Maximum regex pattern length |
maxGlobPatternLength | 256 | Maximum glob pattern length |
maxStringLength | 1024 | Maximum string literal length |
maxArrayLength | 64 | Maximum array literal length |
maxFunctionArgs | 16 | Maximum function arguments |
maxMemberAccessDepth | 16 | Maximum member access chain depth |
Complete Examples
Secure Data Transfer
version: '1'
type: AdvancedAuthorizationPolicy
default_effect: deny
rules:
# Allow connections from authenticated clients
- id: allow-connect
action: Connect
effect: allow
# Require encrypted and signed Data frames
- id: secure-data
frame_type: Data
effect: allow
when: |
is_signed() &&
is_encrypted_at_least("channel") &&
claims.aud == "/" + node.id
# Allow key exchange for encryption setup
- id: key-exchange
frame_type:
- KeyRequest
- KeyAnnounce
effect: allow
when: is_signed()
# Allow secure channel establishment
- id: secure-channel-open
frame_type: SecureOpen
effect: allow
when: is_signed() && claims.aud == "/" + node.id
# Allow system messages
- id: system-messages
address: __sys__**
effect: allowMulti-Tenant with Claims Validation
version: '1'
type: AdvancedAuthorizationPolicy
default_effect: deny
rules:
- id: allow-connect
action: Connect
effect: allow
# Tenant isolation based on claims
- id: tenant-access
address: tenants.**
effect: allow
when: |
exists(claims.tenant_id) &&
starts_with(envelope.to, "tenants." + claims.tenant_id + ".")
# Admin access to all tenants
- id: admin-access
address: tenants.**
effect: allow
when: |
has_scope("admin") &&
is_encrypted_at_least("sealed")
# Shared services with tier check
- id: premium-shared-services
address: shared.premium.**
effect: allow
when: coalesce(claims.tier, "free") == "premium"Time-Based Access Control
version: '1'
type: AdvancedAuthorizationPolicy
default_effect: deny
rules:
- id: allow-connect
action: Connect
effect: allow
# Ensure token is not expired
- id: valid-token
effect: allow
when: |
exists(claims.exp) &&
claims.exp > time.now_ms / 1000
# Ensure token is not used before nbf
- id: not-before-check
effect: deny
when: |
exists(claims.nbf) &&
claims.nbf > time.now_ms / 1000Role-Based Access Control
version: '1'
type: AdvancedAuthorizationPolicy
default_effect: deny
rules:
- id: allow-connect
action: Connect
effect: allow
# Read-only access for viewers
- id: viewer-read
address: api.**.read
effect: allow
when: claims.role in ["viewer", "editor", "admin"]
# Write access for editors
- id: editor-write
address: api.**.write
effect: allow
when: claims.role in ["editor", "admin"]
# Admin-only endpoints
- id: admin-only
address: api.admin.**
effect: allow
when: |
claims.role == "admin" &&
is_encrypted_at_least("sealed")Configuration
Environment Variables
The simplest way to enable advanced authorization policies is by setting the authorization profile:
# Enable policy-based authorization from local file
FAME_AUTHORIZATION_PROFILE=policy-localfileBy default, policy-localfile looks for auth-policy.yml in the current directory. You can specify a custom policy file location:
# Set custom policy file path
FAME_AUTH_POLICY_PATH=config/auth-policy.yamlComplete example with advanced security:
# Advanced security profile
FAME_SECURITY_PROFILE=strict-overlay
FAME_ADMISSION_PROFILE=welcome
FAME_DEFAULT_ENCRYPTION_LEVEL=channel
# Authorization policy
FAME_AUTHORIZATION_PROFILE=policy-localfile
FAME_AUTH_POLICY_PATH=config/auth-policy.yaml
# Welcome service and CA
FAME_ADMISSION_SERVICE_URL=https://welcome/fame/v1/welcome/hello
FAME_CA_SERVICE_URL=https://ca/fame/v1/ca
FAME_CA_CERTS=/etc/fame/certs/root-ca.crt
# OAuth2 settings
FAME_ADMISSION_TOKEN_URL=https://oauth2-server/oauth/token
FAME_ADMISSION_CLIENT_ID=your-client-id
FAME_ADMISSION_CLIENT_SECRET=your-client-secret
FAME_JWT_AUDIENCE=fame.fabricSupport for other policy sources (remote URLs, key-value stores) will be added in future releases.
Programmatic Usage
TypeScript
import {
AdvancedAuthorizationPolicy,
loadPolicyFromFile
} from '@naylence/advanced-security';
// Load from file
const policy = await loadPolicyFromFile('./auth-policy.yaml');
// Or construct programmatically
const policy = new AdvancedAuthorizationPolicy({
policyDefinition: {
version: '1',
type: 'AdvancedAuthorizationPolicy',
default_effect: 'deny',
rules: [
{
id: 'require-encryption',
frame_type: 'Data',
effect: 'allow',
when: 'is_encrypted()',
},
],
},
});Python
from naylence.advanced_security import (
AdvancedAuthorizationPolicy,
load_policy_from_file
)
# Load from file
policy = load_policy_from_file('./auth-policy.yaml')
# Or construct programmatically
policy = AdvancedAuthorizationPolicy(
policy_definition={
'version': '1',
'type': 'AdvancedAuthorizationPolicy',
'default_effect': 'deny',
'rules': [
{
'id': 'require-encryption',
'frame_type': 'Data',
'effect': 'allow',
'when': 'is_encrypted()',
},
],
}
)Error Handling
Parse Errors
If a when expression has syntax errors, the rule is skipped during evaluation:
# This rule will never match due to syntax error
- id: broken-rule
effect: allow
when: claims.sub == # Missing right operandParse errors are logged during policy compilation with details about the error location.
Evaluation Errors
Errors during expression evaluation (e.g., type errors) cause the rule to not match:
# Will not match if claims.count is not a number
- id: numeric-check
effect: allow
when: claims.count > 10The evaluation trace includes error details:
{
"ruleId": "numeric-check",
"result": false,
"expression": "when: evaluation error - cannot compare string > number"
}Best Practices
1. Validate Security Posture First
Put security checks at the beginning of complex expressions:
when: |
is_signed() &&
is_encrypted_at_least("channel") &&
claims.sub == expected_subject2. Use Parentheses for Clarity
# Clear precedence
when: (claims.role == "admin") || (has_scope("write") && is_encrypted())
# Avoid ambiguity
when: a && b || c # This is a && (b || c) - confusing!3. Handle Missing Claims Gracefully
The expression language is null-safe — missing properties return null and comparisons with null safely return false:
# No need for exists() — null-safe by design
when: claims.custom_field == "expected"
# Use coalesce for defaults when you want fallback values
when: coalesce(claims.tier, "free") == "premium"Null Safety: claims.missing == "value" returns false (not an error) when claims.missing is undefined.
This applies to all comparison and predicate functions (contains(), starts_with(), etc.).
4. Keep Expressions Simple
Break complex conditions into multiple rules:
# Instead of one complex rule
# - id: complex-rule
# when: (A && B) || (C && D) || (E && F)
# Use multiple focused rules
- id: condition-ab
effect: allow
when: A && B
- id: condition-cd
effect: allow
when: C && D
- id: condition-ef
effect: allow
when: E && FHTTP Policy Source
The HTTP Policy Source enables loading authorization policies from remote HTTP/HTTPS endpoints. This is ideal for centralized policy management, dynamic updates, and multi-environment deployments.
Features
| Feature | Description |
|---|---|
| Remote Loading | Fetch policies from any HTTP/HTTPS endpoint |
| Caching | In-memory cache with configurable TTL |
| ETag Support | Efficient conditional requests using ETags |
| Authentication | Bearer token support via token providers |
| Format Detection | Auto-detects YAML or JSON based on Content-Type or URL |
| Profiles | Environment-based configuration via policy-http profile |
Configuration (Environment Variables)
When using the policy-http authorization profile, configure via environment variables:
| Variable | Description | Default |
|---|---|---|
FAME_AUTH_POLICY_URL | URL to fetch the policy from | (required) |
FAME_AUTH_POLICY_TIMEOUT_MS | HTTP request timeout in milliseconds | 30000 |
FAME_AUTH_POLICY_CACHE_TTL_MS | Cache TTL in milliseconds (0 = no cache) | 300000 |
FAME_AUTH_POLICY_TOKEN_URL | OAuth2 token endpoint URL | (required) |
FAME_AUTH_POLICY_CLIENT_ID | OAuth2 client ID | (required) |
FAME_AUTH_POLICY_CLIENT_SECRET | OAuth2 client secret | (required) |
FAME_AUTH_POLICY_AUDIENCE | OAuth2 audience parameter | — |
The policy-http profile uses OAuth2 Client Credentials flow for authentication.
Ensure your OAuth2 server is configured to issue tokens with the appropriate scopes (e.g., policy.read).
Configuration Examples
Set the following environment variables before starting your application:
Shell
export FAME_AUTHORIZATION_PROFILE=policy-http
export FAME_AUTH_POLICY_URL=https://config.example.com/policies/auth-policy.yaml
export FAME_AUTH_POLICY_TIMEOUT_MS=5000
export FAME_AUTH_POLICY_CACHE_TTL_MS=60000
export FAME_AUTH_POLICY_TOKEN_URL=https://auth.example.com/oauth/token
export FAME_AUTH_POLICY_CLIENT_ID=your-client-id
export FAME_AUTH_POLICY_CLIENT_SECRET=your-client-secret
export FAME_AUTH_POLICY_AUDIENCE=https://config.example.comProgrammatic Usage
TypeScript
import { HttpAuthorizationPolicySource } from '@naylence/advanced-security';
import { OAuth2ClientCredentialsTokenProvider } from '@naylence/runtime';
import { EnvCredentialProvider } from '@naylence/runtime';
// Create OAuth2 token provider with client credentials
const tokenProvider = new OAuth2ClientCredentialsTokenProvider({
tokenUrl: 'https://auth.example.com/oauth/token',
clientIdProvider: new EnvCredentialProvider({ envVar: 'OAUTH_CLIENT_ID' }),
clientSecretProvider: new EnvCredentialProvider({ envVar: 'OAUTH_CLIENT_SECRET' }),
scopes: ['policy.read'],
audience: 'https://config.example.com',
});
// Create HTTP policy source with OAuth2 authentication
const policySource = new HttpAuthorizationPolicySource({
url: 'https://config.example.com/policies/auth-policy.yaml',
timeoutMs: 5000,
cacheTtlMs: 60000,
tokenProvider,
});
// Load the policy
const policy = await policySource.loadPolicy();
// Force reload (bypasses cache)
const freshPolicy = await policySource.reloadPolicy();
// Get metadata about the last fetch
const metadata = policySource.getMetadata();
console.log('ETag:', metadata?.etag);
console.log('Last fetched:', metadata?.fetchedAt);HTTP Caching and ETags
The HTTP Policy Source supports efficient caching:
- In-Memory Cache: Policies are cached for
cacheTtlMsmilliseconds - ETag Support: Uses HTTP ETags for conditional requests
- 304 Not Modified: When the server returns 304, the cached policy is reused
# Initial response
HTTP/1.1 200 OK
Content-Type: application/yaml
ETag: "abc123"
# Subsequent request includes:
# If-None-Match: "abc123"
# If unchanged, server returns:
HTTP/1.1 304 Not ModifiedContent-Type Detection
The policy source automatically detects the format:
| Content-Type | Format |
|---|---|
application/yaml, text/yaml | YAML |
application/json, text/json | JSON |
text/plain, application/octet-stream | Inferred from URL extension |
For URLs without clear content types, the source examines the URL path:
.yaml,.yml→ YAML.json→ JSON- Default → YAML
Migration from Basic Policies
To upgrade from basic to advanced policies:
- Add
type: AdvancedAuthorizationPolicyto your policy file - Import from
@naylence/advanced-securityinstead of@naylence/runtime - Add
whenexpressions andframe_typefilters as needed
# Before (basic)
- id: require-scope
address: api.**
scope: api.access
effect: allow
# After (advanced)
- id: require-scope-encrypted
address: api.**
scope: api.access
effect: allow
when: is_encrypted()Next Steps
- Authorization Policies — Basic policy syntax and examples
- Advanced Security — Full advanced security setup
- Overlay Security — Envelope signing basics
Complete runnable examples are available in both naylence-examples-ts and naylence-examples-python
under the examples/security/advanced/ directory.