Route extensions

Route extensions are a way to register arbitrary HTTP handlers on Synapse to provide external authorization endpoints. Many systems such as gateways, proxies, query engines and queues have support for delegating access control to another system by calling into a specific endpoint with information about the access decision to be made. Route extensions enable using Cerbos policies to make those decisions by defining how to map the requests and responses to the format required by each component.

Synapse route extensions can be defined in two ways.

  • Declarative mapping

  • Programmatic mapping using pluggable implementations provided by Cerbos or developed by the user

Route definitions

Regardless of the mapping type used, the routes to which they should apply are defined as a map of paths to allowed HTTP methods.

  1. All route extensions are served under the /ext/ prefix of Synapse server. This is implicitly prepended to the paths defined in the routes section. Therfore, if you define the route as /foo/bar, it will be served from the Synapse server at /ext/foo/bar.

  2. Path patterns are global. Take care to avoid using the same or semantically equivalent paths for your route extensions. If multiple paths match a given request, the path that appeared first in the Synapse configuration under the routeExtensions section wins.

extensions:
  routeExtensions:
    foo:
      routes:
        "/team-a/foo": [] (1)
        "/team-b/{baz}": ["GET"] (2)
1 Activate the extension for requests sent via any HTTP method to /ext/team-a/foo. This is an exact match.
2 Activate the extension for GET requests to /ext/team-b/<ANY VALUE>. The curly braces around the second path component defines a wildcard that matches any value.

Route patterns

Synapse matches the paths in a routes map using the gorilla/mux pattern syntax. Literal paths are exact matches and do not include subpaths.

Pattern syntax

Pattern Matches Notes

/orders/status

The literal path only

Exact match.

{name}

A single path segment

Default regex is [^/]+. /orders/{version} matches /ext/orders/v1 but not /ext/orders/v1/billing.

\{name:pattern}

A segment matching an inline regex

Accepts a Go regular expression, for example \{id:[0-9]+}.

\{name:.*}

Zero or more characters, including /

Multi-segment wildcard. /orders/\{rest:.*} matches /ext/orders/v1/billing/invoices.

Use \{name:.*} for a multi-segment wildcard. The form \{name…​} matches a single segment.

Methods and path variables

Specify methods in the value of the map, for example "/foo": ["GET"]. An empty list allows all methods. Host patterns and method prefixes in the path are not supported.

The /ext/ prefix is prepended to every pattern and is not stripped before the extension runs, so the path the extension sees includes it (for example /ext/orders/v1).

Matched path variables are not included separately in the extension input. Use req.path when the extension needs a value from the path.

Overlapping routes and registration order

Routes from all route extensions share one router. If patterns overlap, the first registered pattern handles the request; the most specific pattern is not selected automatically. Registration order is not guaranteed, so configure non-overlapping patterns. Exact duplicate route strings are rejected at startup.

Catch-all routes

When teams own separate API prefixes, each prefix can delegate to one catch-all route. The extension then owns the endpoint mapping within that prefix:

extensions:
  routeExtensions:
    orders:
      extension:
        extensionURL: /extensions/orders.star
      routes:
        "/orders": [] (1)
        "/orders/{path:.*}": [] (2)
1 Handles the bare /ext/orders path.
2 Handles /ext/orders/ and paths below it. The extension validates the path and method.

Use a disjoint prefix for each catch-all route. An empty method list allows all methods, so the extension must reject unsupported path and method combinations.

Declarative mapping

This extension allows users to define mappings for the request/response conversion using CEL expressions.

extensions:
  routeExtensions:
    foo: (1)
      mapping:
        request: (2)
          requestID: 'request.header["X-Request-Id"]'
          principal:
            id: '"abc"'
            roles: '["user"]'
            attr: "request.body.json"
          resource:
            id: '"test"'
            kind: '"request"'
          action: "request.method"
        response: (3)
          status: "check.allow ? 200 : 403"
          body: "check.requestID"
      routes: (4)
        "/bar": ["GET", "POST"]
        "/baz/{baz}": []
