Files
IntuneManagement/Docs/EffectivePermissions.md
T

166 lines
9.8 KiB
Markdown

# Effective Permissions
What the signed-in identity can actually do per policy type, and how the left-nav
access marking, the Profile popup's **Permissions** dialog and
`Get-IMGraphEffectivePermissions` derive it. This page is the "how it works now".
## The two halves of a delegated login
| Half | Where it lives | What it says |
| --- | --- | --- |
| App consent | token `scp` (delegated) / `roles` (app-only) | which Graph scopes the *application* was granted |
| User authorization | Intune RBAC role assignments (+ scope tags) and Entra directory roles | what the *user* may do |
The token carries only the first half (plus `wids`, the directory role template
ids). A user holding just the built-in *Read Only Operator* Intune role signs in with
a token that says `DeviceManagementConfiguration.ReadWrite.All`, and every write is
refused with 403. Effective access is the intersection, so the marking has two layers:
```
Update-IntuneAccessLevels Internal/AccessLevel.ps1
Layer 1 scp/roles vs _Permissions -> Full / Limited / None
Layer 2 Get-IntuneRbacContext -> Full / Limited / None / Unknown Internal/EffectivePermissions.ps1
stamp AccessType = worst(L1, L2), AccessInfo = both reasons
```
Layer 2 can only make a type worse. Unknown never colours: Layer 1 stands.
## Layer 2 in detail
1. **Applies only to delegated tokens.** `idtyp = app` (or no `scp`) means application
permissions, which bypass Intune RBAC - the `roles` claim is the whole answer.
2. **Directory-role short-circuit.** Intune Administrator
(`3a2c62db-5318-420d-8d74-23affee5d9d5`) or Global Administrator
(`62e90394-69f5-4237-9190-012177145e10`) in `wids` grants complete Intune RBAC, so
every Intune category is Full and Graph is not asked. Global Reader and the partial
roles are *not* shortcuts - the user may also hold an Intune role.
3. **Otherwise two Graph calls:**
- `GET /beta/deviceManagement/getEffectivePermissions(scope='*')` - the same function
the Intune portal uses to enable its buttons. It answers with the **allowed**
actions only: `notAllowedResourceActions` comes back empty on every response, so
on its own it cannot tell "the role denies this action" from "no such action
exists for this category".
- `GET /beta/deviceManagement/resourceOperations` - the **catalogue**: every resource
action Intune defines, one entry per action, its `id` being exactly the
`Microsoft.Intune_<Category>_<Action>` name the first call uses. That is the
existence answer the first call cannot give (260 actions over 53 resources on the
tenant this was built against). It describes the service, not the user, so it is
cached per tenant and survives a token refresh. When it cannot be read, existence
is *unknown* and the verdicts degrade as shown below.
4. **Per type:** the `_API` path is mapped to an Intune resource category
(`deviceManagement/deviceConfigurations` -> `DeviceConfigurations`, ...; table in
`Internal/EffectivePermissions.ps1`, overridable per type with `_ResourceCategory`).
Only actions that exist for the category are required, so a category with no
`_Assign` (Roles) or one that spells it `Modify` (ManagedGooglePlay) is not marked
down for the actions it never had.
| For the category | Level |
| --- | --- |
| `_Read` is not in the catalogue | Unknown - no such category exists; say nothing |
| `_Read` exists, not allowed | None |
| `_Read` allowed, type declares only Read scopes | Full (read-only by design) |
| `_Read` allowed, an existing write action (`Create/Update/Delete/Assign/Modify`) not allowed | Limited, tooltip lists the missing actions |
| every existing action is allowed | Full |
The Limited tooltip distinguishes the two shapes of it: "read-only for
`<Category>`" when no write action at all is allowed, and "partial write access to
`<Category>`" when some are (a role that can create and update but not delete or
assign is still Limited, but it is not read-only). The Permissions popup's Role,
Effective and Result columns show "Partial write" for that second shape rather
than "Read" / "Read-only" - Effective and Result only when the token can write
too, since a read-only token over a partial-write role really is read-only.
With no catalogue, the same type yields Unknown when `_Read` is not allowed (it may
not exist), Limited when no write action at all is allowed, and Full otherwise - a
coarser answer that never invents an action name.
APIs that Intune RBAC does not govern (`identity/*`, `identityGovernance/*`,
`organization/*`) and the few listed in `$script:RbacUnmappedApis` are Unknown. A
test fails if a new `deviceManagement/` or `deviceAppManagement/` type is in
neither table.
## Caching and refresh
The user's answer is cached per tenant under the **token fingerprint** (`tid|oid|iat`)
with no TTL; the action catalogue is cached per tenant only, so a refresh re-asks
`getEffectivePermissions` and reuses the catalogue. Both are dropped on disconnect. Any newly minted token - routine renewal, a new sign-in, or **Refresh in the
Profile popup** - has a new `iat`, misses the cache and re-asks. That single user
action therefore covers both kinds of change:
| Change | Visible in the token? | Caught by |
| --- | --- | --- |
| Directory role via PIM (Intune Administrator, Global Admin) | only in a newly issued token; the routine silent acquire returns the cached one until near expiry | Refresh mints a token with the current `wids` |
| Intune RBAC assignment (a role added, PIM for Groups) | never - the token is identical | Refresh still produces a new `iat`, so Layer 2 re-asks Graph |
A failed lookup is remembered for the same fingerprint (no retry on every menu
rebuild) and retried after a refresh. Refresh is provider-agnostic: MSAL
(`WithForceRefresh`), delegated OAuth (`refresh_token` grant) and MgGraph
(`Connect-MgGraph` re-run) all mint a new token; BYO bearer tokens cannot refresh and
the button is already disabled for them.
**Scope tags are not modelled.** `getEffectivePermissions` is the global answer; a
user limited to some tags can still be refused on individual objects.
## Surfaces
- **Left nav** - orange (Limited) / red (None) with the reason in the tooltip; the
existing `HideNoAccess` setting hides red rows. A summary line is logged at sign-in.
- **Profile popup -> Permissions** - one row per policy type: token level, Intune-role
level, effective level, reason, plus a header saying where the Intune half came from
and when the token was issued. WPF: `UI/WPF/Extensions/EffectivePermissionsUIWPF.ps1`;
Avalonia: `UI/Avalonia/Extensions/EffectivePermissionsUIAvalonia.ps1` with
`UI/Avalonia/Classes/EffectivePermissionRowItem.ps1`.
- **`Get-IMGraphEffectivePermissions`** - the same rows for scripts
(`-PolicyType`, `-TokenId`), or `-Raw` for the context (source, the allowed action
set, the catalogue of actions that exist, raw response). Runs even when the setting
below is off.
## Setting
`UseRbacAccessMarking` (General, default on). Off restores the token-only marking
exactly. Registered in `Internal/EffectivePermissions.ps1` because engine code reads
it.
## Verifying against a tenant
Both endpoints, the response shapes and every **category name** in the table have been
run against a live tenant, and `Tests/EffectivePermissions.Tests.ps1` asserts the names
against the catalogue fixture, so a typo fails the suite. What is still unverified is
the other half of each entry - that a given `_API` really is governed by the category it
is mapped to. Those are marked `UNVERIFIED`, and **an unverified mapping is not a safe
mapping**: only a category name that does not exist at all degrades to Unknown. A mapping
to a name that exists but governs a *different* resource produces a confident verdict
derived from the wrong role permissions - a type marked Full because the user's role
covers the category it was mistakenly mapped to, or None because it does not. Both are
wrong, and neither shows as Unknown. The typo test cannot catch this: the wrong name is a
real one. Only a role that grants exactly one category can, so to check on a lab tenant:
```powershell
Import-Module .\IntuneManagement.psd1 -Force
Connect-IMIntuneManagement ... # delegated, as a NON-admin test user
Get-IMGraphEffectivePermissions -Raw | Select-Object Source, TenantId, AsOf
(Get-IMGraphEffectivePermissions -Raw).Allowed | Sort-Object # what the user's role grants
(Get-IMGraphEffectivePermissions -Raw).Catalog | Sort-Object # every action Intune defines
Get-IMGraphEffectivePermissions | Where-Object RbacLevel | Format-Table Id, ResourceCategory, TokenLevel, RbacLevel, EffectiveLevel
```
Assign the test user a custom role granting exactly one category, refresh the token, and
check that the types mapped to it are the ones that move. Move confirmed entries out of
`UNVERIFIED`, and add `_ResourceCategory` overrides where a type maps elsewhere.
## Tests
`Tests/EffectivePermissions.Tests.ps1` (offline, fixtures under
`Tests/Fixtures/EffectivePermissions/`): category resolution, completeness and every
category name against the catalogue, response parsing, per-type verdicts for a full
admin / read-only operator / custom role and for a missing catalogue, the never-upgrade
property, app-only skip, directory-role short-circuit, fingerprint caching, catalogue
caching across a refresh, negative caching, the setting gate, disconnect clearing,
`Update-IntuneAccessLevels` integration and the cmdlet's rows.
The fixtures are recorded responses, not hand-written: `ReadOnlyOperator.json` is the
built-in role's 55 actions, `ResourceOperations.json` the whole catalogue, and every
`notAllowedResourceActions` is empty because that is what Graph returns. A verdict test
that fails is a bug in the code or the mapping - do not "fix" it by inventing
not-allowed data the API never sends.