Skip to content

Commit 0130728

Browse files
Copilotvharseko
andauthored
Add onQueryResult script hook to filter managed object query results (#139)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vharseko <6818498+vharseko@users.noreply.github.com> Co-authored-by: Valery Kharseko <vharseko@3a-systems.ru>
1 parent e602515 commit 0130728

5 files changed

Lines changed: 141 additions & 6 deletions

File tree

openidm-core/src/main/java/org/forgerock/openidm/managed/ManagedObjectSet.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* information: "Portions copyright [year] [name of copyright owner]".
1313
*
1414
* Copyright 2011-2016 ForgeRock AS.
15+
* Portions copyright 2026 3A Systems, LLC.
1516
*/
1617
package org.forgerock.openidm.managed;
1718

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

153+
/** Script to execute for each object returned from a query; return false to exclude the object. */
154+
onQueryResult,
155+
152156
/** Script to execute when an object is about to be stored in the repository. */
153157
onStore,
154158

@@ -1286,6 +1290,33 @@ public boolean handleResource(ResourceResponse resource) {
12861290
return false;
12871291
}
12881292
}
1293+
// Execute the onQueryResult script if configured; skip object if it returns a falsy value
1294+
try {
1295+
Object queryResultScriptResult = execScriptHook(managedContext, ScriptHook.onQueryResult,
1296+
resource.getContent(),
1297+
prepareScriptBindings(managedContext, request, resource.getId(),
1298+
new JsonValue(null), new JsonValue(null)));
1299+
// Normalize the script result using simple truthiness semantics:
1300+
// - null (or no return) => include (do not filter)
1301+
// - Boolean false, numeric zero, or empty string => exclude
1302+
if (queryResultScriptResult != null) {
1303+
boolean include = true;
1304+
if (queryResultScriptResult instanceof Boolean) {
1305+
include = (Boolean) queryResultScriptResult;
1306+
} else if (queryResultScriptResult instanceof Number) {
1307+
include = ((Number) queryResultScriptResult).doubleValue() != 0.0d;
1308+
} else if (queryResultScriptResult instanceof CharSequence) {
1309+
include = ((CharSequence) queryResultScriptResult).length() != 0;
1310+
}
1311+
if (!include) {
1312+
// Object excluded by onQueryResult script
1313+
return true;
1314+
}
1315+
}
1316+
} catch (ResourceException e) {
1317+
ex[0] = e;
1318+
return false;
1319+
}
12891320
if (ServerConstants.QUERY_ALL_IDS.equals(request.getQueryId())) {
12901321
// Don't populate relationships if this is a query-all-ids query.
12911322
resourceResponse = resource;

openidm-core/src/test/java/org/forgerock/openidm/managed/ManagedObjectSetTest.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* information: "Portions copyright [year] [name of copyright owner]".
1313
*
1414
* Copyright 2016 ForgeRock AS.
15+
* Portions copyright 2026 3A Systems, LLC.
1516
*/
1617
package org.forgerock.openidm.managed;
1718

@@ -38,6 +39,7 @@
3839
import java.security.KeyStore;
3940
import java.security.KeyStore.PasswordProtection;
4041
import java.security.KeyStore.SecretKeyEntry;
42+
import java.util.ArrayList;
4143
import java.util.Collections;
4244
import java.util.LinkedList;
4345
import java.util.List;
@@ -48,12 +50,14 @@
4850

4951
import com.fasterxml.jackson.databind.ObjectMapper;
5052

53+
import org.forgerock.json.JsonPointer;
5154
import org.forgerock.json.JsonValue;
5255
import org.forgerock.json.crypto.JsonDecryptFunction;
5356
import org.forgerock.json.crypto.simple.SimpleDecryptor;
5457
import org.forgerock.json.crypto.simple.SimpleKeySelector;
5558
import org.forgerock.json.crypto.simple.SimpleKeyStoreSelector;
5659
import org.forgerock.json.resource.MemoryBackend;
60+
import org.forgerock.json.resource.QueryRequest;
5761
import org.forgerock.json.resource.ResourceException;
5862
import org.forgerock.json.resource.ResourceResponse;
5963
import org.forgerock.json.resource.Router;
@@ -78,6 +82,7 @@
7882
import org.forgerock.services.context.RootContext;
7983
import org.forgerock.util.promise.Promise;
8084
import org.forgerock.util.promise.ResultHandler;
85+
import org.forgerock.util.query.QueryFilter;
8186
import org.testng.annotations.Test;
8287
import org.testng.annotations.BeforeClass;
8388

@@ -100,6 +105,7 @@ public class ManagedObjectSetTest {
100105
private static final String CONF_MANAGED_USER_USING_ALIAS1 = "/conf/managed-user-alias1.json";
101106
private static final String CONF_MANAGED_USER_USING_NO_ENCRYPTION = "/conf/managed-user-no-encryption.json";
102107
private static final String CONF_MANAGED_USER_WITH_ACTION = "/conf/managed-user-action.json";
108+
private static final String CONF_MANAGED_USER_WITH_ON_QUERY_RESULT = "/conf/managed-user-on-query-result.json";
103109
private static final String RESOURCE_ID = "user1";
104110
private static final String KEYSTORE_PASSWORD = "Password1";
105111
private static final int NUMBER_OF_USERS = 5;
@@ -481,6 +487,14 @@ private ManagedObjectSet createManagedObjectSet(final String configJson, final C
481487
new NullActivityLogger());
482488
}
483489

490+
private ManagedObjectSet createManagedObjectSetWithScriptRegistry(final String configJson,
491+
final CryptoService cryptoService, final IDMConnectionFactory connectionFactory) throws Exception {
492+
final AtomicReference<RouteService> routeService = new AtomicReference<>(mock(RouteService.class));
493+
final JsonValue config = getResource(configJson);
494+
return new ManagedObjectSet(scriptRegistry, cryptoService, routeService, connectionFactory, config,
495+
new NullActivityLogger());
496+
}
497+
484498
private JsonValue createUser(final String resourceId, final JsonValue userContent,
485499
final ManagedObjectSet managedObjectSet) throws ResourceException {
486500
Promise<ResourceResponse, ResourceException> promise = managedObjectSet.createInstance(new RootContext(),
@@ -537,6 +551,69 @@ List<ResourceException> getErrors() {
537551
}
538552
}
539553

554+
/**
555+
* Tests that when the {@code onQueryResult} script returns {@code false} for an object,
556+
* that object is excluded from the query results.
557+
*/
558+
@Test
559+
public void testQueryCollectionOnQueryResultExcludesObjects() throws Exception {
560+
// given
561+
final CryptoService cryptoService = createCryptoService();
562+
final ConnectionObjects connectionObjects = createConnectionObjects();
563+
final ManagedObjectSet managedObjectSet =
564+
createManagedObjectSetWithScriptRegistry(CONF_MANAGED_USER_WITH_ON_QUERY_RESULT, cryptoService,
565+
connectionObjects.getConnectionFactory());
566+
addRoutesToRouter(connectionObjects.getRouter(), managedObjectSet, new MemoryBackend());
567+
568+
// create 2 active users and 1 inactive user
569+
createUser("activeUser1", createUserObject("activeUser1", true), managedObjectSet);
570+
createUser("activeUser2", createUserObject("activeUser2", true), managedObjectSet);
571+
createUser("inactiveUser", createUserObject("inactiveUser", false), managedObjectSet);
572+
573+
// when: query all users
574+
final List<ResourceResponse> results = new ArrayList<>();
575+
final QueryRequest queryRequest = newQueryRequest(MANAGED_USER_RESOURCE_PATH)
576+
.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue());
577+
managedObjectSet.queryCollection(new RootContext(), queryRequest, results::add)
578+
.getOrThrowUninterruptibly();
579+
580+
// then: only active users are included; inactive user is excluded by onQueryResult
581+
assertThat(results).hasSize(2);
582+
assertThat(results.stream()
583+
.map(r -> r.getContent().get(FIELD_ACTIVE).asBoolean())
584+
.allMatch(Boolean.TRUE::equals)).isTrue();
585+
}
586+
587+
/**
588+
* Tests that when no {@code onQueryResult} script is configured, all objects are returned
589+
* from the query without any filtering.
590+
*/
591+
@Test
592+
public void testQueryCollectionWithoutOnQueryResultIncludesAllObjects() throws Exception {
593+
// given
594+
final CryptoService cryptoService = createCryptoService();
595+
final ConnectionObjects connectionObjects = createConnectionObjects();
596+
final ManagedObjectSet managedObjectSet =
597+
createManagedObjectSet(CONF_MANAGED_USER_USING_NO_ENCRYPTION, cryptoService,
598+
connectionObjects.getConnectionFactory());
599+
addRoutesToRouter(connectionObjects.getRouter(), managedObjectSet, new MemoryBackend());
600+
601+
// create 3 users (mix of active/inactive)
602+
createUser("user0", createUserObject("user0", "pwd0", "user0@example.com"), managedObjectSet);
603+
createUser("user1", createUserObject("user1", "pwd1", "user1@example.com"), managedObjectSet);
604+
createUser("user2", createUserObject("user2", "pwd2", "user2@example.com"), managedObjectSet);
605+
606+
// when: query all users
607+
final List<ResourceResponse> results = new ArrayList<>();
608+
final QueryRequest queryRequest = newQueryRequest(MANAGED_USER_RESOURCE_PATH)
609+
.setQueryFilter(QueryFilter.<JsonPointer>alwaysTrue());
610+
managedObjectSet.queryCollection(new RootContext(), queryRequest, results::add)
611+
.getOrThrowUninterruptibly();
612+
613+
// then: all 3 users are returned (no onQueryResult hook to filter them)
614+
assertThat(results).hasSize(3);
615+
}
616+
540617
private static class ConnectionObjects {
541618
private IDMConnectionFactory connectionFactory;
542619
private Router router;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name" : "user",
3+
"onQueryResult" : {
4+
"type" : "text/javascript",
5+
"source" : "object.active !== false;"
6+
},
7+
"schema" : {
8+
"properties" : {
9+
"_id" : {
10+
"type" : "string"
11+
},
12+
"active" : {
13+
"type" : "boolean"
14+
}
15+
}
16+
}
17+
}

openidm-doc/src/main/asciidoc/integrators-guide/appendix-objects.adoc

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
information: "Portions copyright [year] [name of copyright owner]".
1313

1414
Copyright 2017 ForgeRock AS.
15-
Portions Copyright 2024 3A Systems LLC.
15+
Portions Copyright 2024-2026 3A Systems LLC.
1616
////
1717
1818
:figure-caption!:
@@ -244,9 +244,10 @@ Specifies the configuration of each managed object.
244244
"onDelete" : script object,
245245
"postDelete": script object,
246246
"onValidate": script object,
247-
"onRetrieve": script object,
248-
"onStore" : script object,
249-
"onSync" : script object
247+
"onRetrieve" : script object,
248+
"onQueryResult" : script object,
249+
"onStore" : script object,
250+
"onSync" : script object
250251
}
251252
----
252253
@@ -322,6 +323,12 @@ script object, optional
322323
+
323324
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.
324325
326+
onQueryResult::
327+
script object, optional
328+
329+
+
330+
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.
331+
325332
onStore::
326333
script object, optional
327334

