Principal = {
ID: "anonymous",
Attributes: {}
}Objects, Templating & Co
The dynamic nature of mechanisms is given by the ability to work on different objects and by using templates and expressions to implement various use cases.
Mechanisms in heimdall are dynamic: they can inspect request data, act on authenticated subject information, and produce output that subsequent pipeline steps can use. This page describes the objects available during rule execution, and the templating and expression capabilities that operate on them.
Objects
Objects represent state during the execution of a particular rule. These are entities either created or used by specific mechanisms, and can represent things like the actual request, the authenticated subject of the request, and many others. Which objects are available in a particular mechanism is documented at each mechanism’s configuration reference.
Principal
A Principal represents an identity of an entity associated with the current request — such as a user, service, or machine — and verified by a particular authenticator. Principals are immutable and ephemeral — they exist only for the lifetime of a single request.
Each principal has the following properties:
ID: stringThe unique identifier of the principal.
Attributes: mapA key-value map containing all known attributes about the principal.
Each object of this type can be represented as a JSON object. Here are some examples:
Principal = {
ID: "foobar",
Attributes: {
"sub": "foobar",
"exp": "1670600805",
"jti": "7b91ed8a-0251-4e02-8d51-9792785851e8",
"iat": "1670600305",
"iss": "https://testauthserver.local",
"nbf": "1670600305",
"extra": {
"foo": ["bar", "baz"]
}
}
}Subject
A Subject is a higher-order concept — a key-value collection of Principals verified by all successfully executed authenticators, where the keys represent principal names and the values are the corresponding Principal objects.
Like Principals, Subjects are immutable and ephemeral, existing only for the lifetime of a single request.
In addition to its Principal collection, each Subject exposes the following virtual properties:
ID: stringThe unique identifier of the Subject. Its value is taken from the Principal named "default".
Attributes: mapA key-value map containing all attributes of the Principal named "default".
Subject.ID and Subject.Attributes are syntactic sugar for Subject.default.ID and Subject.default.Attributes respectively. |
Each object of this type can be thought of as a JSON object. Here is an example:
Subject = {
"default": Principal {
ID: "62302d83-da01-4ba6-9440-119728757455",
Attributes: {
"email": "john.doe@example.com"
}
},
"device": Principal {
ID: "597ef6b5-64fc-49e3-ad1e-7599046908d9",
Attributes: {
"os": "Ubuntu 25.04"
}
}
}Request
This object contains information about the request handled by heimdall and has the following attributes and methods:
Method: stringThe HTTP method used, like
GET,POST, etc.
URL: URLThe URL of the matched request. This object has the following properties and methods:
Captures: mapAllows accessing of the values captured by the named wildcards used in the matching path expression of the rule.
Host: stringThe host part of the url.
Hostname(): methodThis method returns the host name stripping any valid port number if present.
Port(): methodReturns the port part of the
Host, without the leading colon. IfHostdoesn’t contain a valid numeric port, returns an empty string.Path: stringThe path part of the url.
Query(): methodThe parsed query with each key-value pair being a string to array of strings mapping.
RawQuery: stringThe raw query part of the url.
Scheme: stringThe HTTP scheme part of the url.
String(): methodThis method returns the URL as valid URL string of a form
scheme:host/path?query.
ClientIPAddresses: string arrayThe list of IP addresses the request passed through with the first entry being the ultimate client of the request. Only available if heimdall is configured to trust the client, sending this information, e.g. in the
X-Forwarded-Fromheader (see also trusted_proxies configuration for more details).Header(name): methodThis method expects the name of a header as input and returns its value as a
string. If the header is not present in the HTTP request an empty string ("") is returned. If a header appears multiple times in the request, the returnedstringis a comma separated list of all values.A single header may be a comma-separated list of actual values as well. Best example is the Acceptheader, which might be set to e.g.text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8.Cookie(name): methodThis method expects the name of a cookie as input and returns the value of it as
string. If the cookie is not present in the HTTP request an empty string ("") is returned.Body(): methodThe parsed body with contents depending on the
Content-Typeheader. Supported content types are any MIME types withjsonoryamlsubtype, as well asapplication/x-www-form-urlencoded. If the MIME type is unsupported or noContent-Typeheader is present, the method returns the raw body as a string.The actual request body is parsed only on the first use of this function. All subsequent calls return the cached result. Example 4. Example resultsIf the
Content-Typeheader is set toapplication/jsonand the actual request body is a valid JSON object, shown below{ "context": "heimdall" }The call to the
Body()function will return exactly this representation as a map.If the
Content-Typeheader is set toapplication/yamland the actual request body is a valid YAML object, shown belowcontext: heimdallThe call to the
Body()function will return{ "context": "heimdall" }representation as a map.If the
Content-Typeheader is set toapplication/x-www-form-urlencodedand the actual request body is a valid object, shown belowcontext=heimdallThe call to the
Body()function will return this representation as a map with each value being a string array. In this particular case as{ "context": [ "heimdall" ] }.
Here is an example for a request object:
Request = {
Method: "GET",
Url: {
Scheme: "https",
Host: "localhost",
Path: "/test/abc",
RawQuery: "baz=zab&baz=bar&foo=bar",
Captures: { "value": "abc" }
},
ClientIP: ["127.0.0.1", "10.10.10.10"]
}Outputs
This object represents a pipeline execution-specific key-value map used by pipeline steps to store and share results. Each step stores its result under its own ID as the key.
Example:
Outputs = {
"id_1": ["a", "b"],
"id_2": { "foo": "bar", "baz": false }
}Payload
This object represents the contents of a payload, like the request body or a response body. The contents depend on the MIME-Type of the payload. For json, yaml or x-www-form-urlencoded encoded payload, the object is transformed to a JSON object. Otherwise, it is just a string.
Here are some examples:
The following JSON object is a typical response from OPA.
Payload = { "result": true }Payload = "SomeStringValue"Error
This object represents an error raised during the execution of a rule. It is available in if CEL expressions of Error Handlers and is not available in templates. The following properties are available:
Source: stringThe ID of the mechanism which raised the error.
StepID: stringThe ID of the pipeline step which raised the error.
Proper error handling requires attention to the actual error type available via type(Error).
Values
This object represents a key-value map, with both the key and value being of string type. The actual values can be templated (see Templating). The contents and the variables available in templates depend on the configuration of the particular mechanism, respectively the corresponding override in a rule.
Here is an example:
Values = {
"some-key-1": "value-1",
"some-key-2": "value-2"
}Templating
Some mechanisms support templating using Golang Text Templates. Templates can act on the objects described above — which exactly are available is documented at each mechanism’s configuration reference.
To ease the usage, all sprig functions, except env and expandenv, as well as the following functions are available:
urlenc- Encodes a given string using URL encoding. Useful when generating request bodies or query parameters for communication with upstream systems.atIndex- Implements Python-like access to arrays. Takes a single integer argument as the index. A positive index behaves like standard array access. A negative index accesses elements from the end of the array (-1is the last element,-2the second-last, etc.).Example:
{{ atIndex 2 [1,2,3,4,5] }}evaluates to3and{{ atIndex -2 [1,2,3,4,5] }}evaluates to4.splitList- Splits a given string using a separator. The result is a string array.Example:
{{ splitList "/" "/foo/bar" }}evaluates to the["", "foo", "bar"]array.secret- Reads a secret value from a configured secret source (see Secret Management).Example:
{{ secret "some_source" "my_api_key" }}reads the value at selector"my_api_key"from the"some_source"secret source.If this function is used in a rule, the referenced secret source must have allow_in_rules: trueset, otherwise the rule will fail to load.
Imagine we have a POST request for the URL https://foobar.baz/zab?foo=bar, with a header X-Foo set to bar, for which heimdall was able to identify a subject consisting of a single "default" principal with ID=foo and Attributes containing an entry email: foo@bar. The following template generates a JSON object from this information:
{
"subject_id": {{ quote .Subject.ID }},
"email": {{ quote .Subject.Attributes.email }},
"request_url": {{ quote .Request.URL }},
"foo_value": {{ index .Request.URL.Query.foo 0 | quote }}
"request_method": {{ quote .Request.Method }},
"x_foo_value": {{ .Request.Header "X-Foo" | quote }},
"whatever": {{ .Outputs.whatever | quote }}
}.Request.URL.Query.foo returns an array of strings, so the first element is accessed explicitly via index. |
This will result in the following JSON object:
{
"subject_id": "foo",
"email": "foo@bar.baz",
"request_url": "https://foobar.baz/zab?foo=bar",
"foo_value": "bar",
"request_method": "POST",
"x_foo_value": "bar",
"whatever": "some value"
}Imagine we have a POST request to the URL https://foobar.baz/zab/1234, with 1234 being the identifier of a file to be updated with the request body, and access control is handled via OpenFGA. This can be achieved with the following authorizer:
id: openfga_authorizer
type: remote
config:
endpoint:
url: https://openfga/stores/files/check
payload: |
{
"user": "user:{{ .Subject.ID }}",
"relation": "write",
"object": "file:{{ .Request.URL.Captures.id }}"
}
expressions:
- expression: |
Payload.allowed == trueWhen the payload template is rendered for the above request with Subject.ID=foo, the following JSON object is produced:
{
"user": "user:foo",
"relation": "write",
"object": "file:1234"
}You can find further examples as part of the mechanism descriptions that support templating.
Expressions
Expressions can be used to execute conditional logic. Currently, only CEL is supported as expression language. All standard CEL functions, as well as extension functions, are available. Which objects are available in a particular expression is documented at each mechanism’s configuration reference. The Error object is only available in Error Handler expressions.
In addition to the built-in CEL functions and extension methods, the following custom functions are also available:
at- Provides Python-like access to array elements. A positive index behaves like standard[]array access. A negative index accesses elements from the end of the array (-1is the last element,-2the second-last, etc.).Example:
[1,2,3,4,5].at(2)returns3and[1,2,3,4,5].at(-2)returns4.last- Returns the last element of an array, or nil if the array is empty.Example:
[1,2,3,4,5].last()returns5.regexFind- Returns the first (leftmost) match of a regular expression within a string. Returns an empty string if no match is found.Example:
"abcd1234".regexFind("[a-zA-Z][1-9]")returns"d1".regexFindAll- Returns an array of all matches of a regular expression within a string. Returns an empty array if no match is found.Example:
"123456789".regexFindAll("[2,4,6,8]")returns["2","4","6","8"].split- Splits a string by the given separator and returns an array of strings.Example:
"/foo/bar/baz".split("/")returns["", "foo", "bar", "baz"].networks- Accepts a single CIDR string or an array of CIDR strings, and returns a matcher object that can be used to check whether an IP address belongs to one of the specified ranges.Examples:
Check whether all client IPs are within the specified private ranges (e.g., to enforce that a request only arrived via internal networks):
Request.ClientIPAddresses.all(ip, ip in networks(["172.16.0.0/12", "192.168.0.0/16"]))Check whether at least one client IP is within a specific range (e.g., to detect that the request passed through a known proxy):
Request.ClientIPAddresses.exists(ip, ip in networks("10.0.0.0/8"))
Some examples:
Given the following Payload object
Payload = { "result": true }a CEL expression to check whether the result attribute is set to true would look as follows:
Payload.result == truehas(Subject.Attributes.groups) &&
Subject.Attributes.groups.exists(g, g == "admin")As with templates, Subject.ID and Subject.Attributes are syntactic sugar for Subject.default.ID and Subject.default.Attributes respectively.
Request.URL.Path.split("/").last()type(Error) == authentication_error && Error.Source == "foo"For available error types, see Error State Types.
Last updated on Jun 3, 2026