Skip to content

[CLOUDS-8722] Replace deprecated PIM endpoint with Microsoft Graph API - #250

Open
perzycharles wants to merge 6 commits into
mainfrom
fix/CLOUDS-8722-pim-endpoint
Open

[CLOUDS-8722] Replace deprecated PIM endpoint with Microsoft Graph API#250
perzycharles wants to merge 6 commits into
mainfrom
fix/CLOUDS-8722-pim-endpoint

Conversation

@perzycharles

@perzycharles perzycharles commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Motivation

CLOUDS-8722
https://dd.slack.com/archives/C02FPP4JQHX/p1788163680308149

Summary

  • Replaces the legacy api.azrbac.mspim.azure.com PIM endpoint in get_active_entra_role_ids with two Microsoft Graph API calls
  • The legacy endpoint fails in Azure Cloud Shell because it requires an ARM-audience token (management.core.windows.net) that the PIM endpoint rejects with Unauthorized(BadRequest), and Cloud Shell MSI does not support the correct PIM audience
  • Graph API is natively supported by Cloud Shell MSI and is already used by get_entra_role_permissions in the same file

Two Graph calls replace the single broken PIM call:

  1. roleAssignments — permanent directory role assignments (accessible to all users)
  2. roleAssignmentScheduleInstances — PIM-activated time-bound assignments (requires RoleManagement.Read.Directory; regular users receive Forbidden, caught and treated as empty set — semantically correct since users without elevated roles have no PIM-activated roles)

Test plan

I didn't have issue when executing the script in CloudShell because Users can register applications is enabled in our sandbox.
https://datadoghq.atlassian.net/browse/CLOUDS-8722?focusedCommentId=3601350

However, I had the same Unauthorized error when running the same rest request as our code.
az rest \ --resource "https://management.core.windows.net/" \ -u "https://api.azrbac.mspim.azure.com/api/v2/privilegedAccess/aadroles/roleAssignments?\$select=roleDefinitionId&\$filter=subjectId eq '$(az ad signed-in-user show --query id -o tsv)' and assignmentState eq 'Active'&\$top=999" \ --query "value[].roleDefinitionId"

I also run the same rest requests as the fix to verify. I have no PIM-eligible roles configured, so I was never granted the RoleManagement.Read.Directory permission that comes with elevated Entra roles. This is the expected state for a regular user.

$ USER_ID=$(az ad signed-in-user show --query id -o tsv)

$ az rest \
  -u "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$filter=principalId eq '$USER_ID'&\$select=roleDefinitionId&\$top=999" \
  --query "value[].roleDefinitionId"

$ az rest \
  -u "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances?\$filter=principalId eq '$USER_ID'&\$select=roleDefinitionId&\$top=999" \
  --query "value[].roleDefinitionId"
cloudshell

Fixes: CLOUDS-8722

🤖 Generated with Claude Code

…ctive_entra_role_ids

The legacy api.azrbac.mspim.azure.com endpoint fails in Cloud Shell because
it requires an ARM-audience token (management.core.windows.net) that the PIM
endpoint rejects with Unauthorized/BadRequest, and Cloud Shell MSI does not
support the correct PIM audience directly.

Replace with two Microsoft Graph API calls:
- roleAssignments for permanent directory role assignments
- roleAssignmentScheduleInstances for PIM-activated time-bound assignments

Graph is natively supported by Cloud Shell MSI and is already used by
get_entra_role_permissions in the same file. Regular users without elevated
roles receive Forbidden on roleAssignmentScheduleInstances (missing
RoleManagement.Read.Directory scope), which is caught and treated as an
empty set — semantically correct since such users have no PIM-activated roles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@perzycharles
perzycharles requested a review from a team as a code owner September 4, 2026 06:17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@PCaponetti PCaponetti left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Graph API gets throttled a bunch due to low quota, so adding another call to it is scary. Would it be possible to add a cache to another place where it is called? domains/cloud_platform/azure/apps/resource-collection/.../unifiedroleassignmentscheduleinstances.go is a good candidate.

# PIM-activated (time-bound) assignments. Regular users without elevated roles lack the
# RoleManagement.Read.Directory scope to call this endpoint; Forbidden means no PIM roles.
try:
pim_active = set(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why no tests?

.param("--query", "value[].roleDefinitionId")
)
)
except (AccessError, RuntimeError):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should we have a catch-all as well? I think 404 escapes this, no?

@perzycharles perzycharles left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review! Two points on the throttling concern:

  1. Different quota pool: The throttling in resource-collection is Datadog's backend service hitting Graph continuously on behalf of many customers. This quickstart runs in the customer's own Cloud Shell using their own Azure credentials — it draws from the customer's tenant quota, entirely separate from Datadog's service quota.

  2. The second call is effectively free for most users: Regular users (no elevated Entra roles) get Forbidden on roleAssignmentScheduleInstances immediately since they lack RoleManagement.Read.Directory — no data is fetched. Only users who already hold a privileged Entra role actually execute that call, and the whole function runs at most once per script invocation.

So in practice this adds 0 Graph calls for regular users and 1 call for privileged users, both against the customer's own tenant quota rather than ours.

@perzycharles perzycharles left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review! Two points on the throttling concern:

  1. Different quota pool: The throttling in resource-collection is Datadog's backend service hitting Graph continuously on behalf of many customers. This quickstart runs in the customer's own Cloud Shell using their own Azure credentials — it draws from the customer's tenant quota, entirely separate from Datadog's service quota.

  2. The second call is effectively free for most users: Regular users (no elevated Entra roles) get Forbidden on roleAssignmentScheduleInstances immediately since they lack RoleManagement.Read.Directory — no data is fetched. Only users who already hold a privileged Entra role actually execute that call, and the whole function runs at most once per script invocation.

So in practice this adds 0 Graph calls for regular users and 1 call for privileged users, both against the customer's own tenant quota rather than ours.

…_entra_role_ids

- Broaden exception catch from (AccessError, RuntimeError) to
  (AzIntegrationError, RuntimeError) so ResourceNotFoundError (404),
  InteractiveAuthenticationRequiredError, RefreshTokenError, and other
  AzIntegrationError subclasses are all handled rather than propagating
- Add 7 unit tests covering the happy path, deduplication, and all
  exception types that should be treated as empty PIM role sets
- Rebuild dist bundles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@perzycharles perzycharles left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catches — both addressed in the latest commit:

  1. 404 / catch-all: Changed except (AccessError, RuntimeError) to except (AzIntegrationError, RuntimeError). ResourceNotFoundError (404), InteractiveAuthenticationRequiredError, RefreshTokenError, and all other AzIntegrationError subclasses are now caught. RuntimeError stays alongside it for the generic fallthrough in execute_cmd.py.

  2. Tests: Added 7 unit tests in tests/test_role_assignments.py covering the happy path, deduplication, and each exception type (AccessError, ResourceNotFoundError, RuntimeError) being treated as an empty PIM role set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
perzycharles and others added 2 commits September 10, 2026 10:49
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@perzycharles
perzycharles enabled auto-merge (squash) September 10, 2026 01:51
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.

2 participants