mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 10:55:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user