Developing Starlark extensions
Starlark is a lightweight Python dialect that can be used to drive the Synapse extension logic. They are easy to write and test because they do not require any intermediate compilation stages or specialised tools.
To use a Starlark script as the extension implementation, set the extensionURL configuration field to a valid extension URL in one of the following forms:
-
/path/to/script.star(File name must have a.starextension) -
starlark+http://scripts.example.com/synapse/envoy?checksum=sha256:677cce2788330f16f27981130eaa50aa4189beab2121903bcea5b3c4c918dccc(Protocol of the URL must be prefixed withstarlark+)
All Synapse extension points support running Starlark scripts. Each request is served by a fresh instance of a script and there’s no shared global state. Each extension type requires a specific set of functions that must be implemented by the script. See the extensions section below for details about these API contracts.
REPL
Synapse ships with a REPL (read, evaluate, print loop) to help with debugging Starlark scripts.
Synapse starlark repl [--exec] [SCRIPT_FILE]
Without any arguments, the command starts the REPL where you can write expressions and evaluate them. If started with a script file as the argument, it executes the script and populates the globals with symbols exported by the script (variables, function definitions and so on). If the --exec argument is provided, the REPL exits after executing the script and printing its outputs.
Press Ctrl+D to exit the REPL. You can load script files from disk using the load function.
>>> load("my_script.star", "my_script")
>>> my_script.foo("bar") # Executes the `foo` function exported by the script.
Standard environment
In addition to the built-in language constructs, the following variables and functions are available to use by any script.
| Name | Description |
|---|---|
|
Delete the cached value from Synapse cache |
|
Return the cached value from Synapse cache |
|
Save a value to shared Synapse cache with optional expiry and existence check |
|
Do a |
|
Do a lookup using one of the configured data sources |
|
Do a |
|
The optional configuration map attached to the extension definition in the Synapse configuration file |
|
The kind of the extension. One of |
|
The name of this extension as defined in the Synapse configuration file |
|
Ceiling of x |
|
Absolute value of x as a float |
|
Floor of x |
|
Value of x modulo y |
|
Value of x raised to the power of y |
|
Remainder of x/y |
|
Round x to nearest integer |
|
Create structs. E.g. |
|
Converts the given Unix time corresponding to the number of seconds and (optionally) nanoseconds since January 1, 1970 UTC into an object of type Time |
|
A constant representing a duration of one hour |
|
Reports whether loc is a valid time zone name |
|
A constant representing a duration of one microsecond |
|
A constant representing a duration of one millisecond |
|
A constant representing a duration of one minute |
|
A constant representing a duration of one nanosecond |
|
Returns the current local time |
|
Parses the given duration string. For more details, refer to https://pkg.go.dev/time#ParseDuration |
|
Parses the given time string using a specific time format and location. The expected arguments are a time string (mandatory), a time format (optional, set to RFC3339 by default, e.g. "2021-03-22T23:20:50.52Z") and a name of location (optional, set to UTC by default). For more details, refer to https://pkg.go.dev/time#Parse and https://pkg.go.dev/time#ParseInLocation. |
|
A constant representing a duration of one second |
|
Returns the Time corresponding to yyyy-mm-dd hh:mm:ss + nsec nanoseconds in the appropriate zone for that time in the given location. All the parameters are optional. |
Loadable modules
Optional modules can be imported using the load function.
# Load the "json" module as "json" which allows you to invoke its functions like `json.dumps` and so on.
load("json", "json")
print(json.dumps(x))
# Load the "hashlib" module as "hash" which allows you to invoke its functions like `hash.sha256` and so on.
load("hashlib", hash="hashlib")
print(hash.sha256(c))
The following modules are available to load.
base64
| Function | Description | Signature |
|---|---|---|
|
Decode base64 string to plain text |
|
|
Encode string to base64 |
|
csv
| Function | Description | Signature |
|---|---|---|
|
Read CSV string into list of string lists |
|
|
Write list of string lists to CSV string |
|
|
Write list of dictionaries to CSV string |
|
hashlib
| Function | Description | Signature |
|---|---|---|
|
Calculate MD5 hash |
|
|
Calculate SHA-1 hash |
|
|
Calculate SHA-256 hash |
|
|
Calculate SHA-512 hash |
|
http
| Function | Description | Signature |
|---|---|---|
|
Perform HTTP request with specified method |
|
|
Perform HTTP DELETE request |
|
|
Perform HTTP GET request |
|
|
Get current timeout setting |
|
|
Perform HTTP OPTIONS request |
|
|
Perform HTTP PATCH request |
|
|
Perform HTTP POST request |
|
|
Perform HTTP POST request with form data |
|
|
Perform HTTP PUT request |
|
|
Set global timeout for requests |
|
json
| Function | Description | Signature |
|---|---|---|
|
Convert JSON to value with optional default |
|
|
Convert value to JSON string with indentation |
|
|
Convert value to JSON |
|
|
Evaluate JSONPath expression |
|
|
Pretty-print JSON with indentation |
|
|
Query JSON using JSONPath |
|
|
Decode JSON with error handling |
|
|
Dump JSON with error handling |
|
|
Encode JSON with error handling |
|
|
Evaluate JSONPath with error handling |
|
|
Indent JSON with error handling |
|
|
Query JSONPath with error handling |
|
random
| Function | Description | Signature |
|---|---|---|
|
Return random element from sequence |
|
|
Return k-sized list of random elements |
|
|
Generate random base32 string |
|
|
Generate random byte string |
|
|
Return random integer in range |
|
|
Return random float in [0.0, 1.0) |
|
|
Generate random string from charset |
|
|
Shuffle sequence in place |
|
|
Return random float in range |
|
|
Generate random UUID |
|
re
| Function | Description | Signature |
|---|---|---|
|
Compile regex pattern |
|
|
Find all matches in string |
|
|
Match pattern at string start |
|
|
Search for pattern in string |
|
|
Split string by pattern |
|
|
Replace pattern matches in string |
|
stats
| Function | Description | Signature |
|---|---|---|
|
Calculate mean (alias) |
|
|
Calculate correlation coefficient |
|
|
Calculate covariance |
|
|
Calculate population covariance |
|
|
Calculate Euclidean distance |
|
|
Calculate geometric mean |
|
|
Calculate harmonic mean |
|
|
Calculate Manhattan distance |
|
|
Find maximum value |
|
|
Calculate arithmetic mean |
|
|
Find median value |
|
|
Calculate midrange |
|
|
Find minimum value |
|
|
Find most frequent values |
|
|
Calculate Pearson correlation |
|
|
Calculate percentile |
|
|
Calculate percentile by nearest rank |
|
|
Calculate population variance |
|
|
Randomly sample elements |
|
|
Calculate sample variance |
|
|
Apply sigmoid function |
|
|
Apply softmax function |
|
|
Calculate standard deviation |
|
|
Calculate standard deviation (alias) |
|
|
Calculate sample standard deviation |
|
|
Calculate sum |
|
|
Calculate trimean |
|
|
Calculate variance |
|
string
| Function | Description | Signature |
|---|---|---|
|
Get Unicode codepoint at index |
|
|
Escape HTML entities |
|
|
Find first occurrence of substring |
|
|
Find first occurrence (raises error) |
|
|
Get length in Unicode code points |
|
|
Shell-escape string |
|
|
Reverse string |
|
|
Find last occurrence of substring |
|
|
Find last occurrence (raises error) |
|
|
Extract substring |
|
|
Unescape HTML entities |
|
|
Shell-unescape string |
|
Data source extension
A script that implements a data source must export a function named lookup. This function will be called by Synapse with an object in the following shape:
struct(
data_source = "myDataSource", (1)
query = "SELECT department FROM employees WHERE id = :employee_id", (2)
query_parameters = { (3)
"employee_id": "simon"
},
cache_options = struct( (4)
cache_key = "simon", (5)
cache_expiry = time.minute, (6)
if_not_exists = 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 type — including complex objects and arrays |
| 3 | Optional query parameters as a dictionary with string keys and values of any type |
| 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 |
The function must return a struct that contains a field named result with the result of the lookup.
def lookup(req):
result = {"output": "hello from starlark lookup", "query": req.query}
return struct(result = result)
Envoy extension
An Envoy extension must export a function named envoy_check that accepts an object in the shape of Envoy check request and returns one of the following responses.
Direct response
Return a complete Envoy check response to return back to the caller.
struct(envoy_check_response = struct(
status = struct(code = 7),
denied_response = struct(body = "Go away")
))
Cerbos mapping
Return a struct 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.
struct(envoy_cerbos_mapping = struct(
check_request = struct( (1)
principal = struct(
id = "daffy",
roles = ["duck"]
),
resources = [
struct(
actions = ["GET"],
resource = struct(
id = "http",
kind = "request",
attr = {
"path": "/foo"
}
)
)
]
),
allow_response = struct( (2)
status = struct(code = 0)
),
deny_response = struct( (3)
status = struct(code = 7),
denied_response = struct(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 map_cerbos_response 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.
struct(cerbos_check_request = struct(
principal = struct(
id = "daffy",
roles = ["duck"]
),
resources = [
struct(
actions = ["GET"],
resource = struct(
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 map_cerbos_response function with an object of the following shape:
{
envoy_request: { ... }, (1)
cerbos_ request: { ... }, (2)
cerbos_response: { ... } (3)
}
| 1 | Original Envoy Check request that triggered this chain |
| 2 | Cerbos CheckResources request |
| 3 | Cerbos CheckResources response |
The map_cerbos_response function should take the above input and return a struct representing an Envoy check response which will be sent back to the caller.
struct(
status = struct(code = 7),
denied_response = struct(
body = "Denied by policy secops-general"
)
)
Example
The following example invokes a PDP call for any paths that are in the secure_paths configuration value.
def envoy_check(req):
http_req = req.attributes.request.http
path = http_req.path
if path in context.extension_config["secure_paths"]:
return struct(cerbos_mapping = struct(
check_request = struct(
principal = struct(id = "daffy", roles = ["user"]),
resources = [struct(
resource = struct(id = "x", kind = "request", attr = {"path": path}),
actions = [http_req.method]
)]
),
allow_response = struct(
status = struct(code = 0),
ok_response = struct(
headers_to_remove = ["bar"]
)
),
deny_response = struct(
status = struct(code = 7),
denied_response = struct(
body = "no go"
)
)
))
return struct(envoy_check_response = struct(
status = struct(code = 0),
ok_response = struct(
headers_to_remove = ["foo"]
)
))
Proxy extension
A proxy extension must export at least one of the following functions.
augment_check_request-
Modify a CheckResources request
augment_check_response-
Modify a CheckResources response
augment_plan_request-
Modify a PlanResources request
augment_plan_response-
Modify a PlanResources response
Each function is invoked with an object in the shape of a CheckResources/PlanResources request or response. The function can modify the object as required and return it back. Refer to https://docs.cerbos.dev/cerbos/latest/api/#_request_and_response_formats for details about the request and response types.
Example
The following example injects hard-coded attributes to a known principal.
def handle_augment(req):
if hasattr(req, "principal"):
if req.principal.id == "john":
req.principal.attr = {
"geography": "GB",
"department": "IT"
}
return req
def augment_check_request(req):
return handle_augment(req)
def augment_plan_request(req):
return handle_augment(req)
Route extension
A route extension must export a function named handle_http_route which receives an object in the following shape as the argument.
struct(
method = "POST", (1)
headers = { (2)
"x-forwarded-for": struct(values = ["127.0.0.1:2090"])
},
raw_url = "https://example.com/path/to/foo?a=av&b=bv1&b=bv2", (3)
host = "example.com", (4)
path = "/path/to/foo", (5)
query_params = { (6)
"a": struct(values = ["av"]),
"b": struct(values = ["bv1", "bv2"])
},
body = "Hello" (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 bytes 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.
struct(http_response = struct(
status = 401 (1)
headers = { (2)
"content-type": struct(values = ["application/text"])
},
body = "Access denied" (3)
))
| 1 | HTTP status code |
| 2 | Headers to add to the response |
| 3 | Body of the response |
Cerbos mapping
Return an object that represents 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.
struct(cerbos_mapping = struct(
check_request = struct( (1)
principal = struct(
id = "daffy",
roles = ["duck"]
),
resources = [
struct(
actions = ["GET"],
resource = struct(
id = "http",
kind = "request",
attr = {
"path": "/foo"
}
)
)
]
),
allow_response = struct( (2)
status = 200,
body = "Welcome"
),
deny_response = struct( (3)
status = 401,
body = "Access denied"
)
))
| 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 an object that contains a Cerbos CheckResources request. In this mode, the extension must export a second function named handle_cerbos_response 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.
struct(check_request = struct(
principal = struct(
id = "daffy",
roles = ["duck"]
),
resources = [
struct(
actions = ["GET"],
resource = struct(
id = "http",
kind = "request",
attr = {
"path": "/foo"
}
)
)
]
))
Synapse sends the Check request to the PDP and then invokes the handle_cerbos_response function of the extension with the following information.
struct(
http_request = struct( ... ), (1)
cerbos_request = struct( ... ), (2)
cerbos_response = struct( ... ) (3)
}
| 1 | Original HTTP request that triggered this route extension |
| 2 | Cerbos CheckResources request |
| 3 | Cerbos CheckResources response |
handle_cerbos_response should return a HTTP response to send back to the caller.
struct(
status = 401,
headers = {
"content-type": struct(values = ["application/json"])
},
body = "Access denied"
}
Example
def handle_http_route(req):
return struct(cerbos_mapping = struct(
check_request = struct(
principal = struct(id = "daffy", roles = ["user"]),
resources = [struct(
resource = struct(id = "x", kind = "request", attr = {"path": req.path}),
actions = [req.method]
)]
),
allow_response = struct(
status = 200,
headers = {"foo": struct(values = ["foo"])},
body = "Welcome"
),
deny_response = struct(
status = 403,
headers = {"foo": struct(values = ["foo"])},
body = "No entry"
)
))