Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ Permit, else rejects it. For more information, see the Javadoc of
[RESTfulPdpBasedAuthzInterceptorTest](webapp/src/test/java/org/ow2/authzforce/webapp/test/pep/cxf/RESTfulPdpBasedAuthzInterceptorTest.java).


### PAP change listener

For optional notifications of successful policy, PDP configuration and
attribute-provider configuration changes, see
[PAP change notifications](docs/pap-change-listener.md).

## Testing

To run unit tests, install Maven and type
Expand Down
4 changes: 3 additions & 1 deletion dist/src/webapp-context.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
override="false" />

<!-- <env-entry>s in web.xml do not override entries below iff override=false. -->
<Environment name="org.ow2.authzforce.rest.service.PapChangeListener.class"
description="Fully qualified PAP change listener Java class with a public no-argument constructor (empty string disables notifications)"
type="java.lang.String" value="" override="false" />
<Environment name="org.ow2.authzforce.config.dir" value="file:/opt/${productId}/conf" type="java.lang.String" override="false"
description="Configuration directory path that may contain \${...} placeholders, to be resolved as system properties: e.g. \${user.dir}. Default values can be supplied using the ':' separator between key and value (see org.springframework.util.SystemPropertyUtils class)" />

Expand Down Expand Up @@ -83,5 +86,4 @@
type="java.lang.Integer"
value="134217728"
override="false" />

</Context>
117 changes: 117 additions & 0 deletions docs/pap-change-listener.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# PAP change notifications

AuthzForce Server can notify an optional Java listener after successful PAP API
operations. No listener is configured by default. This extension does not require
additional runtime libraries in the standard server distribution.

## Contract

Implement `org.ow2.authzforce.rest.service.PapChangeListener`:

```java
package example;

import org.ow2.authzforce.rest.service.PapChangeEvent;
import org.ow2.authzforce.rest.service.PapChangeListener;

public final class MyPapListener implements PapChangeListener {
public MyPapListener() { }

@Override
public void papChanged(PapChangeEvent event) {
// Record the notification in your integration's persistent queue.
// A separate worker can publish it to a broker.
}
}
```

Compile the extension against the matching version of
`org.ow2.authzforce:authzforce-ce-server-rest-service` (normally with Maven
`provided` scope). Do not bundle a second copy of the server API classes.
The listener must have a public no-argument constructor. The server creates one
instance per web application, using its classloader; configuration of the
listener's own infrastructure is the extension's responsibility.

Callbacks run synchronously on request threads. The implementation must be
thread-safe and finish promptly; its duration adds to PAP request latency.
Concurrent operations may produce callbacks in a different order from storage
mutations. An event UUID and timestamp are not a domain revision.

Runtime exceptions from the callback are logged and do not turn an already
successful PAP operation into an HTTP failure. Fatal JVM errors are not caught.
If the listener implements `AutoCloseable`, Spring closes it when the web
application context is destroyed. Coordinate shutdown of any workers owned by
the extension.

## Install and configure

1. Put the extension JAR and its additional runtime dependencies on the web
application's classpath, for example in
`/opt/authzforce-ce-server/webapp/WEB-INF/lib`.
2. Set the following entry in the Tomcat context configuration (for example
`/etc/tomcat11/Catalina/localhost/authzforce-ce.xml`):

```xml
<Environment
name="org.ow2.authzforce.rest.service.PapChangeListener.class"
description="Fully qualified PAP change listener Java class"
type="java.lang.String"
value="example.MyPapListener"
override="false" />
```

3. Redeploy/restart the web application.

An absent or empty entry disables notifications. A nonempty entry naming an
unavailable class, a class that does not implement the interface, or a listener
that cannot be constructed causes startup to fail. This avoids silently running
without an explicitly configured integration.

Changing the extension requires building its JAR and updating the deployment,
not recompiling AuthzForce source code. This is not hot loading. Review
compatibility with the listener API when upgrading the server.

## Events

All events contain an event UUID, creation timestamp, domain ID and operation.
Policy identifiers are supplied only where applicable:

