Skip to content

Add onQueryResult script hook to filter managed object query results#139

Open
Copilot wants to merge 7 commits intomasterfrom
copilot/add-on-query-result-script-hook
Open

Add onQueryResult script hook to filter managed object query results#139
Copilot wants to merge 7 commits intomasterfrom
copilot/add-on-query-result-script-hook

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

  • ManagedObjectSetTest.java — already had Portions copyright 2026 3A Systems, LLC.
  • ManagedObjectSet.java — added Portions copyright 2026 3A Systems, LLC.
  • appendix-objects.adoc — updated to Portions Copyright 2024, 2026 3A Systems LLC.
  • appendix-scripting.adoc — updated to Portions Copyright 2024-2026 3A Systems LLC.
  • managed-user-on-query-result.json — new JSON file, no comment syntax possible
  • ManagedObjectSet.java — replaced Boolean.FALSE.equals() check with full truthiness semantics (null=include, Boolean false/Number 0/empty CharSequence=exclude)
  • ManagedObjectSetTest.java — removed unused QueryResponse import
Original prompt

Context

Discussion #131 requests the ability to filter individual objects returned from a query on managed objects, similar to how validSource / validTarget work in sync.json mappings. Currently, there is no way to include/exclude specific objects from query results based on custom scripted logic (e.g., access control based on the requester's attributes).

What needs to be implemented

Add a new onQueryResult script hook to the ManagedObjectSet class in openidm-core/src/main/java/org/forgerock/openidm/managed/ManagedObjectSet.java.

Behaviour

  • The new hook should be called once per object returned from a query in the queryCollection() method (lines ~1275–1309).
  • The script receives the current query result object (as object) plus standard bindings: context, request, resourceName.
  • If the script returns false (or a falsy value), the object is excluded from the result set and NOT passed to handler.handleResource(...).
  • If the script returns true (or a truthy value, or null — i.e., when no hook is configured), the object is included as normal.
  • If the script throws an exception, it should be treated as an error and propagate correctly (set ex[0] and return false from handleResource).

Changes required

1. ManagedObjectSet.java

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

  1. Add onQueryResult to the ScriptHook enum (after onRetrieve, line ~150):
/** Script to execute for each object returned from a query; return false to exclude the object. */
onQueryResult,
  1. In queryCollection(), inside the QueryResourceHandler.handleResource() anonymous class, after the onRetrieve block and before/after relationship population, invoke the new hook and skip the object if the result is Boolean.FALSE:
// Execute the onQueryResult script if configured; skip object if it returns false
Object queryResultScriptResult = null;
try {
    queryResultScriptResult = execScriptHook(managedContext, ScriptHook.onQueryResult,
            resource.getContent(),
            prepareScriptBindings(managedContext, request, resource.getId(),
                    new JsonValue(null), new JsonValue(null)));
} catch (ResourceException e) {
    ex[0] = e;
    return false;
}
if (Boolean.FALSE.equals(queryResultScriptResult)) {
    // Object excluded by onQueryResult script
    return true; // continue iteration but skip this object
}

This block should be inserted after the onRetrieve block and before the relationship population and handler.handleResource(...) call.

2. Documentation update

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

Add documentation for the new onQueryResult hook in the managed objects script hooks section, similar to how onRetrieve is documented.

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

Add onQueryResult to the script triggers table for managed.json (the table that lists onCreate, onUpdate, onRetrieve, etc. with their available variables).

Example configuration (managed.json)

After this change, users will be able to configure the hook like this:

{
  "name": "user",
  "onQueryResult": {
    "type": "text/javascript",
    "source": "var requester = context.security.authenticationId; object.owner === requester || requester === 'admin';"
  }
}

The script has access to:

  • object — the managed object being evaluated
  • context — the current security/request context
  • request — the query request

Return true to include the object in results, false to exclude it.

Files to change

  • openidm-core/src/main/java/org/forgerock/openidm/managed/ManagedObjectSet.java
  • openidm-doc/src/main/asciidoc/integrators-guide/appendix-objects.adoc
  • openidm-doc/src/main/asciidoc/integrators-guide/appendix-scripting.adoc

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: подготовь ответ и согласуй со мной

Assistant: Вот подготовленный ответ на дискуссию #131. Пожалуйста, проверьте и согласуйте:


Предлагаемый ответ на дискуссию:


Thank you for the suggestion! This is a valid use case — filtering query results per-requester based on access control logic, similar to how validSource/validTarget work in sync mappings.

