Authorization you can read, test, and trust
An open-source policy language and engine for the entire stack — from web apps and APIs to IoT and AI agents. Readable policies. Dedicated testing framework. Request-response and real-time streaming.
TODO: Why SAPL
TODO: Streaming
Placeholder
TODO: Readable Policies
Placeholder
TODO: Policy Testing
Placeholder
Use it with any stack
Standalone server
Run SAPL Node as a dedicated authorization service. Any language can query it via the HTTP API — REST for one-shot decisions, Server-Sent Events for streaming.
Framework SDKs
One annotation or decorator per endpoint. Idiomatic integration for Spring, NestJS, Django, Flask, FastAPI, Tornado, FastMCP, and .NET.
In-process on the JVM
Add the PDP as a dependency to your Java, Kotlin, or Scala project. Policies evaluate in your process — no network hop, no sidecar, no separate service to operate.
All integration modes support single and multi-subscriptions, streaming and one-shot decisions, and the full obligation/advice/resource transformation model.
// SpEL expressions reference method parameters with #name
@PreEnforce(
action = "'notifyParticipant'",
resource = "{'recipient': #recipient, 'message': #message}"
)
public String notifyParticipant(String recipient, String message) {
return notificationService.send(recipient, message);
}
# Lambda builds resource from route params and JWT secrets
@pre_enforce(
action="exportData",
resource=lambda ctx: {
"pilotId": ctx.params.get("pilot_id", ""),
"sequenceId": ctx.params.get("sequence_id", ""),
},
secrets=lambda ctx: {"jwt": ctx.request.state.token},
)
async def get_export_data(pilot_id: str, sequence_id: str):
return {"pilotId": pilot_id, "data": "export-payload"}
// Arrow function accesses return value after execution
@PostEnforce({
action: 'readRecord',
resource: (ctx) => ({
type: 'record',
data: ctx.returnValue,
}),
})
@Get('record/:id')
getRecord(@Param('id') id: string) {
return this.records.find(r => r.id === id);
}
// ISubscriptionCustomizer builds the subscription programmatically
[HttpGet("exportData/{pilotId}/{sequenceId}")]
[PreEnforce(Action = "exportData", Customizer = typeof(ExportCustomizer))]
public IActionResult GetExportData(string pilotId, string sequenceId)
{
return Ok(new { pilotId, sequenceId, data = "export-payload" });
}
# Lambda builds resource from route params, with JWT secrets
@pre_enforce(
action="exportData",
resource=lambda ctx: {
"pilotId": ctx.params.get("pilot_id", ""),
"sequenceId": ctx.params.get("sequence_id", ""),
},
secrets=lambda ctx: {"jwt": getattr(ctx.request, "sapl_token", None)},
)
async def get_export_data(request, pilot_id: str, sequence_id: str):
return JsonResponse({"pilotId": pilot_id, "data": "export-payload"})
Test policies like you test code
SAPL is the only authorization engine with a dedicated policy testing language. Mock external data sources, emit streaming attribute changes, and assert that decisions update correctly — all declaratively. Enforce coverage thresholds as quality gates in CI/CD pipelines and generate coverage reports.
policy "permit on emergency"
permit
action == "read" & resource == "time";
"status".<mqtt.messages> == "emergency";
scenario "decision changes when emergency status changes"
given
- attribute "statusMock" "status".<mqtt.messages> emits "emergency"
when "user" attempts "read" on "time"
expect permit
then
- attribute "statusMock" emits "ok"
expect not-applicable
then
- attribute "statusMock" emits "emergency"
expect permit;
> sapl test --dir policies --testdir tests --policy-hit-ratio 100 --condition-hit-ratio 100
permit_on_emergency.sapltest
Permit access only during emergency status
PASS permit when MQTT status is emergency 13ms
PASS not-applicable when MQTT status is ok 1ms
PASS indeterminate when MQTT PIP returns error 1ms
Policy scope - only read action on time resource
PASS not-applicable for write action even during emergency 1ms
PASS not-applicable for delete action even during emergency 1ms
PASS not-applicable for read on different resource 1ms
Streaming - decision changes dynamically with MQTT status
PASS permit then not-applicable as status changes 1ms
PASS not-applicable then permit as status changes 1ms
PASS alternating decisions with multiple status changes 1ms
Tests: 10 passed, 10 total
Time: 27ms
Coverage:
Policy Hit Ratio 100.00% >= 100.00% PASS
Condition Hit Ratio 100.00% >= 100.00% PASS
TODO: Scenarios
TODO: AI Security / Spring Security / Data-Level Security / IoT & MQTT
TODO: Scriptable authorization — sapl check as a shell-level authorization gate. Exit code 0 = PERMIT, non-zero = deny. Use in shell scripts, cron jobs, CI/CD pipelines, Makefiles, git hooks — anywhere a command-line decision is needed.
European open source
SAPL is developed at FTK in Dortmund, Germany, and funded through EU research programmes. It is deployed in production across European research and industry projects protecting critical infrastructure and sensitive personal data.
SAPL secures virtual power plant operations across European island energy systems, protecting grid control and distributed energy resource management.
SAPL protects sensitive clinical data in a multi-centre mental health study for young people across seven European countries.
Open source, self-hosted
Apache 2.0 licensed. No vendor lock-in, no external dependencies, no data leaves your infrastructure. Run the PDP embedded in your application or as a standalone server.
SAPL has received funding from the European Union's Horizon 2020 research and innovation programme under Grant Agreement No. 957852 (VPP4Islands) and from the European Union's Horizon Europe programme under Grant Agreement No. 101080923 (SMILE). The views expressed are those of the authors and do not necessarily reflect those of the European Commission.










