Skip to content

Latest commit

 

History

History
215 lines (178 loc) · 13.4 KB

README.md

File metadata and controls

215 lines (178 loc) · 13.4 KB

Elevate for Windows

The Windows 11 counterpart of the macOS menu bar app: a system-tray flyout for just-in-time Microsoft Entra and Azure PIM role activation, built with WinUI 3 on .NET 10, installed from an MSI downloaded from the GitHub releases or installed with winget.

UI design: docs/design/elevate-windows.html (Fluent mockups of the flyout, windows, tray states and tokens; open the file in a browser, it follows the OS theme).

Install

winget install Reothor.Elevate

Or download Elevate-<version>-x64.msi (or -arm64.msi for Arm PCs) from the latest release and run it: both routes install the same MSI. It installs for the current user into %LOCALAPPDATA%\Programs\Elevate (no admin rights), adds a Start Menu entry and can launch Elevate when it finishes. It also installs the elevate command-line tool into a cli subfolder (%LOCALAPPDATA%\Programs\Elevate\cli) and adds that subfolder to your user PATH, so elevate --version works in a new terminal (an already-open terminal needs to be restarted). If you also installed the standalone CLI with winget (Reothor.Elevate.CLI), uninstall one of them so a single elevate is on the PATH. It needs the .NET 10 runtime (winget install Microsoft.DotNet.Runtime.10); the Windows App SDK runtime is bundled. Windows 11 (build 22000) or newer.

To install for every user of the PC instead, run the MSI with ALLUSERS=1 from an elevated prompt (or accept the UAC prompt):

msiexec /i Elevate-<version>-x64.msi ALLUSERS=1

That installs into %ProgramFiles%\Elevate, puts the Start Menu entry in everyone's Start Menu and adds the cli subfolder to the system PATH. Settings, accounts and saved sign-ins stay per user either way. Upgrade a per-machine install the same way, with ALLUSERS=1: run without it, the installer stops with a message saying so, because it would otherwise put a second, per-user copy next to the per-machine one. A per-machine install does not remove a copy a user installed for themselves: uninstall the per-user one first, or the PC ends up with both.

The MSI, the app and the bundled CLI are code-signed with a Certum certificate; the publisher shows as "Open Source Developer Frode Hus". SmartScreen can still show "Windows protected your PC" for a new release until the publisher has built reputation: confirm the publisher name in the dialog, then choose More info and Run anyway. The SHA-256 in the release notes is there for an independent check:

(Get-FileHash .\Elevate-<version>-x64.msi).Hash

Upgrade with winget upgrade Reothor.Elevate or by running a newer MSI; uninstall from Settings > Apps, which removes the app, the CLI and the PATH entry. Each release opens its own pull request against microsoft/winget-pkgs, so the published version may lag the GitHub release by a day or two; see Release.

Use

Elevate lives in the notification area. Left-click the icon for the flyout, right-click for Open, Settings and Quit. The tray glyph shows the number of active roles, a warning dot when one expires within five minutes and a hollow dot while a request awaits approval. A toast fires five minutes before a role expires with an Extend button, and again when it has expired.

Sign in from the flyout's Add account… with one of:

  • Entra app registration through the Windows account picker (WAM), with the system browser as the fallback. Supports Entra directory roles, Azure resource roles and PIM for Groups. Enter the application (client) ID in Settings first; the registration must list both redirect URIs shown there under the Mobile and desktop applications platform: ms-appx-web://microsoft.aad.brokerplugin/{client id} for the broker and http://localhost for the browser fallback. See docs/entra-app-registration.md. Or use the project's shared registration, c9011cc5-7422-4630-a432-73ff4df5834e, which already lists both redirects — optional, no SLA, and an administrator must consent once per tenant; see docs/shared-app-registration.md. One account can also use a different registration — a client ID of its own instead of the one in Settings, set when it is added or later from its account menu (Change app registration…, or Upgrade to Entra app registration… for an account on one of the methods below). Hidden when your organization manages the client ID with the ClientId policy. Changing the Settings client ID keeps the accounts that follow it, with their tenants, roles and profiles, and asks them to sign in again.
  • Other app (browser sign-in): any public-client registration, through the system browser on http://localhost.
  • The Azure CLI app or the Azure PowerShell app: no registration or consent needed, but Azure resource roles only — Entra directory roles and PIM for Groups are neither read nor activated, because Microsoft grants those apps no Graph PIM permissions. Use your own or another app registration for those.

Everything the macOS app does is here: select several roles across pivots and activate them together, save a selection as a profile (pinned profiles are chips in one row under the pivots, All N lists the rest with search; the Profiles window renames, pins, reorders, edits the roles and their durations in place, and deletes; a chip opens the run window with per-entry durations, the remembered reason, ticket and start time), Ctrl-click Activate, Extend or a chip to go straight through with the last reason and duration when the policy allows it, and a global shortcut (Settings) that runs one profile the same way. Requests waiting for your approval appear in a pinned Approvals group above Active now with Approve and Deny; a toast announces each new request once, and the tray icon carries an orange dot while any are pending.

The Azure pivot groups its eligibilities by scope instead of listing them flat: management groups, subscriptions, resource groups and resources, each header opening and closing with its chevron and saying how many roles sit under it. A scope that only passes through — no eligibility of its own and one way down — is folded into the node below it and named there ("Alpha / prod"), so the panel does not spend a row and a level of indent on nothing. In select mode a scope header carries its own checkbox: one press takes every eligibility under it, which is what "Contributor across these twelve subscriptions" costs now. The search box narrows the tree and reaches the whole ARM path, and matches stay in place even under a header you had closed.

Management groups sit beside the subscriptions rather than above them: ARM writes a management group scope as its own flat path and never repeats it in a subscription's, so the eligibilities alone cannot say which subscriptions belong to which management group.

