DS 7.2.0

Query

Examples in this documentation depend on features activated in the ds-evaluation setup profile.

To search, perform an HTTP GET with a _queryFilter=expression parameter. For details about the query filter expression, see Query.

The _queryId parameter, described in Query, is not used in DS software at present.

The following table shows LDAP search filters and corresponding query filter expressions:

LDAP Filter REST Filter

(&)

_queryFilter=true

(uid=*)

_queryFilter=_id+pr

(uid=bjensen)

_queryFilter=_id+eq+'bjensen'

(uid=*jensen*)

_queryFilter=_id+co+'jensen'

(uid=jensen*)

_queryFilter=_id+sw+'jensen'

(&(uid=*jensen*)(cn=babs*))

_queryFilter=(_id+co+'jensen'+and+displayName+sw+'babs')

(|(uid=*jensen*)(cn=sam*))

_queryFilter=(_id+co+'jensen'+or+displayName+sw+'sam')

(!(uid=*jensen*))

_queryFilter=!(_id+co+'jensen')

(uid<=jensen)

_queryFilter=_id+le+'jensen'

(uid>=jensen)

_queryFilter=_id+ge+'jensen'

For query operations, the filter expression is constructed from the following building blocks. Make sure you URL-encode the filter expressions, which are shown here without URL-encoding to make them easier to read.

In filter expressions, the simplest json-pointer is a field of the JSON resource, such as userName or id. A json-pointer can also point to nested elements, as described in the JSON Pointer Internet-Draft:

Comparison expressions

Build filters using the following comparison expressions:

json-pointer eq json-value

Matches when the pointer equals the value, as in the following example:

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+eq+'babs@example.com'&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "bjensen",
    "_rev" : "<revision>",
    "_schema" : "frapi:opendj:rest2ldap:posixUser:1.0",
    "_meta" : {
      "lastModified" : "<datestamp>"
    },
    "userName" : "babs@example.com",
    "displayName" : [ "Barbara Jensen", "Babs Jensen" ],
    "name" : {
      "givenName" : "Barbara",
      "familyName" : "Jensen"
    },
    "description" : "Original description",
    "manager" : {
      "_id" : "trigden",
      "_rev" : "<revision>"
    },
    "contactInformation" : {
      "telephoneNumber" : "+1 408 555 9999",
      "emailAddress" : "babs@example.com"
    },
    "uidNumber" : 1076,
    "gidNumber" : 1000,
    "homeDirectory" : "/home/bjensen",
    "groups" : [ {
      "_id" : "Carpoolers"
    } ]
  } ],
  "resultCount" : 1,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
json-pointer co json-value

Matches when the pointer contains the value, as in the following example:

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+co+'jensen'&_fields=userName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "ajensen",
    "_rev" : "<revision>",
    "userName" : "ajensen@example.com"
  }, {
    "_id" : "gjensen",
    "_rev" : "<revision>",
    "userName" : "gjensen@example.com"
  }, {
    "_id" : "jjensen",
    "_rev" : "<revision>",
    "userName" : "jjensen@example.com"
  }, {
    "_id" : "kjensen",
    "_rev" : "<revision>",
    "userName" : "kjensen@example.com"
  }, {
    "_id" : "rjensen",
    "_rev" : "<revision>",
    "userName" : "rjensen@example.com"
  }, {
    "_id" : "tjensen",
    "_rev" : "<revision>",
    "userName" : "tjensen@example.com"
  } ],
  "resultCount" : 6,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
json-pointer sw json-value

Matches when the pointer starts with the value, as in the following example:

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+sw+'ab'&_fields=userName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "abarnes",
    "_rev" : "<revision>",
    "userName" : "abarnes@example.com"
  }, {
    "_id" : "abergin",
    "_rev" : "<revision>",
    "userName" : "abergin@example.com"
  } ],
  "resultCount" : 2,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
json-pointer lt json-value

Matches when the pointer is less than the value, as in the following example:

$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+lt+'ac'&_fields=userName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "abarnes",
    "_rev" : "<revision>",
    "userName" : "abarnes@example.com"
  }, {
    "_id" : "abergin",
    "_rev" : "<revision>",
    "userName" : "abergin@example.com"
  } ],
  "resultCount" : 2,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
json-pointer le json-value

Matches when the pointer is less than or equal to the value, as in the following example:

