Authorization you can read, test, and trust

Policies your team can read, a testing framework that proves they work, and decisions that update in real time when context changes. Spring, Django, NestJS, .NET, and more.

policy "freeze during peak hours"
deny
    subject.role == "engineer";
    action == "deploy";
    resource.environment == "production";
    // In production, use real business hours:
    // <time.localTimeIsBetween("09:00", "17:00", "Europe/Berlin")>;
    // For this demo, the window toggles every few seconds:
    <time.toggle(8000, 4000)>;
obligation {
    "type": "notify",
    "channel": "ops-alerts",
    "message": "Deployment blocked: peak hours"
}

Authorization for AI agents and MCP

AI agents call tools, access resources, and act on behalf of users. Each of these operations needs authorization that goes beyond allow or deny. SAPL evaluates policies inside your MCP server or AI framework, with full access to the request context. Policies can allow a tool call while attaching obligations: redact sensitive fields from the response, log the access, require human approval above a threshold, or cap a parameter value.

Framework-native

Decorators and annotations run inside your FastMCP, Spring AI, or NestJS server. Authorization is part of the application, not an external proxy. No extra infrastructure to deploy or operate.

Beyond allow/deny

Every decision can carry obligations and advice. Filter tool responses, transform parameters, enforce audit trails, or trigger approval workflows. The constraint engine executes these automatically.

Per-tool, per-resource, per-prompt

Policies see the full MCP operation context: which tool, what parameters, which user, what secrets. Control tool visibility, gate resource access, and constrain prompt arguments individually.

AI scenarios with working code

RAG Pipeline

Document-level access control in retrieval-augmented generation. Filter and redact retrieved content before it reaches the LLM.

AI Tool Authorization

Per-tool authorization for Spring AI applications. Control which tools agents can call and transform tool responses via obligations.

Human-in-the-Loop

Policy-driven approval workflows for sensitive AI operations. The policy decides when human confirmation is needed, not the code.

MCP Server Authorization

Authorize MCP tool calls, resources, and prompts inside MCP servers. Decorators, constraint handlers, JWT/ABAC.

Decisions that stay current

Every authorization engine evaluates a request and returns a decision, and SAPL handles that case with zero overhead. The difference is what happens when context changes. Traditional engines have no way to revise the decision, so the application enforces a stale result until the next explicit check. With SAPL, the application subscribes to decisions and the PDP pushes updates whenever underlying attributes change. This is event-driven rather than polling, which reduces both latency and resource utilization. Toggle the threat level below and watch.

Policy Set
set "api activity access"
priority deny or abstain
for action == "monitorActivity"

policy "lockdown on critical"
deny
    "https://siem.local/threat"..level == "critical";

policy "full access for investigation"
permit
    "https://siem.local/threat"..level == "high";

policy "redacted by default"
permit
    "https://siem.local/threat"..level == "low";
obligation {
    "type": "filterJsonContent",
    "actions": [
        {"type":"blacken", "path":"$.user", "discloseLeft": 1},
        {"type":"blacken", "path":"$.ip"}
    ]
}
Endpoint
@EnforceRecoverableIfDenied
@GetMapping(
    value = "/api/activity",
    produces = MediaType
        .TEXT_EVENT_STREAM_VALUE)
public Flux<ApiEvent> stream() {
    return activityService
        .streamAll();
}
Attribute Source
SIEM Threat Level
Click to change
PDP Decision
PERMIT
redact PII
API Activity Monitor
TimeRequestStatusUserSource IP
Access denied
Critical threat level. Activity stream suspended.
SSE Event Stream connected

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
policy "permit on emergency"
permit
    action == "read" & resource == "time";
    "status".<mqtt.messages> == "emergency";
Test (excerpt)
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  not-applicable when MQTT status is any other value  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 from emergency to ok  1ms
      PASS  not-applicable then permit as status changes from ok to emergency  1ms
      PASS  alternating permit and not-applicable 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

Scenarios

Step-by-step guides for common authorization patterns, each with working code and runnable demos.

Spring Security

Secure a Spring Boot application with attribute-based access control. Method-level enforcement, embedded PDP, reactive policies.

AI Security

RAG pipeline authorization, MCP tool access control, human-in-the-loop approval workflows for AI agent operations. Coming soon.

Policy Testing

Declarative policy tests with the SAPLTest DSL, coverage reporting, and CI/CD quality gates. Coming soon.

View all scenarios

Ready to evaluate SAPL?

Professional support, consulting, and training available from FTK in Dortmund, Germany. We help you plan your authorization architecture, integrate SAPL into your stack, and train your team.

Opens your email client. No data is collected or processed by this website.

European open source

SAPL originated in European research and is built around transparency, privacy, and digital independence. It is deployed in production across European research and industry projects protecting critical infrastructure and sensitive personal data. Open source, self-hosted, no vendor lock-in.

SAPL secures virtual power plant operations across European island energy systems, protecting grid control and distributed energy resource management.

Horizon 2020 - Grant Agreement No. 957852

SAPL protects sensitive clinical data in a multi-centre mental health study for young people across seven European countries.

Horizon Europe - Grant Agreement No. 101080923

®

Open source, self-hosted. Apache 2.0 licensed. No vendor lock-in, no external dependencies, no data leaves your infrastructure. Run SAPL embedded in your application or as a standalone server.

Schneider Electric Aix-Marseille University Cardiff University Brunel University London CSIC RDIUP FTK WIZ Research IDEA Ingenieria Inavitas NVISION Regenera Levante Univerza v Mariboru
Funded by the European Union

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.