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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2011-2016 ForgeRock AS.
* Portions copyright 2026 3A Systems, LLC.
*/
package org.forgerock.openidm.managed;

Expand Down Expand Up @@ -149,6 +150,9 @@ private enum ScriptHook {
/** Script to execute once an object is retrieved from the repository. */
onRetrieve,

/** Script to execute for each object returned from a query; return false to exclude the object. */
onQueryResult,

/** Script to execute when an object is about to be stored in the repository. */
onStore,

Expand Down Expand Up @@ -1286,6 +1290,20 @@ public boolean handleResource(ResourceResponse resource) {
return false;
}
}
// Execute the onQueryResult script if configured; skip object if it returns false
try {
Object queryResultScriptResult = execScriptHook(managedContext, ScriptHook.onQueryResult,
resource.getContent(),
prepareScriptBindings(managedContext, request, resource.getId(),
new JsonValue(null), new JsonValue(null)));
if (Boolean.FALSE.equals(queryResultScriptResult)) {
// Object excluded by onQueryResult script
return true;
}
} catch (ResourceException e) {
ex[0] = e;
return false;
}
if (ServerConstants.QUERY_ALL_IDS.equals(request.getQueryId())) {
// Don't populate relationships if this is a query-all-ids query.
resourceResponse = resource;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2016 ForgeRock AS.
* Portions copyright 2026 3A Systems, LLC.
*/
package org.forgerock.openidm.managed;

Expand All @@ -38,6 +39,7 @@
import java.security.KeyStore;
import java.security.KeyStore.PasswordProtection;
import java.security.KeyStore.SecretKeyEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -48,12 +50,15 @@

import com.fasterxml.jackson.databind.ObjectMapper;

import org.forgerock.json.JsonPointer;
import org.forgerock.json.JsonValue;
import org.forgerock.json.crypto.JsonDecryptFunction;
import org.forgerock.json.crypto.simple.SimpleDecryptor;
import org.forgerock.json.crypto.simple.SimpleKeySelector;
import org.forgerock.json.crypto.simple.SimpleKeyStoreSelector;
import org.forgerock.json.resource.MemoryBackend;
import org.forgerock.json.resource.QueryRequest;
import org.forgerock.json.resource.QueryResponse;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.json.resource.Router;
Expand All @@ -78,6 +83,7 @@
import org.forgerock.services.context.RootContext;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.ResultHandler;
import org.forgerock.util.query.QueryFilter;
import org.testng.annotations.Test;
import org.testng.annotations.BeforeClass;

Expand All @@ -100,6 +106,7 @@ public class ManagedObjectSetTest {
private static final String CONF_MANAGED_USER_USING_ALIAS1 = "/conf/managed-user-alias1.json";
private static final String CONF_MANAGED_USER_USING_NO_ENCRYPTION = "/conf/managed-user-no-encryption.json";
private static final String CONF_MANAGED_USER_WITH_ACTION = "/conf/managed-user-action.json";
private static final String CONF_MANAGED_USER_WITH_ON_QUERY_RESULT = "/conf/managed-user-on-query-result.json";
private static final String RESOURCE_ID = "user1";
private static final String KEYSTORE_PASSWORD = "Password1";
private static final int NUMBER_OF_USERS = 5;
Expand Down Expand Up @@ -481,6 +488,14 @@ private ManagedObjectSet createManagedObjectSet(final String configJson, final C
new NullActivityLogger());
}

private ManagedObjectSet createManagedObjectSetWithScriptRegistry(final String configJson,
final CryptoService cryptoService, final IDMConnectionFactory connectionFactory) throws Exception {
final AtomicReference<RouteService> routeService = new AtomicReference<>(mock(RouteService.class));
final JsonValue config = getResource(configJson);
return new ManagedObjectSet(scriptRegistry, cryptoService, routeService, connectionFactory, config,
new NullActivityLogger());
}

private JsonValue createUser(final String resourceId, final JsonValue userContent,
final ManagedObjectSet managedObjectSet) throws ResourceException {
Promise<ResourceResponse, ResourceException> promise = managedObjectSet.createInstance(new RootContext(),
Expand Down Expand Up @@ -537,6 +552,69 @@ List<ResourceException> getErrors() {
}
}

/**
* Tests that when the {@code onQueryResult} script returns {@code false} for an object,
* that object is excluded from the query results.
*/
@Test
public void testQueryCollectionOnQueryResultExcludesObjects() throws Exception {
// given
final CryptoService cryptoService = createCryptoService();
final ConnectionObjects connectionObjects = createConnectionObjects();
final ManagedObjectSet managedObjectSet =
createManagedObjectSetWithScriptRegistry(CONF_MANAGED_USER_WITH_ON_QUERY_RESULT, cryptoService,
connectionObjects.getConnectionFactory());
addRoutesToRouter(connectionObjects.getRouter(), managedObjectSet, new MemoryBackend());

// create 2 active users and 1 inactive user
createUser("activeUser1", createUserObject("activeUser1", true), managedObjectSet);
createUser("activeUser2", createUserObject("activeUser2", true), managedObjectSet);
createUser("inactiveUser", createUserObject("inactiveUser", false), managedObjectSet);

// when: query all users
final List<ResourceResponse> results = new ArrayList<>();
final QueryRequest queryRequest = newQueryRequest(MANAGED_USER_RESOURCE_PATH)
.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue());
managedObjectSet.queryCollection(new RootContext(), queryRequest, results::add)
.getOrThrowUninterruptibly();

// then: only active users are included; inactive user is excluded by onQueryResult
assertThat(results).hasSize(2);
assertThat(results.stream()
.map(r -> r.getContent().get(FIELD_ACTIVE).asBoolean())
.allMatch(Boolean.TRUE::equals)).isTrue();
}

/**
* Tests that when no {@code onQueryResult} script is configured, all objects are returned
* from the query without any filtering.
*/
@Test
public void testQueryCollectionWithoutOnQueryResultIncludesAllObjects() throws Exception {
// given
final CryptoService cryptoService = createCryptoService();
final ConnectionObjects connectionObjects = createConnectionObjects();
final ManagedObjectSet managedObjectSet =
createManagedObjectSet(CONF_MANAGED_USER_USING_NO_ENCRYPTION, cryptoService,
connectionObjects.getConnectionFactory());
addRoutesToRouter(connectionObjects.getRouter(), managedObjectSet, new MemoryBackend());

// create 3 users (mix of active/inactive)
createUser("user0", createUserObject("user0", "pwd0", "user0@example.com"), managedObjectSet);
createUser("user1", createUserObject("user1", "pwd1", "user1@example.com"), managedObjectSet);
createUser("user2", createUserObject("user2", "pwd2", "user2@example.com"), managedObjectSet);

// when: query all users
final List<ResourceResponse> results = new ArrayList<>();
final QueryRequest queryRequest = newQueryRequest(MANAGED_USER_RESOURCE_PATH)
.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue());
managedObjectSet.queryCollection(new RootContext(), queryRequest, results::add)
.getOrThrowUninterruptibly();

// then: all 3 users are returned (no onQueryResult hook to filter them)
assertThat(results).hasSize(3);
}

