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 when the extension is first initialized. This is similar to a constructor and can be used to do things such as creating long-lived objects, performing validation of configuration and so on.
When Synapse is gracefully shut down, it invokes the cerbosDeinit function on any module that exports it. This is similar to a destructor and can be used to close connections and perform other cleanup tasks.
Both cerbosInit and cerbosDeinit functions take no arguments and return an i32 where 0 denotes success and any non-zero value denotes failure. If the extension has any custom configuration, it’s accessible via the Extism.
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
}
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 string -
value: Pointer to a byte array -
duration: 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 string -
value: Pointer to a byte array -
duration: Integer value defining cache duration in milliseconds
-
- Outputs
-
-
An integer value. Non-zero value indicates failure.
-
checkResources-
Perform a Cerbos
CheckResourcescall.- 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
PlanResourcescall.- 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.
{
"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.
{
"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
}
result := gjson.GetBytes(req.Query, "query").String()
resp := LookupResponse{Result: fmt.Appendf(nil, "%q", result)}
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.
{
"envoyCerbosMapping": {
"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"
}
}
}
}
| 1 | Cerbos CheckResources request to send |
| 2 | Envoy response to send if Cerbos returns EFFECT_ALLOW |
| 3 | Envoy response to send if Cerbos returns EFFECT_DENY |
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)
}
| 1 | Original Envoy Check request that triggered this chain |
| 2 | Cerbos CheckResources request |
| 3 | Cerbos CheckResources response |
The envoyMapCerbosResponse function should take the above input and return a pointer to a JSON-encoded Envoy check response which will be sent back to the caller.
{
"status": {"code": 7},
"deniedResponse": {
"body": "Denied by policy secops-global"
}
}
Proxy extension
A proxy extension must export at least one of the following functions. (It’s legal to implement more than one.)
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 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.
Route extension
A route extension must export a function named handleHTTPRoute which receives a pointer to the JSON-encoded HTTP request which takes the following form.
{
"method": "POST", (1)
"headers": { (2)
"x-forwarded-for": { "values": ["127.0.0.1:2090"] }
},
"rawUrl": "https://example.com/path/to/foo?a=av&b=bv1&b=bv2", (3)
"host": "example.com", (4)
"path": "/path/to/foo", (5)
"queryParams": { (6)
"a": { "values": ["av"] },
"b": { "values": ["bv1", "bv2"] }
},
"body": "aGVsbG8K" (7)
}
| 1 | HTTP method |
| 2 | Header values |
| 3 | Full URL of the request |
| 4 | Host segment from the URL |
| 5 | Path segment from the URL |
| 6 | Query parameters from the URL |
| 7 | Request body as a base64-encoded string |
The return value of the function must be one of the following values.
Direct response
Return a complete HTTP response to be sent back to the caller.
{
"httpResponse": {
"status": 401, (1)
"headers": { (2)
"content-type": { "values": ["application/text"] }
},
"body": "Z28gYXdheQo=" (3)
}
}
| 1 | HTTP status code |
| 2 | Headers to add to the response |
| 3 | Base64-encoded body |
Cerbos mapping
Return a JSON object that contains a Cerbos CheckResources request and the HTTP 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 HTTP 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": 200,
"body": "aGVsbG8K"
},
"denyResponse": { (3)
"status": 401,
"body": "Z28gYXdheQo="
}
}
}
| 1 | Cerbos CheckResources request |
| 2 | HTTP response to send if Cerbos returns EFFECT_ALLOW |
| 3 | HTTP response to send if Cerbos resturns EFFECT_DENY |
Cerbos request
Return a JSON object that contains the Cerbos CheckResources request. In this mode, the extension must export a second function named handleCerbosResponse that accepts the CheckResources response and returns a HTTP response. Use this mode when you want to construct a complex HTTP response using values from the Cerbos response.
{
"cerbosMapping": {
"checkRequest": { (1)
"principal": {
"id": "daffy_duck",
"roles": ["duck"]
},
"resources": [{
"actions": ["GET"],
"resource": {
"id": "http",
"kind": "request",
"attr": {
"path": "/foo"
},
}
}]
}
}
}
| 1 | Cerbos CheckResources request |
Synapse sends the Check request to the PDP and then invokes the handleCerbosResponse function of the extension with the following information.
{
"httpRequest": { ... }, (1)
"cerbosRequest": { ... }, (2)
"cerbosResponse": { ... } (3)
}
| 1 | Original HTTP request that triggered this route extension |
| 2 | Cerbos CheckResources request |
| 3 | Cerbos CheckResources response |
handleCerbosResponse should return a HTTP response to send back to the caller.
{
"status": 401,
"headers": {
"content-type": { "values": ["application/json"] }
},
"body": "eyJtZXNzYWdlIjogIkRlbmllZCBieSBwb2xpY3kgc2Vjb3BzLWdsb2JhbCJ9Cg=="
}