Developing Webassembly extensions :: Cerbos Authorization Management Platform // Documentation

Developing Webassembly extensions

Synapse supports Webassembly (WASM) extensions using the Extism framework. While it’s possible to develop extensions without any framework, it’s recommended to use one of the supported Extism plugin developments kits as they provide helpers for sending JSON back and forth, reading configuration and other useful functions.

WASM extensions should be reactor modules that are long-lived. Note that Synapse pools multiple instances of a single extension in order to handle parallel requests (WASM memory is not thread-safe). This is an important consideration to keep in mind when creating long-lived resources such as database connections as there will be multiple independent copies of each WASM module alive at the same time on a single Synapse instance.

Common functionality

Lifecycle management

If the WASM module exports a cerbosInit function, Synapse invokes it to initialize the instance. Note that Synapse maintains an internal pool of instances to reduce load times and handle multi-threading so this function will be invoked multiple times (i.e. it’s not a singleton). It’s useful for allocating long-lived, per-instance, reusable resources such as database connections.

When Synapse is gracefully shut down, the cerbosDeinit function is invoked on all instances of the module if it’s exported. It can be used to perform cleanup tasks such as closing database connections acquired during cerbosInit.

Both cerbosInit and cerbosDeinit take no arguments and return an i32 where 0 denotes success and any non-zero value denotes failure. Custom configuration set in the Synapse configuration file under the extension’s configuration field is accessible via the Extism PDK’s config helpers (pdk.GetConfig in Go, Config.get in JavaScript/TypeScript).

The following snippet demonstrates how these functions might be implemented in a Go WASM module.

var sqlite *sqlx.DB

//go:wasmexport cerbosInit
func cerbosInit() int32 {
    connectionStr, exists := pdk.GetConfig("connectionString")
    if !exists {
        pdk.SetError(errors.New("connectionString configuration is required"))
        return 1
    }

db, err = sqlx.Connect("sqlite3", connectionStr)
    if err != nil {
        pdk.SetError(err)
        return 1
    }

sqlite = db
    return 0
}

//go:wasmexport cerbosDeinit
func cerbosDeinit() int32 {
    if err := sqlite.Close(); err != nil {
        pdk.SetError(err)
        return 1
    }

return 0
}

Logging

Log messages emitted by the Extism PDK are routed to Synapse’s logger and tagged with the configured extension name. Messages below the Synapse log level set via --log.level are suppressed. Use the PDK’s log helpers from inside any export to surface diagnostic output through the host:

Host functions

Synapse exposes the following host functions that can be called by any extension. They are all exported under the module name extism:host/user.

cacheDelete

Delete a value from the cache.

Inputs

Outputs

cacheGet

Get a value from the cache.

Inputs

Outputs

cacheSet

Save a value to cache.

Inputs

Outputs

cacheSetIfNotExists

Save a value to cache if it doesn’t already exist.

Inputs

Outputs

checkResources

Perform a Cerbos CheckResources call.

Inputs

Outputs

dataSourceLookup

Lookup information from a data source.

Inputs

Outputs

planResources

Perform a Cerbos PlanResources call.

Inputs

Outputs

The following snippet illustrates how to access host functions from a Go WASM module.

//go:wasmimport extism:host/user cacheSet
func cacheSet(uint64, uint64, uint32) int32

func writeToCache(key string value []byte) {
    keyMem := pdk.AllocateString(key)
    defer keyMem.Free()
    valueMem := pdk.AllocateBytes(value)
    defer valueMem.Free()

result := cacheSet(keyMem.Offset(), valueMem.Offset(), 10000)
    if result != 0 {
        // handle error
    }
}

Data source extension

A data source extension must export a function named lookup that accepts a pointer to a JSON-encoded lookup request and returns a pointer to a JSON-encoded lookup result.

Lookup request

{
    "dataSource": "myDataSource", (1)
    "query": "SELECT department FROM employees WHERE id = :employee_id", (2)
    "queryParameters": { (3)
        "employee_id": "simon"
    },
    "cacheOptions": { (4)
        "cacheKey": "simon", (5)
        "cacheExpiry": "300s", (6)
        "ifNotExists": true (7)
    }
}
1 Name of the data source to query
2 Query to execute on the data source. This is specific to the data source. The value can be any valid JSON value, including complex objects and arrays.
3 Optional query parameters
4 Optional cache options for caching the result of the lookup
5 Cache key for this lookup
6 Optional duration for caching the result of the lookup
7 Optional flag to prevent overwriting an existing cache key