private static class ConnectionObjects {
private IDMConnectionFactory connectionFactory;
private Router router;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name" : "user",
"onQueryResult" : {
"type" : "text/javascript",
"source" : "object.active !== false;"
},
"schema" : {
"properties" : {
"_id" : {
"type" : "string"
},
"active" : {
"type" : "boolean"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
information: "Portions copyright [year] [name of copyright owner]".

Copyright 2017 ForgeRock AS.
Portions Copyright 2024 3A Systems LLC.
Portions Copyright 2024, 2026 3A Systems LLC.
////
:figure-caption!:
Expand Down Expand Up @@ -244,9 +244,10 @@ Specifies the configuration of each managed object.
"onDelete" : script object,
"postDelete": script object,
"onValidate": script object,
"onRetrieve": script object,
"onStore" : script object,
"onSync" : script object
"onRetrieve" : script object,
"onQueryResult" : script object,
"onStore" : script object,
"onSync" : script object
}
----
Expand Down Expand Up @@ -322,6 +323,12 @@ script object, optional
+
A script object to trigger when an object is retrieved from the repository. The object that was retrieved is provided in the root scope as an `object` property. The script can change the object. If an exception is thrown, then object retrieval fails.
onQueryResult::
script object, optional
+
A script object to trigger for each object returned from a query. The object being evaluated is provided in the root scope as an `object` property. The script should return `true` (or a truthy value) to include the object in the query results, or `false` to exclude it. If an exception is thrown, the query fails.
onStore::
script object, optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
information: "Portions copyright [year] [name of copyright owner]".

Copyright 2017 ForgeRock AS.
Portions Copyright 2024-2025 3A Systems LLC.
Portions Copyright 2024-2026 3A Systems LLC.
////
:figure-caption!:
Expand Down Expand Up @@ -1148,7 +1148,7 @@ condition, transform
====
Scripts called in the managed object configuration (`conf/managed.json`) file::
onCreate, onRead, onUpdate, onDelete, onValidate, onRetrieve, onStore, onSync, postCreate, postUpdate, and postDelete
onCreate, onRead, onUpdate, onDelete, onValidate, onRetrieve, onQueryResult, onStore, onSync, postCreate, postUpdate, and postDelete
+
`managed.json` supports only one script per hook. If multiple scripts are defined for the same hook, only the last one is kept.
Expand Down Expand Up @@ -1191,6 +1191,9 @@ a|object, oldObject, newObject
a|onDelete, onRetrieve, onRead
a|object
a|onQueryResult
a|object
a|postDelete
a|oldObject
Expand Down
Loading