$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+le+'ad'&_fields=userName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "abarnes",
    "_rev" : "<revision>",
    "userName" : "abarnes@example.com"
  }, {
    "_id" : "abergin",
    "_rev" : "<revision>",
    "userName" : "abergin@example.com"
  }, {
    "_id" : "achassin",
    "_rev" : "<revision>",
    "userName" : "achassin@example.com"
  } ],
  "resultCount" : 3,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
json-pointer gt json-value

Matches when the pointer is greater than the value, as in the following example:

$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+gt+'wa'&_fields=userName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "wlutz",
    "_rev" : "<revision>",
    "userName" : "wlutz@example.com"
  } ],
  "resultCount" : 1,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
json-pointer ge json-value

Matches when the pointer is greater than or equal to the value, as in the following example:

$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+ge+'va'&_fields=userName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "wlutz",
    "_rev" : "<revision>",
    "userName" : "wlutz@example.com"
  } ],
  "resultCount" : 1,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
Presence expression

json-pointer pr matches any resource on which the json-pointer is present:

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/groups?_queryFilter=displayName+pr&_fields=displayName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "Accounting Managers",
    "_rev" : "<revision>",
    "displayName" : "Accounting Managers"
  }, {
    "_id" : "Directory Administrators",
    "_rev" : "<revision>",
    "displayName" : "Directory Administrators"
  }, {
    "_id" : "HR Managers",
    "_rev" : "<revision>",
    "displayName" : "HR Managers"
  }, {
    "_id" : "PD Managers",
    "_rev" : "<revision>",
    "displayName" : "PD Managers"
  }, {
    "_id" : "QA Managers",
    "_rev" : "<revision>",
    "displayName" : "QA Managers"
  } ],
  "resultCount" : 5,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
Literal expressions

true matches any resource in the collection.

false matches no resource in the collection.

In other words, you can list all resources in a collection as in the following example:

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/groups?_queryFilter=true&_fields=_id&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "Accounting Managers",
    "_rev" : "<revision>"
  }, {
    "_id" : "Directory Administrators",
    "_rev" : "<revision>"
  }, {
    "_id" : "HR Managers",
    "_rev" : "<revision>"
  }, {
    "_id" : "PD Managers",
    "_rev" : "<revision>"
  }, {
    "_id" : "QA Managers",
    "_rev" : "<revision>"
  } ],
  "resultCount" : 5,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}
Complex expressions

Combine expressions using boolean operators and, or, and ! (not), and by using parentheses (expression) with group expressions. The following example queries resources with last name Jensen and manager name starting with Bar:

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=\
(userName+co+'jensen'+and+manager/displayName+sw+'Sam')&_fields=displayName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "jjensen",
    "_rev" : "<revision>",
    "displayName" : [ "Jody Jensen" ]
  }, {
    "_id" : "tjensen",
    "_rev" : "<revision>",
    "displayName" : [ "Ted Jensen" ]
  } ],
  "resultCount" : 2,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

Notice that the filters use the JSON pointers name/familyName and manager/displayName to identify the fields nested inside the name and manager objects.

Graph-like queries

The default REST to LDAP sample mapping defines references with resourcePath fields. When the REST to LDAP mapping defines references to other resources in this way, the mapping supports recursion. Client applications can use query filters that traverse a "graph" of resources.

The following example gets users whose manager belongs to the Directory Administrators group. The search is not indexed by default, so the directory superuser makes the request:

$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users/?_queryFilter=/manager/groups/_id+eq+'Directory%20Administrators'&_fields=_id&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "ashelton",
    "_rev" : "<revision>"
  }, {
    "_id" : "btalbot",
    "_rev" : "<revision>"
  }, {
    "_id" : "dakers",
    "_rev" : "<revision>"
  }, {
    "_id" : "dsmith",
    "_rev" : "<revision>"
  }, {
    "_id" : "eward",
    "_rev" : "<revision>"
  }, {
    "_id" : "gjensen",
    "_rev" : "<revision>"
  }, {
    "_id" : "hmiller",
    "_rev" : "<revision>"
  }, {
    "_id" : "jburrell",
    "_rev" : "<revision>"
  }, {
    "_id" : "jcampai2",
    "_rev" : "<revision>"
  }, {
    "_id" : "jfalena",
    "_rev" : "<revision>"
  }, {
    "_id" : "jvaughan",
    "_rev" : "<revision>"
  }, {
    "_id" : "kcarter",
    "_rev" : "<revision>"
  }, {
    "_id" : "mreuter",
    "_rev" : "<revision>"
  }, {
    "_id" : "newuser",
    "_rev" : "<revision>"
  }, {
    "_id" : "pworrell",
    "_rev" : "<revision>"
  }, {
    "_id" : "rbannist",
    "_rev" : "<revision>"
  }, {
    "_id" : "rdaugherty",
    "_rev" : "<revision>"
  }, {
    "_id" : "rschneid",
    "_rev" : "<revision>"
  }, {
    "_id" : "striplet",
    "_rev" : "<revision>"
  }, {
    "_id" : "tclow",
    "_rev" : "<revision>"
  }, {
    "_id" : "tmason",
    "_rev" : "<revision>"
  }, {
    "_id" : "tschmith",
    "_rev" : "<revision>"
  }, {
    "_id" : "tward",
    "_rev" : "<revision>"
  } ],
  "resultCount" : 23,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

