Using /v3/roles endpoint to query criteria covered identity assignments

Hello,

I’m developing a plugin for Nexis 4 to import data from ISC**.**

To retrieve all roles, I’m currently using the /v3/roles endpoint. Each role JSON includes a membership element containing both the criteria and the explicitly assigned identities.

However, I’ve noticed that when a role’s membership is determined by a criteria, the identities covered by that criteria are not included in the identities array.

To handle this, I’m currently calling /v3/roles/{id}/assigned-identities for each role to get the complete list of assigned identities.

This works functionally, but I’m concerned about performance, since it requires many API calls — one per role, plus additional pagination calls in some cases.

Is there a more efficient or recommended way to retrieve all role–identity assignments in bulk?

@michelmoegn -

You don’t need to hit /v3/roles/{id}/assigned-identities per role. The scalable pattern can be:

One bulk search over identities with an innerHit on access

Query the identities index and ask for only nested access items where type: ROLE. This returns role assignments for each identity—including criteria-based role grants—so you get the full set of identity↔role pairs in a small number of paged requests. (Search API)

Example (250 per page)

POST /v3/search?limit=250
Content-Type: application/json

{
  "indices": ["identities"],
  "queryType": "SAILPOINT",
  "query": { "query": "*", "fields": [] },
  "innerHit": {
    "type": "access",
    "query": "@access.type:ROLE"
  },
  "queryResultFilter": {
    "includes": [
      "id","name","displayName","email",
      "@access.id","@access.name","@access.displayName","@access.type"
    ]
  }
}

  • Page with searchAfter (preferred for large exports) or offset until no more results. (developer.sailpoint.com)

  • Each hit yields the identity + one or more inner hits for roles → map to (identityId, identityName, roleId, roleName) rows.

If you only want certain roles

Filter the inner hit:

"innerHit": {
  "type": "access",
  "query": "@access.type:ROLE AND @access.id:(\"2c9180...abc\" OR \"2c9180...def\")"
}

Same paging rules apply.

Why this is better than per-role calls

  • Includes criteria-based assignments automatically (the identities index reflects effective access).

  • Far fewer calls: a handful of paged searches vs. N roles × pages on /v3/roles/{id}/assigned-identities. (That endpoint is accurate but inherently O(N) over roles.)

  • Tunable payloads via queryResultFilter.includes keeps responses light.

Caveats & complements

  • Search is eventually consistent; most tenants find the lag small. If you need assignment metadata (who/when/source, request vs. criteria, etc.) per identity, there’s a beta API:
    GET /beta/identities/{identityId}/role-assignments (use sparingly after you’ve discovered the identity set via search). (Get Role Assignment)

  • If you ever must list members for one role (e.g., a quick check), /v3/roles/{id}/assigned-identities is fine—just don’t loop it for every role. (List identities assigned a role)


TL;DR

Use POST /v3/search on the identities index with:
innerHit { type: "access", query: "@access.type:ROLE" }, paginate with searchAfter, and export the inner hits. This yields a complete, bulk role–identity map (including criteria-based memberships) with minimal API calls.

Cheers!!!

4 Likes

Membership via criteria or assigned identities are supposed to mutually exclusive. Hence, you will see only one of them. Identities here are not the ones that would have got the role via membership, but assigned manually under the role

1 Like

Thank you,

It worked with a small adjustment, I had to remove the ‘@’ in the result filter:

{
  "indices": ["identities"],
  "queryType": "SAILPOINT",
  "query": { "query": "*", "fields": [] },
  "innerHit": {
    "type": "access",
    "query": "@access.type:ROLE"
  },
  "queryResultFilter": {
    "includes": [
      "id",
      "name",
      "access.id",
      "access.type"
    ]
  }
}

Best regards,

Michel

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.