Introduction Series: 1. Overview • 2. Subscriptions • 3. Policy Structure • 4. Decisions • 5. Attributes • 6. Getting Started
Accessing Attributes
External Attributes
When a PEP constructs an authorization subscription, it includes information available in the application’s current context: the authenticated user (subject), the requested operation (action), the target resource, and contextual data (environment). This represents what the application knows at that moment.
However, policies often require information that isn’t readily available to the PEP:
- A user’s department membership (stored in HR systems)
- Security clearance levels (maintained in identity management and as document metadata)
- Current work schedules (managed by scheduling systems)
- Real-time conditions (current time, system status, resource availability)
Policy Information Points (PIPs) bridge this knowledge gap by fetching attributes from external sources on demand.
Static vs. Streaming Attributes
The authorization subscription provides a snapshot of what the PEP knows when making the request. Once sent, these attributes don’t change unless the PEP creates a new subscription. This works well for stable data like usernames or resource identifiers, but many authorization decisions depend on dynamic conditions that change over time: current time, system status, metrics, etc.
Because the authorization protocol is based on the PDP pushing decisions to the PEP (not the PEP pushing updated attributes), dynamic attributes must come from PIPs. This is why time-based policies use <time.now> rather than expecting the PEP to include timestamps - the PDP needs to access time continuously, not just at subscription creation.
PIPs enable both:
- Bridging knowledge gaps: Fetching data the PEP doesn’t have
- Enabling streaming policies: Providing dynamic attributes that update over time
Consider a basic policy that checks attributes from the authorization subscription:
policy "compartmentalize read access by department"
permit
resource.type == "patient_record" & action == "read";
subject.role == "doctor";
resource.department == subject.department;
This policy works if the authorization subscription includes both subject.role and subject.department. But what if that information isn’t in the subscription?
External Attribute Access
In many cases, the PEP doesn’t know the specifics of access policies and doesn’t have access to all information required for authorization decisions. When a user’s role or department is stored in an external system (user directory, HR database, etc.), policies can use PIPs to fetch this data.
In natural language, a suitable policy could be “Permit doctors to read data from any patient.” The policy addresses the profile attribute of the subject, stored externally. SAPL allows expressing this policy as follows:
Introduction - Sample Policy 2
policy "doctors read patient data"
permit
action == "read" &
resource.type == "patient_record";
subject.username.<user.profile>.function == "doctor";
The first statement filters by action and resource type, fast checks that don’t require external data.
The policy assumes that the user’s function is not provided in the authorization subscription but is stored in the user’s profile. The second statement accesses the attribute user.profile (using an attribute finder step .<finder.name>) to retrieve the profile of the user with the username provided in subject.username. The fetched profile is a JSON object with a property named function. The expression compares it to "doctor".
Streaming Attributes
In many scenarios, authorization decisions should update automatically when data changes. SAPL’s streaming model enables this without polling or manual refresh.
Consider access control based on work shifts:
policy "read patient records during business hours"
permit
resource.type == "patient_record" & action == "read";
subject.role == "doctor";
resource.department == subject.department;
<time.localTimeIsBetween("08:00:00", "18:00:00")>;
The <time.localTimeIsBetween(...)> attribute is evaluated fresh each time the policy runs. More importantly, it streams: When a doctor is granted access at 17:59, the PEP receives PERMIT and maintains the connection. At exactly 18:00:01, the PDP automatically pushes a new DENY decision to the PEP - without any polling or manual refresh. The PEP can then terminate the session or deny further operations.
This is Attribute Stream-based Access Control (ASBAC) - policies respond to changing conditions in real-time.
Composing Streaming Attributes
PIPs can be composed for more sophisticated scenarios:
policy "doctors read records during assigned shift"
permit
resource.type == "patient_record" & action == "read";
subject.role == "doctor";
resource.department == subject.department;
var currentDay = time.dayOfWeek(<time.now>);
var todaysShift = subject.employeeId.<schedules.shifts(currentDay)>;
<time.localTimeIsBetween(todaysShift.start, todaysShift.end)>;
This policy:
- Gets the current day of the week from
<time.now>(a built-in streaming PIP) - Uses that to fetch the doctor’s shift schedule for today from a custom
schedulesPIP - Checks if the current time is within the shift window
All of these attributes stream - when the clock crosses a shift boundary, or when shift schedules are updated in the scheduling system, the PDP automatically sends new decisions to the PEP.
This example assumes a custom
schedulesPIP that provides shift information. See Attribute Finders for implementing custom PIPs.
Traditional access control systems make one-time decisions. SAPL maintains continuous authorization that adapts to changing conditions - time passing, data updates, or policy changes - all without the PEP needing to re-request decisions.
Built-in and Custom PIPs
The examples above use built-in PIPs like time.now and time.localTimeIsBetween. SAPL includes a library of built-in PIPs for common authorization needs: time and date functions, string manipulation, filtering, JSON processing, and more.
SAPL’s plugin architecture enables domain-specific authorization. Organizations can implement custom PIPs as plugins to integrate their domain-specific data sources and business logic into authorization policies:
- HR systems and organizational hierarchies
- Scheduling and calendar systems
- Compliance and regulatory engines
- Real-time monitoring and metrics
- Industry-specific business rules
- Any database, API, or system relevant to authorization decisions
Custom PIPs are implemented as plugins to the PDP, requiring no modifications to SAPL core. The schedules PIP in the shift example above would be a custom PIP specific to that organization’s workforce management system.
This plugin architecture enables organizations to create sophisticated authorization logic tailored to their exact business requirements. A hospital can implement medical protocol PIPs, a bank can implement trading rule PIPs, and a manufacturing facility can implement safety certification PIPs - all using the same SAPL policy language.
Implementation details for custom PIPs are covered in Attribute Finders.
Structuring Policy Conditions
All conditions after permit or deny are body statements. While there is no grammar-level distinction between them, it is good practice to put fast, local checks first and slower PIP-based lookups later. This helps the engine skip expensive external calls early when simple conditions already determine the outcome.
Recommended ordering:
- Fast local checks first: Resource type, action, simple equality checks on subscription attributes. These evaluate instantly and can short-circuit the rest.
- PIP-based lookups later: Attribute finder expressions (
<finder.name>) may involve network calls or database queries and should only run when the fast checks have already passed.
Note that &/| and &&/|| are at different precedence levels. && and || bind less tightly than & and |, allowing you to group conditions without parentheses. In future releases, these operator pairs will also select between different asynchronous evaluation strategies.
Policy sets have a dedicated
FORclause that acts as a target expression for filtering which policies in the set are evaluated. See Policy Sets for details.
Functions vs. Attributes
SAPL expressions can call both functions and attributes. They serve fundamentally different purposes:
Functions are pure mappings. Given the same input, a function always returns the same output. They are synchronous, side-effect-free, and evaluated inline. Functions use the dot-call syntax familiar from most languages:
time.dayOfWeek(<time.now>)
filter.blacken(subject.creditCardNumber, 0, 12)
Attributes (accessed through PIPs) are fundamentally different. They represent external, potentially changing state. An attribute lookup may involve a network call, a database query, or a subscription to a live data stream. Attributes are:
- Asynchronous: They may take time to resolve, involving I/O operations.
- Non-idempotent: The same attribute may return different values at different times (the current time, a user’s role after a promotion, a sensor reading).
- Subscription-based: Attributes return reactive streams. The PDP subscribes to them, and the PIP pushes new values whenever the underlying data changes.
This distinction is why attributes use a dedicated syntax (angle brackets <...>) rather than the function call syntax. The angle brackets signal to both the reader and the engine that this expression involves external I/O and may trigger ongoing subscriptions.
Attribute Finder Syntax
SAPL provides two forms of attribute access:
Environment attributes use angle brackets without a base value. They access global or contextual data:
<time.now>
<time.localTimeIsBetween("08:00", "18:00")>
Entity attributes use a dot followed by angle brackets, chaining from a base value. The value on the left is passed to the PIP as context:
subject.username.<user.profile>
"42".<traccar.position>
The head attribute finder syntax |<finder.name> applies an attribute finder to the result of the preceding expression in a pipeline-like fashion.
Parameters
Parameters are additional inputs passed to a PIP inside parentheses. They are positional and can be any SAPL expression, including literals, variables, or function results.
What parameters mean depends on the PIP. Common uses include temporal boundaries, configuration objects, and behavior modifiers:
<time.now(5000)> // update interval in milliseconds
<time.localTimeIsBetween("09:00", "17:00")> // start and end times
<http.get(requestSettings)> // request configuration object
topic.<mqtt.messages(1)> // QoS level
PIPs can be overloaded by parameter count. For example, <time.now> returns the current time with a default update interval, while <time.now(5000)> does the same with a 5-second interval.
Options
Options control the stream infrastructure that wraps every attribute lookup. They are specified in square brackets after the parameters and must be a JSON object:
<http.get(request) [{ "initialTimeOutMs": 500, "retries": 5 }]>
Options are not passed to the PIP itself. Instead, they configure how the engine handles the reactive stream that the PIP returns. The available option fields are:
| Option | Default | Purpose |
|---|---|---|
initialTimeOutMs |
3000 |
Timeout for the first value. Emits undefined if exceeded. |
pollIntervalMs |
30000 |
Re-subscription interval when the PIP stream completes. |
retries |
3 |
Retry attempts with exponential backoff on errors. |
backoffMs |
1000 |
Initial backoff delay between retries. |
fresh |
false |
If true, bypasses the shared stream cache. |
Options follow a three-level priority chain. Policy-level options (in square brackets) override PDP-level defaults (configured in pdp.json under variables.attributeFinderOptions), which override the built-in defaults listed above. This allows operators to tune stream behavior globally without modifying policies, while individual policies can override when needed.
See Attribute Finders for full details on implementing and using PIPs.