Skip to main content

Every permission we ask for, and why

The full list: twelve Microsoft Graph read permissions, the endpoint each one serves, and what would stop working without it. Every call is a GET.

Written for whoever runs IT11 min readUpdated

A security product asking for tenant-wide read access to your directory is asking for a great deal, and the honest way to justify it is line by line. This article lists every Microsoft Graph call discovery makes, the application permission it needs, and what specifically stops working if you leave that permission out.

Two things are true of the whole list and worth stating before the table. Every call is a GET — discovery cannot write to your tenant even if it wanted to, because the read path we use exposes no write method at all. And every permission is a read permission; there is no .ReadWrite. anywhere in it.

Application, not delegated

The token we hold is issued to an application under the client-credentials grant. The scope requested is https://graph.microsoft.com/.default, which does not mean everything: it means precisely the set of application permissions your app registration was granted and an administrator consented to, and nothing else. Adding a permission to the registration without granting consent changes nothing. Removing consent removes our access on the next token, without our involvement.

This is why the permissions must be added under Application permissions rather than Delegated permissions when you set the connection up. A delegated grant describes what an application may do while acting for a signed-in user; there is no signed-in user in this flow, so a delegated grant produces a connection that authenticates cleanly and then reads nothing.

The twelve calls

Every Microsoft Graph call estate discovery makes, the application permission it needs, and what it is for.
The call we makeThe permission this needsWhy we need it
GET /organization?$select=id,displayName,verifiedDomainsOrganization.Read.AllIdentifies the tenant and its verified domains. The domains are how we tell internal from external, which is what makes a mail-forwarding rule either routine or an emergency. This is the one genuinely fatal step: without a tenant identity nothing else can be attributed to anyone, so a failure here ends the run rather than degrading it.
GET /users?$select=id,displayName,userPrincipalName,mail,…User.Read.AllThe staff list — every account, whether it is enabled, whether it is a guest, when it was created. The presence of a mail attribute is the signal that an account is also somebody's daily mailbox, which is what turns an administrator account into an administrator account being used to read email.
GET /reports/authenticationMethods/userRegistrationDetailsAuditLog.Read.All and UserAuthenticationMethod.Read.AllWho actually has a second factor registered, who is capable of a passkey, who has self-service password reset. This lives under a different endpoint from /users, so the answer has to be joined onto each account by object ID. A user missing from this report is treated as not assessed, never as no second factor.
GET /directoryRoles?$expand=membersRoleManagement.Read.DirectoryWho holds administrator roles, and how many. The member list is heterogeneous — users, service principals and groups arrive in the same array, discriminated only by @odata.type — and a human administrator and an application holding the same role are entirely different findings with entirely different fixes.
GET /groups and GET /groups/{id}/members/$countGroup.Read.AllGroup membership as a privilege path. A role-assignable group is a way to become an administrator without appearing in the administrator list, so we read the member count for those groups specifically rather than for every group — one call per group is more than the discovery budget allows.
GET /identity/conditionalAccess/policiesPolicy.Read.AllWhether a second factor is actually required rather than merely available, and whether legacy sign-in protocols are blocked. Registered multi-factor authentication with no policy enforcing it is a very common and very expensive misunderstanding.
GET /subscribedSkusOrganization.Read.AllYour licence tier and which service plans are provisioned, so we never recommend a capability you cannot buy — and so we can tell you about paid seats nobody is using and security features you already own but have switched off.
GET /devicesDevice.Read.AllRegistered devices: managed, compliant, personally owned, jailbroken or rooted, and stale. A device registered to your tenant is a way in, whether or not anyone remembers registering it.
GET /oauth2PermissionGrantsDirectory.Read.AllWhich applications hold standing access to your company's data, and what they were granted. This is the quietest serious risk in a Microsoft tenant: an application consented to years ago by someone who has left still holds its access, and nothing in the console draws attention to it.
GET /servicePrincipals?$select=…verifiedPublisher…Application.Read.AllNames the applications behind those grants and reports whether the publisher is verified by Microsoft. Without this the report would say 7b15cff7-3b15-… where it needs to say Azure DevOps, and nobody can make a decision about a GUID.
GET /auditLogs/signIns (default lookback 7 days)AuditLog.Read.All — and Microsoft Entra ID P1Legacy authentication actually in use, sign-ins flagged as risky, impossible geography, and sign-ins where no Conditional Access policy applied at all. This is the one entry on the list where the permission is not enough on its own; see what your licence lets us see.
GET /users/{id}/mailFolders/inbox/messageRulesMailboxSettings.ReadInbox rules that forward mail outside your organisation or hide messages by marking them read and moving them out of sight — the classic persistence trick after a business email compromise. Bounded to 200 mailboxes, administrators and recently-active accounts first.