1 Unique name for the instance
2 Section for defining how to map the incoming HTTP request into a Cerbos CheckResources request
3 Section for defining how to map the Cerbos response into the HTTP response expected by the caller
4 Routes to which this mapping applies. See Route patterns.

Mapping reference

  • request (required):

    • requestID: a CEL expression returning a string.

    • principal (required):

      • id (required): a CEL expression returning a string.

      • roles (required): a CEL expression returning a list of strings, or a list of CEL expressions each returning a string.

      • attr: a CEL expression returning a map of strings to values, or a map of strings to CEL expressions each returning a value.

      • policyVersion: a CEL expression returning a string.

      • scope: a CEL expression returning a string.

    • resource (required):

      • kind (required): a CEL expression returning a string.

      • id (required): a CEL expression returning a string.

      • attr: a CEL expression returning a map of strings to values, or a map of strings to CEL expressions each returning a value.

      • policyVersion: a CEL expression returning a string.

      • scope: a CEL expression returning a string.

    • action (required): a CEL expression returning a string.

    • auxData:

      • jwt:

        • token: a CEL expression returning a string.

        • keySetID: a CEL expression returning a string.

  • response:

    • status: a CEL expression returning an HTTP status code as an integer. Defaults to 200 when allowed and 403 when denied.

    • headers: a CEL expression returning a map of strings to strings, or a map of strings to CEL expressions each returning a string.

    • body: a CEL expression returning bytes or a string.

Request expressions have access to the incoming HTTP request in the CEL variable request, which has the following fields:

  • method: the request’s HTTP method as an uppercase string.

  • header: map of HTTP request headers. Keys are header names in canonical form and values are strings (comma-separated when multiple headers with the same name are present in the request).

  • headers: map of HTTP request headers. Keys are header names in canonical form and values are lists of strings.

  • body: object with the following fields:

    • bytes: the raw request body

    • text: the request body as a string

    • json: the JSON-decoded request body

Response expressions have access to the CheckResources result in the CEL variable check which has the following fields:

  • requestId, principal, resource, and action: the inputs produced by the request expressions.

  • cerbosCallId: the ID for audit logging as a string.

  • allow: whether the action is allowed as a boolean.

  • outputs: the output from the policy as a map of source (policy-id#rule-name) to value.

  • validationErrors: errors encountered when validating the principal and resource against the corresponding schemas as a list of cerbos.schema.v1.ValidationError messages.

Using the YAML scalar block syntax > for CEL expressions is recommended to avoid quoting issues. action: "view" would be interpreted as the CEL expression view (which would fail to compile because a variable named view is not defined), when the desired setting was the CEL expression "view" (which just returns the intended string literal).

Minimal configuration
extensions:
  routeExtensions:
    foo:
      mapping:
        request:
          principal:
            id: >
              request.header["X-User-Id"]
            roles:
              - >
                request.header["X-User-Role"]
          resource:
            kind: >
              request.body.json.kind
            id: >
              request.body.json.id
          action: >
            "view"
Customizing the response
extensions:
  routeExtensions:
    foo:
      mapping:
        request:
          # ...
        response:
          status: >
            check.allow ? 204 : 404
          headers:
            X-Cerbos-Call-Id: >
              check.cerbosCallId
          body: >
            json.encode({"outputs": check.outputs})

Programmatic mapping

extensions:
  routeExtensions:
    infra: (1)
      extension:
        extensionURL: "/extensions/infra.wasm?checksum=sha256:2b2e0ddc150e8563127ba9bd13199519b357c85ca5b3de598c4bcd5729599c6c" (2)
        configuration: (3)
          environment: staging
      routes: (4)
        "/team-a/foo": []
        "/team-b/{baz}": ["GET"]
1 Unique name for the extension
2 URL to fetch the extension from. See extension URL format for more information.
3 Configuration values needed by the extension. [Optional]
4 Routes to which this mapping applies. See Route patterns.

Building route extensions