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:
Go:
pdk.Log(pdk.LogDebug, "msg")(levels:LogTrace,LogDebug,LogInfo,LogWarn,LogError).JavaScript/TypeScript:
console.debug("msg")(alsoconsole.trace,console.log/info,console.warn,console.error).
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
key: Pointer to a string
Outputs
- An integer value. Non-zero value indicates failure.
cacheGet
Get a value from the cache.
Inputs
key: Pointer to a string
Outputs
- Pointer to a byte array containing the value.
cacheSet
Save a value to cache.
Inputs
key: Pointer to a stringvalue: Pointer to a byte arrayduration: Integer value defining cache duration in milliseconds
Outputs
- An integer value. Non-zero value indicates failure.
cacheSetIfNotExists
Save a value to cache if it doesn’t already exist.
Inputs
key: Pointer to a stringvalue: Pointer to a byte arrayduration: Integer value defining cache duration in milliseconds
Outputs
- An integer value. Non-zero value indicates failure.
checkResources
Perform a Cerbos CheckResources call.
Inputs
request: Pointer to a byte array containing the JSON-encodedCheckResourcesrequest
Outputs
- Pointer to a byte array containing the JSON-encoded
CheckResourcesresponse
dataSourceLookup
Lookup information from a data source.
Inputs
request: Pointer to a byte array containing the JSON-encoded lookup request. See Data sources.
Outputs
- Pointer to a byte array containing the JSON-encoded lookup response. See Data sources.
planResources
Perform a Cerbos PlanResources call.
Inputs
request: Pointer to a byte array containing the JSON-encodedPlanResourcesrequest
Outputs
- Pointer to a byte array containing the JSON-encoded
PlanResourcesresponse
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.