Interactive CLI to activate Azure PIM group memberships and approve PIM group requests
Project description
azure-pim-cli
Activate eligible Azure PIM group memberships and approve pending PIM group requests you can approve, from one interactive picker.
Uses a real browser session (Playwright over CDP) to grab a Microsoft Graph
bearer token from an authenticated Azure Portal, bypassing the Conditional
Access blocks that break Connect-MgGraph / az CLI / Cloud Shell in the
tenant.
Windows only. Relies on
robocopy,taskkill, and Chrome paths specific to Windows.
Install
pip install azure-pim-cli
playwright install chromium
First run
pim-activate
What happens:
--auto-cdp(on by default) kills any running Chrome, copies your real Chrome profile toC:\temp\chrome_pim_profile(preserves Intune compliance cookies), launches Chrome with--remote-debugging-port=9222.- Playwright attaches over CDP to that Chrome and opens the PIM Approvals blade.
- If you're not signed in, complete portal SSO in the Chrome window that pops up. The script waits (up to 300s).
- The script sniffs a
graph.microsoft.comBearer token from the portal session, or scrapes MSAL storage on an already-authenticated tab. - Prints pending approvals (if any) and eligibilities, then shows the interactive checkbox picker.
Subsequent runs skip login (session cookies persist in the copied Chrome profile).
Typical output flow
[chrome] launching Chrome with debug port 9222 ...
[token] captured via storage-existing-tab (expires in ~40m, ...)
User: user@example.com (05a0a5ea-...)
Fetching pending PIM group approvals (Graph)...
>> 2 pending approval(s) awaiting your decision.
Fetching eligible PIM group assignments...
>> 3 NEW eligible group(s) since last fetch!
? Select PIM items to activate/approve:
❯ ◯ APPROVE sg-prj-ExampleProject-... <- user@example.com PT2H
◯ ACTIVATE *NEW* sg-app-reporting-reader (member) max 8h MFA=True
◯ ACTIVATE sg-db-warehouse-contributor (member) max 8h MFA=True
...
If no approvals are pending you'll see:
[dim]No pending approvals for you.[/dim]
and it moves straight to the eligibilities list.
Common commands
# Combined interactive picker — approvals FIRST, then eligibilities
pim-activate
# List everything, no action (approvals + eligibilities tables)
pim-activate --list-only
# Refresh 24h eligibility cache
pim-activate --refresh
# Only my eligibilities (skip approvals feed)
pim-activate --eligibilities-only
# Only pending approvals I can approve
pim-activate --approvals-only
# Skip Playwright entirely — paste a Graph bearer token from DevTools
pim-activate --token "eyJ0eXAi..."
Bulk activation via --group regex
--group REGEX skips the picker and activates every eligibility whose
displayName matches (case-insensitive). Combine with --parallel N for
concurrent POSTs.
# Activate ALL sg-* eligibilities, 8 workers, 8h
pim-activate --group "^sg-" --justification "bulk activate" --hours 8 --eligibilities-only --parallel 8
# Only analytics groups
pim-activate --group "-app-" --justification "analytics work" --hours 4 --parallel 4
# Dry-run (list matches, no action)
pim-activate --group "db-warehouse" --list-only
- Regex matches
displayNameon both eligibilities and pending approvals. - Groups already active are auto-skipped (avoids
PendingRoleAssignmentRequesterrors on reruns). --paralleldefault is 8. Higher risks Graph 429.- Polls each request up to 180s;
Timeoutstatus usually still succeeds — verify with--list-only.
Auto-CDP flags
--auto-cdp is on by default and handles the Chrome profile copy + debug-port
launch automatically. Override when needed:
# Disable auto-cdp (attach to a manually-launched debug Chrome, or use bundled Playwright)
pim-activate --no-auto-cdp
# Force re-copy real Chrome profile (e.g. cookies stale)
pim-activate --refresh-chrome-profile
# Non-default debug port
pim-activate --auto-cdp-port 9333
# Non-default profile-copy target
pim-activate --auto-cdp-profile "D:\pim_chrome"
# Attach to already-running debug Chrome instead
pim-activate --no-auto-cdp --cdp-endpoint "http://localhost:9222"
All flags
| Flag | Default | Description |
|---|---|---|
--refresh |
off | Bypass 24h eligibility cache |
--list-only |
off | Show tables, no activation/approval |
--group REGEX |
- | Filter both feeds by displayName |
--justification TEXT |
- | Prompted if omitted |
--hours N |
8 | Duration (clamped per group policy) |
--ticket N |
- | Ticket number for policies that require it |
--approvals-only |
off | Skip eligibilities feed |
--eligibilities-only |
off | Skip pending approvals feed |
--headless |
off | Run Chromium headless (needs prior login) |
--keep-open |
off | Keep browser alive after token grab (debug) |
--token TOKEN |
- | Skip Playwright; use this Graph bearer token |
--parallel N |
8 | Concurrent workers for bulk actions |
--channel STR |
chrome | Browser channel: chrome, msedge, '' for bundled chromium |
--auto-cdp |
on | Auto-launch Chrome with debug port on copied real profile |
--no-auto-cdp |
- | Disable auto-cdp |
--auto-cdp-port |
9222 | Debug port |
--auto-cdp-profile |
C:\temp\chrome_pim_profile |
Profile-copy target |
--refresh-chrome-profile |
off | Force re-copy real Chrome profile |
--cdp-endpoint URL |
- | Attach to existing debug Chrome |
Cache
- Eligibilities cached 24h at
%LOCALAPPDATA%\pim_activate\eligible_cache.json. - Pending approvals are never cached (time-sensitive).
- NEW entries flagged
*NEW*in picker and*in list mode.
Auth model
Portal PIM app (c44b4083-... / Microsoft_Azure_PIMCommon) sits inside the
authenticated Portal SPA. Its Graph token has all PIM scopes we need. We grab
that token via Playwright over CDP (attached to a Chrome that runs against a
copy of your real profile, so Intune compliance cookies come along).
az account get-access-token --resource https://graph.microsoft.comfails — first-party az CLI cannot request PIM Graph scopes in the tenant.Connect-MgGraph -UseDeviceCodefails — CA blocks the Graph PowerShell app.- Cloud Shell fails — CA blocks the Cloud Shell session.
- Portal-in-browser + sniff works.
Scope caveat
Portal caches READ scope on first load of Entra ID blades. ReadWrite scope for
activation POST is cached only when portal visits the PIM Groups activation
blade. --auto-cdp opens that blade automatically.
Approval action needs PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup,
which portal caches when you visit the approvals blade. If Graph 403s on
approve, open PIM Approvals blade in portal once, then rerun.
Endpoints used
GET /v1.0/meGET /v1.0/identityGovernance/privilegedAccess/group/eligibilityScheduleInstancesGET /v1.0/groups/{id}GET /v1.0/policies/roleManagementPolicyAssignmentsGET /v1.0/policies/roleManagementPolicies/{id}/rulesPOST /v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleRequests(selfActivate)GET /v1.0/identityGovernance/privilegedAccess/group/assignmentScheduleRequests/{id}(poll)GET /beta/identityGovernance/privilegedAccess/group/assignmentScheduleRequests/filterByCurrentUser(on='approver')(pending approvals — portal-parity server-side scoping)GET /beta/identityGovernance/privilegedAccess/group/assignmentApprovals/{approvalId}(approval steps)PATCH /beta/identityGovernance/privilegedAccess/group/assignmentApprovals/{approvalId}/steps/{stepId}(approve)
Fallback: manual token
If Playwright breaks (browser update, CA change), grab a token manually:
- Open portal.azure.com, sign in.
- F12 → Network tab.
- Navigate to PIM Groups. Find a request to
graph.microsoft.com/*. - Copy the
Authorizationheader value (drop theBearerprefix). pim-activate --token "<paste>".
Known limitations
- Denying approvals: not supported. Use portal.
- Non-group PIM (AAD roles, Azure resource roles): different endpoints.
- Scheduled/future activations: only immediate
selfActivate. - bash/xterm terminal + interactive picker:
questionaryneeds a Win32 console — run from cmd.exe, PowerShell, or Windows Terminal. Bash/Cygwin raisesNoConsoleScreenBufferError. Non-interactive flags (--group,--list-only) work in any shell.
Development
git clone https://github.com/wesselvdlinden/azure-pim-cli
cd azure-pim-cli
python -m venv .venv && .venv\Scripts\activate
pip install -e ".[dev]"
# Run tests
pytest
# Lint
ruff check . && ruff format --check .
# Type check
mypy src/azure_pim_cli
Publishing a release
- On PyPI, create a new project
azure-pim-cliand configure a Trusted Publisher:- Owner:
wesselvdlinden - Repo:
azure-pim-cli - Workflow:
release.yml - Environment:
pypi
- Owner:
- Tag and push:
git tag v0.1.0 && git push origin v0.1.0 - The
release.ymlworkflow builds and publishes automatically via OIDC — no API token needed.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file azure_pim_cli-0.1.0.tar.gz.
File metadata
- Download URL: azure_pim_cli-0.1.0.tar.gz
- Upload date:
- Size: 33.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b508e8da4e1da3cc7409f2066db01291e78d1ae9f75e5b1821ae975e6226b8aa
|
|
| MD5 |
e5bd553d58307d0b59356f094d0c62e7
|
|
| BLAKE2b-256 |
394475b79b9880266a751e7073b983a63b719d81867167ee693801b0c5f4dd3d
|
Provenance
The following attestation bundles were made for azure_pim_cli-0.1.0.tar.gz:
Publisher:
release.yml on TheScriptingGuy/azure-pim-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
azure_pim_cli-0.1.0.tar.gz -
Subject digest:
b508e8da4e1da3cc7409f2066db01291e78d1ae9f75e5b1821ae975e6226b8aa - Sigstore transparency entry: 2173178275
- Sigstore integration time:
-
Permalink:
TheScriptingGuy/azure-pim-cli@1fe22a05dd5e6260241db8cbc39a858b663a6763 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/TheScriptingGuy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1fe22a05dd5e6260241db8cbc39a858b663a6763 -
Trigger Event:
push
-
Statement type:
File details
Details for the file azure_pim_cli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: azure_pim_cli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
45e65e6c09074b0bf1e536577e057b6326dca4a6741209454e3c676589ea079e
|
|
| MD5 |
4cac688cae1a9d5b423b9c39a2b4de66
|
|
| BLAKE2b-256 |
fc9a7f7a766f7c934fb985f9abfb063a6d0dbe3ae6cc5aba261590f6cc431b2e
|
Provenance
The following attestation bundles were made for azure_pim_cli-0.1.0-py3-none-any.whl:
Publisher:
release.yml on TheScriptingGuy/azure-pim-cli
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
azure_pim_cli-0.1.0-py3-none-any.whl -
Subject digest:
45e65e6c09074b0bf1e536577e057b6326dca4a6741209454e3c676589ea079e - Sigstore transparency entry: 2173178286
- Sigstore integration time:
-
Permalink:
TheScriptingGuy/azure-pim-cli@1fe22a05dd5e6260241db8cbc39a858b663a6763 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/TheScriptingGuy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1fe22a05dd5e6260241db8cbc39a858b663a6763 -
Trigger Event:
push
-
Statement type: