Skip to Content
Naylence Docs are in active development. Share feedback in Discord.
SecurityAdvanced Authorization Policies

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:

FeatureDescription
Frame type filteringGate rules by envelope frame type (Data, SecureOpen, etc.)
Expression conditionsEvaluate complex when expressions
Security posture checksVerify signing and encryption status
Claims inspectionAccess JWT/OAuth2 claims in expressions
Envelope metadataAccess envelope fields (id, to, frame type, etc.)
Node contextAccess node information (id, path, etc.)

Quick Start

auth-policy.yaml
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 TypeDescription
DataApplication data messages
DeliveryAckDelivery acknowledgments
NodeAttachNode attachment request
NodeHelloNode hello handshake
NodeWelcomeNode welcome response
NodeAttachAckNode attachment acknowledgment
AddressBindAddress binding request
AddressUnbindAddress unbinding request
CapabilityAdvertiseCapability advertisement
CapabilityWithdrawCapability withdrawal
NodeHeartbeatNode heartbeat
NodeHeartbeatAckHeartbeat acknowledgment
CreditUpdateFlow control credit update
KeyAnnouncePublic key announcement
KeyRequestPublic key request
SecureOpenSecure channel open request
SecureAcceptSecure channel accept
SecureCloseSecure 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: deny

The 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:

  1. Ternary: ? :
  2. Logical OR: ||
  3. Logical AND: &&
  4. Membership: in, not in
  5. Equality: ==, !=
  6. Comparison: <, <=, >, >=
  7. Additive: +, -
  8. Multiplicative: *, /, %
  9. Unary: !, -
  10. 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 / 1000

Common claims:

  • claims.sub — Subject (user/client ID)
  • claims.aud — Audience
  • claims.iss — Issuer
  • claims.exp — Expiration timestamp
  • claims.iat — Issued at timestamp
  • claims.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 identifier
  • envelope.sid — Session identifier
  • envelope.traceId — Trace identifier
  • envelope.corrId — Correlation identifier
  • envelope.flowId — Flow identifier
  • envelope.to — Destination address
  • envelope.frame.type — Frame type string
  • envelope.sec.sig.present — Whether signature is present
  • envelope.sec.sig.kid — Signature key ID
  • envelope.sec.enc.present — Whether encryption is present
  • envelope.sec.enc.alg — Encryption algorithm
  • envelope.sec.enc.kid — Encryption key ID
  • envelope.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 identifier
  • node.sid — Node session ID
  • node.provisionalId — Provisional ID (before confirmed)
  • node.physicalPath — Node’s physical path in fabric
  • node.hasParent — Whether node has a parent
  • node.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 / 1000

Available fields:

  • time.now_ms — Current time in milliseconds since epoch
  • time.now_iso — Current time as ISO 8601 string

Built-in Functions

Scope Functions

Check granted OAuth2/OIDC scopes:

FunctionDescription
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:

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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

FunctionDescription
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:

  1. Missing bindings evaluate to null
  2. Missing properties evaluate to null
  3. Predicate functions return false when passed null
  4. Operators on null follow 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 null

Expression Limits

To prevent resource exhaustion, expressions have configurable limits:

LimitDefaultDescription
maxExpressionLength4096Maximum expression string length
maxAstDepth32Maximum nesting depth
maxAstNodes256Maximum AST nodes
maxRegexPatternLength256Maximum regex pattern length
maxGlobPatternLength256Maximum glob pattern length
maxStringLength1024Maximum string literal length
maxArrayLength64Maximum array literal length
maxFunctionArgs16Maximum function arguments
maxMemberAccessDepth16Maximum 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: allow

Multi-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 / 1000

Role-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-localfile

By 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.yaml

Complete example with advanced security:

.env
# 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.fabric

Support 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 operand

Parse 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 > 10

The 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_subject

2. 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 && F

HTTP 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

FeatureDescription
Remote LoadingFetch policies from any HTTP/HTTPS endpoint
CachingIn-memory cache with configurable TTL
ETag SupportEfficient conditional requests using ETags
AuthenticationBearer token support via token providers
Format DetectionAuto-detects YAML or JSON based on Content-Type or URL
ProfilesEnvironment-based configuration via policy-http profile

Configuration (Environment Variables)

When using the policy-http authorization profile, configure via environment variables:

VariableDescriptionDefault
FAME_AUTH_POLICY_URLURL to fetch the policy from(required)
FAME_AUTH_POLICY_TIMEOUT_MSHTTP request timeout in milliseconds30000
FAME_AUTH_POLICY_CACHE_TTL_MSCache TTL in milliseconds (0 = no cache)300000
FAME_AUTH_POLICY_TOKEN_URLOAuth2 token endpoint URL(required)
FAME_AUTH_POLICY_CLIENT_IDOAuth2 client ID(required)
FAME_AUTH_POLICY_CLIENT_SECRETOAuth2 client secret(required)
FAME_AUTH_POLICY_AUDIENCEOAuth2 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:

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.com

Programmatic Usage

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:

  1. In-Memory Cache: Policies are cached for cacheTtlMs milliseconds
  2. ETag Support: Uses HTTP ETags for conditional requests
  3. 304 Not Modified: When the server returns 304, the cached policy is reused
Server Response Headers
# 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 Modified

Content-Type Detection

The policy source automatically detects the format:

Content-TypeFormat
application/yaml, text/yamlYAML
application/json, text/jsonJSON
text/plain, application/octet-streamInferred 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:

  1. Add type: AdvancedAuthorizationPolicy to your policy file
  2. Import from @naylence/advanced-security instead of @naylence/runtime
  3. Add when expressions and frame_type filters 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

Complete runnable examples are available in both naylence-examples-ts and naylence-examples-python under the examples/security/advanced/ directory.

Last updated on