REST to LDAP translates such graph-like HTTP queries into a series of corresponding LDAP requests. Complex graph-like queries can have a significant server-side performance impact.

Complex JSON queries

The default JSON query index works for JSON attributes that hold arbitrary JSON objects. This includes JSON with nested objects, such as {"array":[{"x":1,"y":2},{"x":3,"y":4}]}.

To try this feature:

  1. Install a DS server with the ds-evaluation setup profile.

  2. Replace the "frapi:opendj:rest2ldap:posixUser:1.0" resource type definition with a resource type for a user with a json attribute in the mapping configuration file, /path/to/opendj/config/rest2ldap/endpoints/api/example-v1.json:

    // A user with a "json" attribute.
    "frapi:opendj:rest2ldap:jsonUser:1.0": {
        "superType": "frapi:opendj:rest2ldap:user:1.0",
        "objectClasses": [ "jsonObject" ],
        "properties": {
            "json": {
                "type": "json",
                "isMultiValued": true
            }
        }
    },

    Alternatively, download jsonUser.json to replace the existing file.

  3. Restart DS to load the new API configuration.

  4. Perform searches using the configuration.

    This search finds an entry with a json attribute that has an "array" field containing an array of objects:

    $ curl \
     --user kvaughan:bribery \
     --cacert ca-cert.pem \
     --silent \
     --get \
     --data-urlencode "_queryFilter=json/array[x eq 1] and json/array[y eq 4]" \
     --data-urlencode "_fields=json" \
     --data-urlencode "_prettyPrint=true" \
     "https://localhost:8443/api/users/"
    
    {
      "result" : [ {
        "_id" : "abarnes",
        "_rev" : "<revision>",
        "json" : [ {
          "array" : [ {
            "x" : 1,
            "y" : 2
          }, {
            "x" : 3,
            "y" : 4
          } ]
        } ]
      } ],
      "resultCount" : 1,
      "pagedResultsCookie" : null,
      "totalPagedResultsPolicy" : "NONE",
      "totalPagedResults" : -1,
      "remainingPagedResults" : -1
    }
    • The filter json/array[x eq 1] and json/array[y eq 4] matches because it matches both objects in the array.

    • The filter json/array[x eq 1 and y eq 2] matches because it matches the first object of the array.

    • The filter json/array[x eq 1 and y eq 4] fails to match, because the array has no object {"x":1,"y":4}.

Paged results

You can page through search results using the following query string parameters that are further described in Query:

  • _pagedResultsCookie=string

  • _pagedResultsOffset=integer

  • _pageSize=integer

The following example demonstrates how paged results are used:

# Request five results per page, and retrieve the first page:
$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=true&_fields=userName&_pageSize=5&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "abarnes",
    "_rev" : "<revision>",
    "userName" : "abarnes@example.com"
  }, {
    "_id" : "abergin",
    "_rev" : "<revision>",
    "userName" : "abergin@example.com"
  }, {
    "_id" : "achassin",
    "_rev" : "<revision>",
    "userName" : "achassin@example.com"
  }, {
    "_id" : "ahall",
    "_rev" : "<revision>",
    "userName" : "ahall@example.com"
  }, {
    "_id" : "ahel",
    "_rev" : "<revision>",
    "userName" : "ahel@example.com"
  } ],
  "resultCount" : 5,
  "pagedResultsCookie" : "<cookie>",
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

$ export COOKIE=$(cut -d \" -f 4 <(grep pagedResultsCookie \
 <(curl --cacert ca-cert.pem \
 --user admin:password \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=true&_fields=userName&_pageSize=5&_prettyPrint=true")))