openidm-doc/src/main/asciidoc/integrators-guide/appendix-scripting.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
information: "Portions copyright [year] [name of copyright owner]".
1313

1414
Copyright 2017 ForgeRock AS.
15-
Portions Copyright 2024-2025 3A Systems LLC.
15+
Portions Copyright 2024-2026 3A Systems LLC.
1616
////
1717
1818
:figure-caption!:
@@ -1148,7 +1148,7 @@ condition, transform
11481148
====
11491149
11501150
Scripts called in the managed object configuration (`conf/managed.json`) file::
1151-
onCreate, onRead, onUpdate, onDelete, onValidate, onRetrieve, onStore, onSync, postCreate, postUpdate, and postDelete
1151+
onCreate, onRead, onUpdate, onDelete, onValidate, onRetrieve, onQueryResult, onStore, onSync, postCreate, postUpdate, and postDelete
11521152
11531153
+
11541154
`managed.json` supports only one script per hook. If multiple scripts are defined for the same hook, only the last one is kept.
@@ -1191,6 +1191,9 @@ a|object, oldObject, newObject
11911191
a|onDelete, onRetrieve, onRead
11921192
a|object
11931193
1194+
a|onQueryResult
1195+
a|object
1196+
11941197
a|postDelete
11951198
a|oldObject
11961199

0 commit comments

Comments
 (0)