Current workaround options:

  1. onRetrieve script hook — already available in managed.json, it runs for every object returned from a query (when executeOnRetrieve=true is passed). You can use it to modify the returned object (e.g., strip sensitive fields), but it cannot currently exclude an object from the result set — throwing an exception there would fail the entire query.

  2. Router filter (router.json) — you can intercept query responses via onResponse hook in conf/router.json and post-process the result list, filtering out objec...

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Add onQueryResult script hook to ManagedObjectSet for filtering Add onQueryResult script hook to filter managed object query results Mar 26, 2026
Copilot AI requested a review from vharseko March 26, 2026 19:01
@vharseko vharseko requested a review from maximthomas March 26, 2026 19:01
@vharseko vharseko marked this pull request as ready for review March 27, 2026 05:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new managed object scripting hook to allow per-object filtering of managed object query results, enabling custom access-control and result-shaping logic during queryCollection().

Changes:

  • Added a new onQueryResult managed object script hook and invoked it once per query result to optionally exclude objects.
  • Updated integrators guide documentation to describe onQueryResult and list it among managed.json triggers/variables.
  • Added test coverage and a new managed object config fixture demonstrating onQueryResult filtering.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
openidm-core/src/main/java/org/forgerock/openidm/managed/ManagedObjectSet.java Introduces onQueryResult hook and executes it per query result to skip excluded resources.
openidm-core/src/test/java/org/forgerock/openidm/managed/ManagedObjectSetTest.java Adds tests verifying filtering behavior with/without the new hook.
openidm-core/src/test/resources/conf/managed-user-on-query-result.json Adds a managed object config fixture enabling onQueryResult filtering in tests.
openidm-doc/src/main/asciidoc/integrators-guide/appendix-objects.adoc Documents the new onQueryResult hook in managed object configuration.
openidm-doc/src/main/asciidoc/integrators-guide/appendix-scripting.adoc Adds onQueryResult to the managed.json script trigger listings and variables table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1293 to +1301
// 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;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onQueryResult filtering only excludes when the script returns Boolean.FALSE. The docs/PR intent says a “falsy value” should also exclude; for example a JavaScript hook returning 0, "", or undefined will currently not be filtered. Consider either (a) normalizing the script result to a boolean using truthiness semantics (treat null/undefined as include, falsy as exclude), or (b) enforcing a strict boolean return (and updating the docs accordingly) to avoid surprising behavior.

Suggested change
// 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;
// Execute the onQueryResult script if configured; skip object if it returns a falsy value
try {
Object queryResultScriptResult = execScriptHook(managedContext, ScriptHook.onQueryResult,
resource.getContent(),
prepareScriptBindings(managedContext, request, resource.getId(),
new JsonValue(null), new JsonValue(null)));
// Normalize the script result using simple truthiness semantics:
// - null (or no return) => include (do not filter)
// - Boolean false, numeric zero, or empty string => exclude
if (queryResultScriptResult != null) {
boolean include = true;
if (queryResultScriptResult instanceof Boolean) {
include = ((Boolean) queryResultScriptResult).booleanValue();
} else if (queryResultScriptResult instanceof Number) {
include = ((Number) queryResultScriptResult).doubleValue() != 0.0d;
} else if (queryResultScriptResult instanceof CharSequence) {
include = ((CharSequence) queryResultScriptResult).length() != 0;
}
if (!include) {
// Object excluded by onQueryResult script
return true;
}

Copilot uses AI. Check for mistakes.
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;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryResponse is imported but not used, which will fail builds that enforce unused-import checks (and adds noise for readers). Please remove the unused import.

Suggested change
import org.forgerock.json.resource.QueryResponse;

Copilot uses AI. Check for mistakes.
@vharseko
Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

…ove unused QueryResponse import

Agent-Logs-Url: https://github.com/OpenIdentityPlatform/OpenIDM/sessions/8e8a7bc1-be7c-4fc8-92df-839f5a0defe5

Co-authored-by: vharseko <6818498+vharseko@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 30, 2026

@copilot apply changes based on the comments in this thread

Done in commit 4ed99a7:

  • ManagedObjectSet.java: Replaced Boolean.FALSE.equals() with full truthiness semantics — null/no-return includes the object; Boolean false, numeric 0, or empty CharSequence excludes it.
  • ManagedObjectSetTest.java: Removed the unused QueryResponse import.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • central.sonatype.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.14/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.14/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.14 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.14/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/OpenIDM/OpenIDM org.codehaus.plexus.classworlds.launcher.Launcher -pl openidm-core test -Dtest=ManagedObjectSetTest -q (dns block)
  • repository.jboss.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --enable-native-access=ALL-UNNAMED -classpath /usr/share/apache-maven-3.9.14/boot/plexus-classworlds-2.9.0.jar -Dclassworlds.conf=/usr/share/apache-maven-3.9.14/bin/m2.conf -Dmaven.home=/usr/share/apache-maven-3.9.14 -Dlibrary.jansi.path=/usr/share/apache-maven-3.9.14/lib/jansi-native -Dmaven.multiModuleProjectDirectory=/home/REDACTED/work/OpenIDM/OpenIDM org.codehaus.plexus.classworlds.launcher.Launcher -pl openidm-core test -Dtest=ManagedObjectSetTest -q (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants