9.8 KiB
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
-
Applies only to delegated tokens.
idtyp = app(or noscp) means application permissions, which bypass Intune RBAC - therolesclaim is the whole answer. -
Directory-role short-circuit. Intune Administrator (
3a2c62db-5318-420d-8d74-23affee5d9d5) or Global Administrator (62e90394-69f5-4237-9190-012177145e10) inwidsgrants 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. -
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:notAllowedResourceActionscomes 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, itsidbeing exactly theMicrosoft.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.
-
Per type: the
_APIpath is mapped to an Intune resource category (deviceManagement/deviceConfigurations->DeviceConfigurations, ...; table inInternal/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 itModify(ManagedGooglePlay) is not marked down for the actions it never had.For the category Level _Readis not in the catalogueUnknown - no such category exists; say nothing _Readexists, not allowedNone _Readallowed, type declares only Read scopesFull (read-only by design) _Readallowed, an existing write action (Create/Update/Delete/Assign/Modify) not allowedLimited, 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
_Readis 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:RbacUnmappedApisare Unknown. A test fails if a newdeviceManagement/ordeviceAppManagement/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
HideNoAccesssetting 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.ps1withUI/Avalonia/Classes/EffectivePermissionRowItem.ps1. Get-IMGraphEffectivePermissions- the same rows for scripts (-PolicyType,-TokenId), or-Rawfor 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:
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.