Settings also holds Start Elevate when I sign in (a per-user Run entry), Check for updates (the flyout offers a newer release with a Windows installer once a day, with Open and Dismiss) and Copy diagnostics (a plain-text report of accounts, tenants, profiles, the shortcut and recent errors, never a token or client ID).

Files live in %LOCALAPPDATA%\Elevate: state.json (accounts, tenants, manual roles, remembered reasons; the same schema as the macOS app), settings.json, the DPAPI-protected MSAL token cache msal.cache, and elevate.log for failures of the shell itself.

Managed configuration

An organization can push Elevate's settings with Group Policy or Intune instead of telling everyone a client id. The app reads HKLM\SOFTWARE\Policies\Reothor\Elevate, then the same key under HKCU (the machine value wins per key), and locks each value it finds: the setting renders disabled with a "Managed by your organization" caption, and Settings and Diagnostics list the keys in effect. An ADMX template ships with each release. Administrators start at docs/enterprise/README.md; the keys are in docs/enterprise/keys.md and the templates in enterprise/.

Build and test

cd windows
dotnet test Elevate.sln

Elevate.Core and its tests build on macOS, Linux and Windows with the .NET 10 SDK. The app (src/Elevate.App, WinUI 3, unpackaged, self-contained Windows App SDK 1.8) and Elevate.App.Model (the testable half: app model, settings, MSAL providers) need Windows 11 and the Windows 11 SDK 10.0.22621; dotnet build Elevate.sln builds them without Visual Studio.

Run the app from the build output with developer switches for screenshots and smoke tests:

dotnet build src/Elevate.App/Elevate.App.csproj
src\Elevate.App\bin\x64\Debug\net10.0-windows10.0.22621.0\win-x64\Elevate.exe --flyout
src\Elevate.App\bin\x64\Debug\net10.0-windows10.0.22621.0\win-x64\Elevate.exe --show settings

--show takes settings, add-account, registration, configure, activation, bulk, add-tenant, discover, save-profile, manage-profiles, run-profile or decision. --test-toast <seconds> schedules one Extend toast with Windows that far out; quit the app before it fires to see it delivered anyway. To exercise the flyout without signing in, seed %LOCALAPPDATA%\Elevate\state.json with an own-app identity, tenants in manualRoles mode, manual roles and profiles (the golden fixture tests/Elevate.Core.Tests/Fixtures/state-macos.json shows the shape; durations are the 128-bit attosecond pair Swift writes) and clear clientId in settings.json; back both files up first.

Installer and winget manifest

dotnet tool install --global wix --version 5.0.2
./installer/build.ps1 -Version 1.0.0            # x64 and arm64 MSIs in installer/out
./winget/New-Manifest.ps1 -Version 1.0.0 -X64Sha256 <sha> -Arm64Sha256 <sha>
winget validate --manifest winget/manifests/r/Reothor/Elevate/1.0.0

installer/build.ps1 publishes the app framework-dependent with the Windows App SDK self-contained, and also publishes the CLI (../cli/src/Elevate.Cli) self-contained per architecture, signing elevate.exe when -Sign is given, and passes it to WiX as CliDir. It builds the MSI with WiX v5 (installer/Elevate.wxs), validates it with wix msi validate, and signs the MSI with the Certum certificate named by CERTUM_KEY_ID when -Sign is given (the SimplySign session must already be open). The MSI's Cli component installs elevate.exe under cli\, and that subfolder goes on the PATH.

The package is dual-purpose (Scope="perUserOrMachine", Windows Installer's "single package authoring"): per user by default, per machine with ALLUSERS=1. Windows Installer redirects ProgramFiles64Folder, ProgramMenuFolder and the HKMU registry root to match, so the source must not name a per-user or a per-machine location outright; the validation step (ICE105 among others) fails the build if it does. The PATH entry and the shortcut are each two components, one per scope, chosen by a condition on ALLUSERS. A registry search for the per-machine install feeds a launch condition that refuses a per-user install on such a PC, so an upgrade never lands next to the install it should replace. (Switching the install to per-machine at that point is not an option: Windows Installer settles the scope before any action runs, and a switched install removes the old version and then fails.) Windows Installer writes the Add/Remove Programs key of a per-user install under HKLM too, keyed to the user who installed it; that is its own behaviour, not the package's. winget/New-Manifest.ps1 fills the templates in winget/templates with the release URLs and hashes.

Release

Releases are shared with the macOS app: tag v<version> on main and the release workflow builds both apps, publishes one GitHub release with the DMG, both MSIs and their SHA-256 files, and attaches the generated winget manifest as a workflow artifact. The MSIs and elevate.exe are signed with the Certum certificate (the secrets are in place; without them the workflow would build unsigned and say so in the release notes). The manifest is submitted to microsoft/winget-pkgs by the publish job (scripts/submit-winget.sh). The full procedure is in docs/releasing.md.

Deactivating a profile run

Choose Deactivate roles… from a profile's menu and select the run by date and time. The review lists only assignments that run created, including roles since removed from the profile. Roles already active when the run started are excluded. The saved run survives an app restart; each assignment is verified with the provider before deactivation, so an expired or replaced activation is skipped. A role still within its minimum activation period stays available to retry while other roles proceed. Completed roles are not retried. When an approval or provisioning response does not identify the original activation interval, the review directs you to deactivate the role individually. If another profile uses the same access, the review names it so the impact is clear. Every role is checked by default; uncheck one to keep that access for this pass. It stays in the run and is offered again the next time you open the review.

Downward chevrons indicate deactivation in progress, both in role rows and in the profile review. A green check appears only after the provider confirms deactivation. Reduced-motion settings use static status indicators.