What is deliberately not on the list

The absences are as much a design decision as the entries.

  • No Mail.Read. We read the rules on an inbox, not the messages in it. MailboxSettings.Read grants the former and not the latter. We can tell you that a rule forwards everything from your finance director to an outside address; we cannot read what it forwarded.
  • No write permission of any kind for discovery. The read path used by discovery is deliberately read-only and has no exported write method, so the set of writes this connector can perform stays enumerable rather than being whatever a caller improvises.
  • No Files.Read.All or SharePoint content access. Document content is outside the scope of the posture assessment.
  • No Directory.ReadWrite.All. Where a change is proposed, it goes through the change set with a named action, a stated blast radius and a recorded undo path — after classification, and after you approve anything that is not provably safe and reversible.

Why we ask for named fields

You will notice $select on almost every call. That is not a performance tweak. Graph returns a much larger default projection for most resources, and taking it would mean holding personal data about your staff that no check in the pack consults.

Graph returns a much larger default projection; asking for what we use keeps responses small and avoids pulling personal data we have no purpose for, which is a GDPR data-minimisation requirement, not an optimisation.
internal/integration/connectors/msgraph — comment on the user field list

The practical consequence for you is that the field list is short, fixed, and reviewable. If a check needs a new attribute, the attribute is added to the projection and the change is visible in the code that reads it, rather than the data having been collected all along in case somebody wanted it.

The bounds we put on ourselves

Two limits exist to keep discovery inside its half-hour budget and to keep it from hammering your tenant.

200 pages per paged enumeration
Any list we follow through @odata.nextLink stops after 200 pages. A misbehaving proxy returning a self-referential link would otherwise spin forever inside your onboarding run. Hitting the cap is recorded as a coverage gap and reported to you — never silently truncated, because a truncated inventory that looks complete is the dangerous outcome.
200 mailboxes for inbox rules
One Graph call per mailbox makes this the most expensive stage by a distance. Administrators are scanned first, then users with a mailbox ordered by most recent sign-in, so the bound degrades coverage in the least harmful order. If we stop early you are told: We checked mailbox rules on the 200 most important accounts rather than all N.

A handful of failures inside the mailbox stage is normal — an account with no mailbox returns a 404 — and is not reported as a gap, because a report that cries wolf gets ignored. Every mailbox failing means the permission is missing, and that is reported.

Fatal, degraded, and everything in between

Only one permission is load-bearing for the whole run. Without Organization.Read.All we cannot identify your tenant, and a report that cannot say whose estate it describes is worthless, so the run stops with cannot identify tenant. Every other permission on the list is partial: leave it out and you lose the checks that depend on that dataset, and you are told which ones and why.

That is the whole design. A report covering eight of ten areas is worth a great deal and no report is worth nothing — but a report covering eight areas while implying it covered ten is worse than either. Which is why the affected checks come back marked skipped, with the missing dataset named, rather than passed. See When a check cannot run.

Auditing what we did with it

Every request we make is attributable to the application you created, so your own tenant audit log is an independent record of our activity that we cannot edit. Filter the Entra sign-in logs by the client ID from your app registration and you will see our token acquisitions; the Graph activity appears against the same service principal. If our account of what we read and your tenant's account of what we read ever diverge, believe your tenant.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in connecting microsoft 365