| Operation | Policy ID | Version |
| --- | --- | --- |
| `POLICY_VERSION_ADDED` | Added policy | Added version |
| `POLICY_VERSION_DELETED` | Affected policy | Actual deleted version, including when requested through `latest` |
| `POLICY_DELETED` | Removed policy | Absent; operation covers all removed versions |
| `PDP_CONFIGURATION_CHANGED` | Absent | Absent |
| `ATTRIBUTE_PROVIDERS_CHANGED` | Absent | Absent |
| `DOMAIN_DELETED` | Absent | Absent |

Notifications describe successful operations, not a computed content diff.
A successful configuration call may therefore notify even when values are
unchanged. Adding a policy version does not necessarily activate it.
Provider events concern configuration, not changes in data queried from
external sources. Events do not contain policy XML or provider configuration,
which could include sensitive information.

No successful-change event is emitted for a rejected operation, conflicting
policy version, or not-found deletion. Domain creation, domain metadata and
repository-limit updates are outside this initial contract. Automatic version
rolling does not produce separate deletion notifications. Direct filesystem
changes and synchronization are not intercepted by the API hooks.

## Delivery and downstream recovery

The callback is best effort, with no retries or transactional outbox in the
server. A crash after a policy is persisted but before the event is recorded can
lose a notification. A callback failure does not roll back the policy.

External integrations may provide durable event queues, asynchronous publishing,
retry and duplicate handling. Such a queue is not atomic with policy storage.
Startup and periodic reconciliation against the PAP can recover the current
state after missed notifications, but not necessarily every intermediate change.
This entails a period of possible staleness, including delayed revocations.

Each embedded PDP instance must recover a coherent set of policies, root
selection and relevant provider configuration. Provider implementation JARs must
already be available to that instance. Building and safely replacing the embedded
PDP, instance fan-out and reconciliation are responsibilities of the downstream
integration, not features introduced by this extension point.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2012-2026 THALES.
*
* This file is part of AuthzForce CE.
*
* AuthzForce CE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AuthzForce CE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AuthzForce CE. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ow2.authzforce.rest.service;

import java.time.Instant;
import java.util.Objects;
import java.util.UUID;

/**
* Successful PAP API operation; does not imply activation of an added policy version.
* Identifiers and timestamps are not a domain revision or an ordering guarantee.
* @param eventId unique notification identifier
* @param timestamp notification creation time
* @param domainId affected domain
* @param operation successful operation
* @param policyId affected policy, or null for domain/configuration operations
* @param policyVersion affected version, or null for whole-policy/domain/configuration operations
*/
public record PapChangeEvent(UUID eventId, Instant timestamp, String domainId,
Operation operation, String policyId, String policyVersion)
{
/** Supported PAP operations. Automatic rolling and direct file edits are not separate events. */
public enum Operation
{
/** A policy version was added. */
POLICY_VERSION_ADDED,
/** A specific policy version was removed. */
POLICY_VERSION_DELETED,
/** A policy and its versions were removed. */
POLICY_DELETED,
/** PDP configuration, including root policy selection, was updated. */
PDP_CONFIGURATION_CHANGED,
/** Attribute-provider configuration was updated, not its external source data. */
ATTRIBUTE_PROVIDERS_CHANGED,
/** A domain was removed. */
DOMAIN_DELETED
}

/** Validate mandatory metadata and the operation-specific policy identifiers. */
public PapChangeEvent
{
Objects.requireNonNull(eventId, "eventId");
Objects.requireNonNull(timestamp, "timestamp");
Objects.requireNonNull(domainId, "domainId");
Objects.requireNonNull(operation, "operation");
switch (operation)
{
case POLICY_VERSION_ADDED, POLICY_VERSION_DELETED -> {
Objects.requireNonNull(policyId, "policyId");
Objects.requireNonNull(policyVersion, "policyVersion");
}
case POLICY_DELETED -> {
Objects.requireNonNull(policyId, "policyId");
if (policyVersion != null) throw new IllegalArgumentException("Whole-policy event cannot specify a version");
}
default -> {
if (policyId != null || policyVersion != null)
throw new IllegalArgumentException("Configuration/domain event cannot specify a policy");
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2012-2026 THALES.
*
* This file is part of AuthzForce CE.
*
* AuthzForce CE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AuthzForce CE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AuthzForce CE. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ow2.authzforce.rest.service;

/**
* Optional notification of successful PAP API operations.
* Implementations must be thread-safe and return promptly: callbacks run synchronously
* on request threads and may run concurrently or arrive out of mutation order.
* Runtime exceptions are logged without changing the successful PAP response.
* Delivery is best effort, not transactional with policy persistence.
*/
@FunctionalInterface
public interface PapChangeListener
{
/**
* Handle a successful PAP operation.
* @param event immutable operation metadata (no policy or provider configuration payload)
*/
void papChanged(PapChangeEvent event);
}

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.ow2.authzforce.xmlns.pdp.ext.AbstractAttributeProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.ow2.authzforce.rest.service.PapChangeEvent.Operation;
import org.w3._2005.atom.Link;
import org.w3._2005.atom.Relation;

Expand Down Expand Up @@ -128,14 +129,29 @@ public PolicyDaoClient.Factory<PolicyVersionResourceImpl, PolicyResourceImpl> ge

private final String domainId;
private final DAO domainDAO;
private final PapChangeNotifier notifier;

private DomainResourceImpl(final String domainId, final Builder<DAO> domainDaoBuilder) throws IOException
{
assert domainDaoBuilder != null;
this.domainId = domainId;
this.domainDAO = domainDaoBuilder.build();
this.notifier = PapChangeNotifier.NONE;
}

private DomainResourceImpl(String domainId, DAO domainDAO, PapChangeNotifier notifier)
{
this.domainId = domainId;
this.domainDAO = domainDAO;
this.notifier = notifier;
}

// Keep DAO-cached clients untouched: request views carry an immutable listener context.
DomainResourceImpl<DAO> withNotifier(PapChangeNotifier notifier)
{
return new DomainResourceImpl<>(domainId, domainDAO, notifier);
}

@Override
public Domain getDomain()
{
Expand Down Expand Up @@ -227,6 +243,7 @@ public DomainProperties deleteDomain()
throw NOT_FOUND_EXCEPTION;
}

notifier.notifyChange(Operation.DOMAIN_DELETED, null, null);
return new DomainProperties(props.getDescription(), props.getExternalId());
}

Expand Down Expand Up @@ -427,6 +444,7 @@ public AttributeProviders updateAttributeProviderList(final AttributeProviders a
throw new BadRequestException(e);
}

notifier.notifyChange(Operation.ATTRIBUTE_PROVIDERS_CHANGED, null, null);
return attributeproviders;
}

Expand Down Expand Up @@ -472,6 +490,7 @@ public Link addPolicy(final PolicySet policy)
throw ADD_POLICY_CONFLICT_EXCEPTION;
}

notifier.notifyChange(Operation.POLICY_VERSION_ADDED, policy.getPolicySetId(), policy.getVersion());
// Policy ID is xs:anyURI, therefore may contain invalid characters for
// URL paths -> needs escaping to be used as URL path segment
final String policyIdUrlPathSegment = URL_PATH_SEGMENT_ESCAPER.escape(policy.getPolicySetId());
Expand All @@ -486,13 +505,13 @@ public PolicyResource getPolicyResource(final String policyId)
throw INVALID_ARG_BAD_REQUEST_EXCEPTION;
}

final PolicyResource policyRes = domainDAO.getPolicyDaoClient(policyId);
final PolicyResourceImpl policyRes = domainDAO.getPolicyDaoClient(policyId);
if (policyRes == null)
{
throw NOT_FOUND_EXCEPTION;
}

return policyRes;
return notifier == PapChangeNotifier.NONE ? policyRes : policyRes.withNotifier(notifier);
}

@Override
Expand Down Expand Up @@ -591,6 +610,7 @@ public PdpProperties updateOtherPdpProperties(final PdpPropertiesUpdate properti
throw new BadRequestException(e);
}

notifier.notifyChange(Operation.PDP_CONFIGURATION_CHANGED, null, null);
final GregorianCalendar cal = new GregorianCalendar(UTC_TZ);
cal.setTimeInMillis(allProps.getLastModified());

Expand Down Expand Up @@ -666,4 +686,4 @@ public PrpProperties getOtherPrpProperties()
return new PrpProperties(mpc, mvc, props.isVersionRollingEnabled());
}

}
}
Loading