Only the dataSource and query fields are required. The query field can be any valid JSON value, including complex nested objects. The API contract for the query field is specific to each data source implementation as it depends on the logic that each data source encapsulates.

The response from the data source only contains the single field result. It could contain any valid JSON value including objects and arrays. The format of the result depends on the data source.

Lookup response

{
    "result": [\
      {"id": 1, "first_name": "Daffy"},\
      {"id": 2, "first_name": "Elmer"}\
    ]
}

The following snippet demonstrates a very simple implementation of a data source that echoes the query back to the caller.


type LookupRequest struct {
    DataSource string          `json:"dataSource"`
    Query      json.RawMessage `json:"query"`
}

type LookupResponse struct {
    Result json.RawMessage `json:"result"`
}

//go:wasmexport lookup
func lookup() int32 {
    var req LookupRequest
    if err := pdk.InputJSON(&req); err != nil {
        pdk.SetError(err)
        return 1
    }

resp := LookupResponse{Result: req.Query}
    if err := pdk.OutputJSON(resp); err != nil {
        pdk.SetError(err)
        return 1
    }

return 0
}

Envoy extension

An Envoy extension must export a function named envoyCheck that accepts a JSON-encoded Envoy check request and returns one of the following responses.

Direct response

Return a complete Envoy check response to return back to the caller.

{
    "envoyCheckResponse": {
        "status": {"code": 7},
        "deniedResponse": {
            "body": "Go away"
        }
    }
}

Cerbos mapping

Return a JSON object that contains a Cerbos CheckRequest and the Envoy check response to send based on the Cerbos response. Synapse sends the Cerbos request to the configured PDP (applying any configured proxy extensions) and responds with the appropriate Envoy response based on whether Cerbos returns ALLOW or DENY.

{
    "cerbosMapping": {
        "checkRequest": { (1)
            "principal": {
                "id": "daffy_duck",
                "roles": ["duck"]
            },
            "resources": [{\
                "actions": ["GET"],\
                "resource": {\
                    "id": "http",\
                    "kind": "request",\
                    "attr": {\
                        "path": "/foo"\
                    },\
                }\
            }]
        },
        "allowResponse": { (2)
            "status": {"code": 0}
        },
        "denyResponse": { (3)
            "status": {"code": 7},
            "deniedResponse": {
                "body": "Go away"
            }
        }
    }
}

Cerbos check request

Return a Cerbos CheckRequest to send to the PDP (applying any configured proxy extensions). In this mode, the extension must implement a second function named envoyMapCerbosResponse to map the CheckResources response to an Envoy response. Use this mode when you need to write complex mapping logic to convert a Cerbos response to an Envoy response.

{
    "cerbosCheckRequest": {
        "principal": {
            "id": "daffy_duck",
            "roles": ["duck"]
        },
        "resources": [{\
            "actions": ["GET"],\
            "resource": {\
                "id": "http",\
                "kind": "request",\
                "attr": {\
                    "path": "/foo"\
                },\
            }\
        }]
    }
}

Synapse will send the above request to the PDP and invoke the extension a second time by calling the envoyMapCerbosResponse function with a pointer to the following JSON-encoded content.

{
    "envoyRequest": { ... }, (1)
    "cerbosRequest": { ... }, (2)
    "cerbosResponse": { ... } (3)
}

Proxy extension

A proxy extension must export at least one of the following functions. (It’s legal to implement more than one.)

augmentAuthzenEvaluationBatchRequest

Modify an AuthZEN AccessEvaluations request

augmentAuthzenEvaluationBatchResponse

Modify an AuthZEN AccessEvaluations response

augmentAuthzenEvaluationRequest

Modify an AuthZEN AccessEvaluation request

augmentAuthzenEvaluationResponse

Modify an AuthZEN AccessEvaluation response

augmentCheckRequest

Modify a CheckResources request

augmentCheckResponse

Modify a CheckResources response

augmentPlanRequest

Modify a PlanResources request

augmentPlanResponse

Modify a PlanResources response

Each function is invoked with a pointer to a JSON-encoded AccessEvaluation/CheckResources/PlanResources` request or response. It must return a pointer to a JSON-encoded value of the same type. Refer to https://docs.cerbos.dev/cerbos/latest/api/#_request_and_response_formats for details about the request and response types.