# Provide the cookie to request the next five results:
$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=true&_fields=userName&_pageSize=5\
&_pagedResultsCookie=$COOKIE&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "ahunter",
    "_rev" : "<revision>",
    "userName" : "ahunter@example.com"
  }, {
    "_id" : "ajensen",
    "_rev" : "<revision>",
    "userName" : "ajensen@example.com"
  }, {
    "_id" : "aknutson",
    "_rev" : "<revision>",
    "userName" : "aknutson@example.com"
  }, {
    "_id" : "alangdon",
    "_rev" : "<revision>",
    "userName" : "alangdon@example.com"
  }, {
    "_id" : "alutz",
    "_rev" : "<revision>",
    "userName" : "alutz@example.com"
  } ],
  "resultCount" : 5,
  "pagedResultsCookie" : "<new-cookie>",
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

# Request the tenth page of five results:
$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=true&_fields=userName\
&_pageSize=5&_pagedResultsOffset=10&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "ashelton",
    "_rev" : "<revision>",
    "userName" : "ashelton@example.com"
  }, {
    "_id" : "awalker",
    "_rev" : "<revision>",
    "userName" : "awalker@example.com"
  }, {
    "_id" : "awhite",
    "_rev" : "<revision>",
    "userName" : "awhite@example.com"
  }, {
    "_id" : "aworrell",
    "_rev" : "<revision>",
    "userName" : "aworrell@example.com"
  }, {
    "_id" : "bfrancis",
    "_rev" : "<revision>",
    "userName" : "bfrancis@example.com"
  } ],
  "resultCount" : 5,
  "pagedResultsCookie" : "<cookie>",
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

The LDAP searches corresponding to the queries in these examples are not indexed. Notice the following features of the responses:

  • "remainingPagedResults" : -1 means that the number of remaining results is not calculated.

  • "totalPagedResults" : -1 means that the total number of paged results is not calculated.

  • "totalPagedResultsPolicy" : "NONE" means that result counting is disabled.

When the LDAP search corresponding to the query is indexed, and _pageSize is set to an integer greater than zero, the response contains an estimated number of "totalPagedResults", and "totalPagedResultsPolicy" : "ESTIMATE":

$ curl \
 --user kvaughan:bribery \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=userName+co+'jensen'&_pageSize=2&_fields=displayName&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "ajensen",
    "_rev" : "<revision>",
    "displayName" : [ "Allison Jensen" ]
  }, {
    "_id" : "gjensen",
    "_rev" : "<revision>",
    "displayName" : [ "Gern Jensen" ]
  } ],
  "resultCount" : 2,
  "pagedResultsCookie" : "<cookie>",
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

Notice that "remainingPagedResults" : -1 in this case, too. REST to LDAP never calculates the value for this field, as doing so would require a potentially costly calculation to determine the current position in the total result set.

The estimated number of results can be useful when the LDAP search uses a big index or a VLV index, and the total number of results is large.

Server-side sort

You can use the _sortKeys parameter, described in Query, to request that the server sort the results it returns.

The following example sorts results by given name:

$ curl \
 --user admin:password \
 --cacert ca-cert.pem \
 --silent \
 "https://localhost:8443/api/users?_queryFilter=name/familyName+eq+'jensen'\
&_sortKeys=name/givenName&_fields=name&_prettyPrint=true"

{
  "result" : [ {
    "_id" : "ajensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Allison",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "bjensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Barbara",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "bjense2",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Bjorn",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "gjensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Gern",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "jjensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Jody",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "kjensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Kurt",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.5814",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Mollie",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.19233",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Molly",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.32652",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Mommy",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.46071",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Mona",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.59490",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Monah",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.72909",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Monica",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.86328",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Moniek",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "user.99747",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Monika",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "rjense2",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Randy",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "rjensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Richard",
      "familyName" : "Jensen"
    }
  }, {
    "_id" : "tjensen",
    "_rev" : "<revision>",
    "name" : {
      "givenName" : "Ted",
      "familyName" : "Jensen"
    }
  } ],
  "resultCount" : 17,
  "pagedResultsCookie" : null,
  "totalPagedResultsPolicy" : "NONE",
  "totalPagedResults" : -1,
  "remainingPagedResults" : -1
}

To sort in reverse order, use _sortKeys=-field.

To specify multiple sort keys, use a comma-separated list of fields.

The sort key fields that you specify must exist in the result entries.

The server must store and then sort the result set for your search. If you expect a large result set for your search, use paged results, described in Paged results, to limit the impact on the server and get your results more quickly.

Copyright © 2010-2022 ForgeRock, all rights reserved.