A Windows system tray application for managing Azure Privileged Identity Management (PIM) requests across multiple tenants.
This application has been an experiment with "Vibe" coding and was developed with assistance from Claude, Anthropic's AI assistant. Claude contributed to code implementation, debugging, and documentation throughout the development process.
- Monitor pending approvals from Entra ID and Azure RBAC in one place
- Approve requests directly from desktop notifications or the tray menu
- Self-activate eligible roles with custom durations and justifications
- Active role detection — roles already active are grayed out to prevent duplicate requests
- Multi-tenant support with per-tenant configuration and subscription filtering
- Desktop notifications when new requests arrive or activations complete
- Auto-update via Velopack — patch updates apply silently on exit, major/minor updates notify via tray icon
- Token resilience — survives laptop lock/sleep overnight with automatic re-authentication
- Extension system — install plugins from a feed, with per-tenant enable/disable and automatic permission management
- Built-in log viewer with configurable log level (persisted across restarts)
- Auto-exclude empty subscriptions — subscriptions with no eligible roles are automatically unchecked to reduce API calls
- .NET 10.0 (Windows), WPF + WinForms interop for system tray
- Azure.Identity with MSAL persistent token caching
- Microsoft Graph API (v1.0 + beta) for Entra ID PIM
- Azure Resource Manager API for Azure RBAC PIM
- Velopack for auto-updates via GitHub Releases
- Windows 10/11
- .NET 10.0 runtime (or SDK to build from source)
dotnet build Azure.PIM.Tray/Azure.PIM.Tray.csproj
dotnet run --project Azure.PIM.Tray/Azure.PIM.Tray.csprojdotnet publish Azure.PIM.Tray/Azure.PIM.Tray.csproj -c Release -r win-x64 --self-containedPush a version tag to trigger the automated release pipeline:
git tag v1.2.0
git push origin v1.2.0This runs the GitHub Actions workflow which builds, packages with Velopack, and creates a GitHub Release with update artifacts. Running instances of the app will pick up the update automatically.
You can also trigger a release manually from the Actions tab on GitHub.
Push an extension tag to build and publish a plugin DLL:
git tag ext-laps-v1.0.0
git push origin ext-laps-v1.0.0This runs the extension release workflow which builds the plugin, and creates a GitHub Release with the DLL attached. Users can install it from Settings > Extensions > Refresh > Install, or manually drop the DLL into the plugins/ folder.
You can also trigger it manually from the Actions tab with a project name and version.
The app requires an Entra ID (Azure AD) app registration named "PIM Request Manager" in each tenant you want to manage. You can create this manually or let the app's Fix Permissions button configure it for you.
- Create a new app registration in the Azure Portal:
- Name: PIM Request Manager
- Supported account types: Single tenant
- Redirect URI: Public client/native —
http://localhost
- Launch the app, open Settings, enter your Tenant ID and email, click Connect & Discover
- Select the tenant in the list and click Fix Permissions (requires Global Administrator)
- The app will automatically add all required API permissions (including those from enabled extensions) and grant admin consent
-
Create the app registration as above
-
Under API permissions, add the following Delegated permissions:
Microsoft Graph (core):
Permission Purpose User.ReadResolve the signed-in user's principal ID via /meRoleAssignmentSchedule.ReadWrite.DirectorySubmit self-activation requests for Entra ID roles RoleEligibilitySchedule.Read.DirectoryList eligible and currently active Entra ID role assignments PrivilegedAccess.ReadWrite.AzureADList pending approvals, retrieve approval details, and approve Entra ID PIM requests RoleManagement.Read.DirectoryRead PIM policies to check if approval is required, and poll activation request status Microsoft Graph (LAPS extension):
Permission Purpose DeviceLocalCredential.Read.AllList LAPS-managed devices and retrieve local admin passwords (added automatically when the LAPS extension is installed and enabled) Azure Service Management:
Permission Purpose user_impersonationAll Azure RBAC PIM operations: list subscriptions, list eligible roles, submit activations, list/approve pending requests, read policies, and poll status -
Click Grant admin consent for all permissions
-
Under Authentication, ensure Allow public client flows is set to Yes
The app registration permissions allow the app to call the PIM APIs, but the signed-in user must also be assigned as an approver in the relevant PIM policies to approve requests. If you can see pending requests but approval fails with "not assigned to you", check the PIM role settings in the Azure Portal to confirm your account is listed as an approver. Note that Azure PIM does not allow users to approve their own requests.
After setup, select the tenant in Settings and check the Permissions column. A green checkmark indicates all permissions are correctly configured (including those required by enabled extensions). If any are missing, the app will show what's needed.
- Launch the app — it starts in the system tray and opens Settings
- Enter your Tenant ID and email, then click Connect & Discover
- Sign in via browser when prompted
- If permissions are missing, click Fix Permissions (requires Global Administrator) or configure them manually as described above
- Click Sign In to authenticate with the app's own credentials
- Expand a tenant node to see subscriptions; uncheck any you don't need to monitor
- Close the window — the app begins monitoring in the background
Repeat for each tenant you want to manage.
The app supports a plugin system for adding optional features. Extensions are distributed as single DLL files and installed via the Settings window or by dropping them into the plugins/ folder.
- Open Settings > expand Extensions
- Click Refresh to load available extensions from the feed
- Click Install next to the extension you want
- Restart the app
- Per-tenant enable/disable — each installed extension shows checkboxes per tenant in Settings
- Permission management — click Grant Permissions on an extension to register its required API scopes
- Required roles — extensions that require specific Entra ID roles (e.g. "Cloud Device Administrator" for LAPS) are grayed out in the tray menu when the role isn't active
| Extension | Description | Required Role |
|---|---|---|
| LAPS Passwords | Retrieve local admin passwords for Entra-managed devices via Microsoft LAPS | Cloud Device Administrator |
Extensions implement ITrayPlugin from the Azure.PIM.Tray.Extensibility contract library. See Azure.PIM.Tray.Ext.Laps for a reference implementation.
The default feed is hosted at feed/extensions.json in this repository. Custom feeds can be configured in tray-config.json via the extensionFeeds array.
Config is stored at %APPDATA%\PimRequestManager\tray-config.json. Subscription caches are stored alongside as subs-{tenantId}.json.
Key settings:
- Subscriptions — exclude subscriptions from scanning via Settings to reduce API calls. Subscriptions with no eligible roles are automatically excluded.
- Log level — configurable in the Log Viewer (Debug, Info, Warning, Error). Defaults to Warning. Persisted across restarts.
- Log to disk — toggle in the Log Viewer. Logs to
%LOCALAPPDATA%\Azure.PIM.Tray\logs\. - Extension feeds — custom feed URLs can be added to the
extensionFeedsarray in the config file. - Disabled extensions — per-tenant extension toggles are stored in each connection's
disabledExtensionsarray.
Azure.PIM.Tray/ Main application
App.xaml.cs Startup, shutdown, window launchers, session/power event hooks
TrayIconManager NotifyIcon, balloons, icon generation (pending/update states)
RefreshOrchestrator Background polling, fan-out refresh, new/completed detection
ContextMenuBuilder Tray context menu construction with plugin integration
ActivationWatcher Activation polling and status notifications
UpdateService Auto-update via Velopack (check, download, apply)
PluginLoader Plugin discovery, initialization, and lifecycle
ExtensionFeedService Feed fetching, extension install/remove
PluginMenuAdapter Bridges IPluginMenuBuilder to TrayMenuWindow (WPF isolation)
TenantContext Per-tenant state: pending requests, eligible roles, active roles
PimDataService Routes to Graph or ARM data services
ArmPimService Azure Resource Manager PIM API client
PimService Microsoft Graph PIM API client
SerializedTokenCredential Global gate preventing concurrent browser auth popups
ConnectionService Credential factory, config persistence, permission management
Azure.PIM.Tray.Extensibility/ Plugin contract library
ITrayPlugin Plugin interface (Id, Name, permissions, roles, menu building)
IPluginMenuBuilder Menu abstraction (items, search boxes, submenus, clipboard)
PluginTenantContext Credentials, identity, and logging for plugins
Azure.PIM.Tray.Ext.Laps/ LAPS password retrieval plugin
LapsPlugin Lists LAPS devices, retrieves passwords, auto-detects availability
| Action | How |
|---|---|
| View pending approvals | Left/right-click tray icon |
| Approve a request | Click a pending request in the menu, or click the balloon notification |
| Activate a role | Expand "Open Request" in the menu, click a role |
| View logs | Click "Log Viewer" in the menu |
| Settings | Click "Settings" in the menu |
| Check for updates | Click the version link in the Settings window |
| Apply an update | Click "Update & Restart" in the Settings window |
| Install extensions | Settings > Extensions > Refresh > Install |
| Enable/disable extension per tenant | Settings > Extensions > Installed > check/uncheck tenant |
| Reload tokens | Right-click "Sign In" > Reload Token |
MIT License — see LICENSE for details.