Permissões *.Selected no Microsoft Graph e SharePoint Online: o guia técnico definitivo
Entra ID · Microsoft Graph · SharePoint Online

Permissões *.Selected no Microsoft Graph e SharePoint Online: o guia técnico definitivo

Como aplicar o princípio do menor privilégio de verdade — do site inteiro até um único arquivo — com managed identities, PowerShell e a API do Graph. Inclui scripts prontos e um laboratório para você testar.

Por Chesley Silva · Microsoft 365 · Segurança de Identidade · Leitura ~16 min

Durante anos, a resposta para “meu app precisa ler uma biblioteca de documentos em um site do SharePoint” foi constrangedora: conceda Sites.Read.All — e pronto, seu aplicativo passa a enxergar todos os sites do tenant. É o oposto do princípio do menor privilégio (least privilege). O escopo Sites.Selected nasceu para resolver isso, e hoje o Microsoft Graph foi além: temos escopos granulares que descem até o nível de lista, item de lista e arquivo.

Este artigo é uma expansão técnica — em português, aterrada na documentação oficial do Microsoft Learn — do excelente post de Laura Kokkarinen (Microsoft MVP), cujos scripts servem de base para os exemplos aqui. Meu objetivo é que você não apenas execute os scripts, mas entenda exatamente o que cada etapa faz no token, no service principal e na herança de permissões do SharePoint — e que consiga testar tudo antes de levar para produção.

1. O modelo mental: por que *.Selected exige três etapas

A primeira coisa a internalizar: os escopos *.Selected não funcionam como os escopos globais. Com Files.Read.All, no instante em que o admin consente, o app já lê arquivos. Com *.Selected é diferente — o consentimento é apenas uma delegação de gestão. Segundo a documentação do Graph, um app consentido para Lists.SelectedOperations.Selected começa sem acesso a nada. É preciso uma atribuição explícita, recurso por recurso.

Na prática, o acesso só existe quando as três condições abaixo são verdadeiras ao mesmo tempo:

  • O app está consentido no Entra ID com o escopo *.Selected (aplicação ou delegado). É a Etapa 1 deste guia.
  • O app recebeu permissão sobre o recurso específico via chamada de POST .../permissions com um papel (role). É a Etapa 2.
  • O app apresenta um token válido contendo o escopo *.Selected na chamada ao recurso.
Por que isso importa para segurança

Como as três etapas são independentes, o administrador ganha dois pontos de corte: remover a permissão de um recurso específico (DELETE .../permissions/{id}) ou revogar o consentimento do escopo no Entra ID. Qualquer um dos dois derruba o acesso. Guarde isso — voltaremos a ele na seção de revogação.

2. Os quatro escopos e quando usar cada um

O Microsoft Graph expõe quatro escopos *.Selected. Todos suportam modo de aplicação (app-only, sem usuário) e delegado (em nome de um usuário). Quanto mais específico o escopo, menor a superfície de ataque.

EscopoNível de acessoUse quando…
Sites.SelectedColeção de site inteirao app precisa operar em um site inteiro (ou você vai usar a API REST do SharePoint)
Lists.SelectedOperations.SelectedUma lista/biblioteca específicao app só toca em uma lista ou biblioteca de documentos
ListItems.SelectedOperations.SelectedItem de lista, pasta ou arquivoo app opera sobre itens/pastas/arquivos específicos (o mais abrangente dos granulares)
Files.SelectedOperations.SelectedArquivo ou pasta de bibliotecao app só manipula arquivos dentro de bibliotecas de documentos
Nomenclatura que confunde Você vai ver Sites.Selected “sozinho” e os outros três como tuplas completas *.SelectedOperations.Selected. Não há diferença funcional entre os dois formatos — a mudança é só de convenção de nomes, adotada quando os escopos mais novos foram criados. Não perca tempo procurando um significado técnico na diferença; não existe.

Files vs. ListItems: a distinção sutil

No SharePoint, todo arquivo é um item de lista, mas nem todo item de lista é um arquivo. Consequência prática: um app com ListItems.SelectedOperations.Selected alcança itens de lista e arquivos (até o papel permitido). Já Files.SelectedOperations.Selected só opera sobre arquivos dentro de bibliotecas de documentos. E — importante — esse comportamento é determinado pelo recurso de destino, não pelo caminho da API: tanto faz chamar /drives/{id}/items/{id} ou /sites/{id}/lists/{id}/items/{id}.

3. Microsoft Graph vs. API REST do SharePoint

O Sites.Selected existe nas duas APIs. Qual conceder depende de qual API o seu código chama. O Graph é a escolha preferencial, mas nem toda operação está disponível nele — então chamar a API REST do SharePoint ainda é necessário em muitos cenários (retenção, alguns metadados administrativos, operações de tenant admin etc.).

APIApp ID (fixo em todos os tenants)Escopos *.Selected suportados
Microsoft Graph00000003-0000-0000-c000-000000000000Todos os quatro
SharePoint Online REST00000003-0000-0ff1-ce00-000000000000Apenas Sites.Selected
Bom saber

Se o seu app usa as duas APIs e você concedeu Sites.Selected em ambas para acessar o mesmo site, basta conceder um único papel no recurso (o mais permissivo que precisar). O grant de recurso não é separado por API — não existe “permissão de recurso do Graph” e “permissão de recurso do SharePoint”. É o mesmo vínculo.

4. Papéis (roles) e o que cada um permite

Ao vincular o app a um recurso, você escolhe um papel. Eles são cumulativos — o mais permissivo já inclui os menos permissivos —, então você nunca precisa listar mais de um.

PapelO que permite
readLer metadados e conteúdo do recurso.
writeLer e modificar metadados e conteúdo.
ownerPapel de proprietário do recurso.
fullcontrolControle total do recurso.

Quem pode gerenciar essas permissões?

Um detalhe que costuma travar automações: gerenciar (criar/remover) permissões em cada nível exige, por sua vez, permissões suficientes de quem chama a API. A documentação do Graph resume assim:

Nível do recursoPermissões necessárias para gerenciar
SiteSites.FullControl.All
ListaSites.FullControl.All, ou Sites.Selected+FullControl/Owner
Item de listaos acima, ou Lists.SelectedOperations.Selected+FullControl/Owner
Arquivoos acima, ou Lists.SelectedOperations.Selected+FullControl/Owner

É por isso que, nos scripts, usamos o PnP.PowerShell com a permissão delegada AllSites.FullControl: o administrador precisa de controle total para conseguir gravar os grants de recurso em nome dele.

5. Etapa 1 — Conceder o escopo *.Selected à managed identity

Se você já tentou conceder API permissions a uma managed identity pelo portal do Entra ID, notou que não existe botão para isso — diferente de um app registration comum. A saída é o PowerShell, atribuindo app roles diretamente ao service principal da managed identity.

Precisamos de três informações: o Tenant ID, o Object ID da managed identity e o App ID da API (Graph e/ou SharePoint, ambos fixos). O script abaixo ainda verifica a versão do PowerShell e o módulo necessário antes de agir — porque, muitas vezes, quem executa é o admin do cliente, e não nós.

# ---- variáveis ----
$tenantId = "00000000-0000-0000-0000-000000000000"
$managedIdentityIds = @("<object-id-da-managed-identity>")   # OBJECT ID, não App ID

$apis = @{
  # Microsoft Graph
  "00000003-0000-0000-c000-000000000000" = @("Sites.Selected",
    "Lists.SelectedOperations.Selected", "ListItems.SelectedOperations.Selected",
    "Files.SelectedOperations.Selected")
  # SharePoint Online REST
  "00000003-0000-0ff1-ce00-000000000000" = @("Sites.Selected")
}

# login interativo — scopes necessários para atribuir app roles
Connect-MgGraph -TenantId $tenantId -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All" -NoWelcome

foreach ($appId in $apis.Keys) {
  $app = Get-MgServicePrincipal -Filter "AppId eq '$appId'"
  foreach ($mi in $managedIdentityIds) {
    foreach ($permission in $apis[$appId]) {
      $role = $app.AppRoles | Where-Object Value -eq $permission | Select-Object -First 1
      New-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $mi `
        -PrincipalId $mi -ResourceId $app.Id -AppRoleId $role.Id
    }
  }
}

Ao rodar, o admin autentica no navegador (os escopos AppRoleAssignment.ReadWrite.All e Application.Read.All são o que permite atribuir app roles). O script então localiza o service principal de cada API e cria a atribuição — na prática dizendo: “conceda esta permissão desta API a esta managed identity”. Se a permissão já existir, tratamos o erro específico e seguimos, em vez de explodir. O script completo, comentado em português, está no download ao final.

Object ID × Application ID

Na Etapa 1 usamos o Object ID da managed identity. Na Etapa 2 (a seguir) usaremos o Application ID. São coisas diferentes e trocá-los é a causa nº 1 de scripts que “rodam sem erro” mas não concedem nada. Ambos ficam em Entra ID > Aplicativos Empresariais, filtrando por Managed Identities.

6. Etapa 2 — Conceder acesso aos recursos específicos

Concedemos o escopo, mas — como vimos no modelo mental — isso sozinho não dá acesso a nada. Agora criamos o vínculo app ↔ recurso. Aqui usamos o PnP.PowerShell, e há um passo que assusta quem não conhece: registrar um app do Entra ID para o próprio PnP.PowerShell, via Register-PnPEntraIDApp.

Isso é necessário porque, hoje, o PnP.PowerShell exige um app registration single-tenant próprio no tenant de destino para autenticar (no passado era um app multi-tenant, menos seguro). Esse app recebe a permissão delegada AllSites.FullControl, para que o script consiga gravar os grants de permissão nos sites em nome do administrador.

$tenantName = "seutenant"                 # parte antes de .onmicrosoft.com
$azureAppName = "nome-do-app-azure"       # display name do grant
$managedIdentityAppId = "<application-id-da-managed-identity>"  # App ID, não Object ID!

# Recursos + papéis. Basta UM papel por recurso (o mais permissivo já inclui os demais).
# PnP aceita: Read, Write, Manage/Owner, FullControl (site usa "Manage"; lista/item/arquivo usam "Owner").
$permissions = @(
  [PSCustomObject]@{ Site="https://$tenantName.sharepoint.com/sites/vendas"; Permission="Read" }
  [PSCustomObject]@{ Site="https://$tenantName.sharepoint.com/sites/vendas"; List="Documents"; Permission="Write" }
  [PSCustomObject]@{ Site="https://$tenantName.sharepoint.com/sites/vendas"; List="Documents"; ListItemId=1; Permission="Owner" }
  [PSCustomObject]@{ Site="https://$tenantName.sharepoint.com/sites/vendas"; List="Documents"; RelativeFilePath="Contratos/Q3.docx"; Permission="FullControl" }
)

# registra (uma vez por tenant) o app do PnP.PowerShell
$appReg = Register-PnPEntraIDApp -ApplicationName "PnP.PowerShell" `
  -Tenant "$tenantName.onmicrosoft.com" -SharePointDelegatePermissions "AllSites.FullControl"

Connect-PnPOnline -Url "https://$tenantName-admin.sharepoint.com" -ClientId $appReg."AzureAppId/ClientId" -Interactive

foreach ($p in $permissions) {
  if ($p.RelativeFilePath) {
    Grant-PnPEntraIDAppFilePermission -AppIdentity $managedIdentityAppId -DisplayName $azureAppName `
      -Site $p.Site -List $p.List -Path $p.RelativeFilePath -Permissions $p.Permission
  } elseif ($p.ListItemId) {
    Grant-PnPEntraIDAppListItemPermission -AppIdentity $managedIdentityAppId -DisplayName $azureAppName `
      -Site $p.Site -List $p.List -ListItem $p.ListItemId -Permissions $p.Permission
  } elseif ($p.List) {
    Grant-PnPEntraIDAppListPermission -AppIdentity $managedIdentityAppId -DisplayName $azureAppName `
      -Site $p.Site -List $p.List -Permissions $p.Permission
  } else {
    Grant-PnPEntraIDAppSitePermission -AppId $managedIdentityAppId -DisplayName $azureAppName `
      -Site $p.Site -Permissions $p.Permission
  }
}
Papéis: Graph × PnP.PowerShell

A API do Graph nomeia os papéis como read, write, owner e fullcontrol. Já o parâmetro -Permissions dos cmdlets PnP usa a forma capitalizada e — atenção — diverge no papel intermediário: Grant-PnPEntraIDAppSitePermission aceita Read, Write, Manage, FullControl (usa Manage), enquanto os cmdlets de lista, item e arquivo aceitam Read, Write, Owner, FullControl (usam Owner). Além disso, os cmdlets Grant-* recebem o Application ID no parâmetro -AppId.

Cada objeto na lista $permissions define o quão fundo o grant desce: só Site → permissão de site (Sites.Selected); com List → nível de lista; com ListItemId → item; com RelativeFilePath → arquivo. Lembre que lista, item e arquivo só existem no Microsoft Graph — a API REST do SharePoint só entende site.

Como fazer o mesmo direto pela API do Graph (sem PnP)

Se você preferir não depender do PnP — por exemplo, dentro de uma automação que já fala Graph — o grant de recurso é um simples POST. Para um site:

POST https://graph.microsoft.com/v1.0/sites/{siteId}/permissions
Content-Type: application/json

{
  "roles": ["write"],
  "grantedToIdentities": [{
    "application": {
      "id": "<application-id-da-managed-identity>",
      "displayName": "Meu App"
    }
  }]
}

A lógica é idêntica para listas, itens e arquivos/pastas — muda apenas o endpoint. Uma resposta 201 Created confirma o vínculo.

7. O detalhe que quebra produção: herança e limites de escopo único

Atenção de arquitetura Conceder permissão de aplicação a uma lista, item ou arquivo quebra a herança de permissões naquele recurso. Cada quebra de herança consome um unique security scope, e o SharePoint Online tem um limite (documentado) por lista/biblioteca. Um app que sai criando grants em milhares de itens individuais pode estourar esse limite e degradar a performance da biblioteca inteira.

A boa notícia: permissão no nível de site (Sites.Selected) não quebra herança, porque o site é a raiz da árvore de herança. Como regra de projeto: prefira o escopo mais alto que ainda respeite o menor privilégio para o seu caso. Descer até o arquivo é poderoso, mas só faça isso quando o volume for pequeno e o isolamento realmente exigir.

Como o acesso é calculado (app-only vs. delegado)

O SharePoint armazena uma tripla (application ID, resource ID, role) — “o app X tem papel Y sobre o recurso Z”. No momento da chamada:

  • App-only (sem usuário): se houver um registro para o app no recurso (ou num pai por herança) e o papel permitir a operação, acesso concedido. É o cenário de maior risco, pois não há usuário limitando nada.
  • Delegado (em nome de um usuário): calcula-se a permissão do app e a do usuário, e faz-se a interseção. O app nunca excede o que o usuário pode; o usuário nunca excede (através do app) o que foi consentido. Prefira delegado sempre que possível.

8. Laboratório: monte, teste e valide você mesmo

Hands-on — do zero ao acesso comprovado

Um roteiro para você reproduzir tudo num tenant de teste (recomendo um tenant de Microsoft 365 Developer Program) e comprovar que o menor privilégio está funcionando.

  1. Provisione a identidade. Crie uma managed identity (ex.: numa Azure Function ou Automation Account) ou, para testar só o fluxo, um app registration. Anote o Object ID e o Application ID.
  2. Rode a Etapa 1 (grant-selected-permissions.ps1). Depois valide o app role no PowerShell:
    Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $objectId |
      Select-Object AppRoleId, ResourceDisplayName
  3. Prove que ainda não há acesso. Antes da Etapa 2, tente ler um site com um token app-only. Você deve receber 403 accessDenied. Isso confirma o modelo: escopo consentido ≠ acesso.
  4. Rode a Etapa 2 (grant-resource-specific-permissions.ps1) concedendo read em um site.
  5. Comprove o acesso concedido. Liste as permissões do site pelo Graph e confirme que sua identidade aparece:
    GET https://graph.microsoft.com/v1.0/sites/{siteId}/permissions
    Em seguida, leia o site — agora deve retornar 200.
  6. Comprove o isolamento. Tente ler um segundo site, para o qual você não concedeu nada. Deve voltar 403. Esse é o “antes e depois” que justifica todo o esforço: o app enxerga só o que você permitiu.
  7. Teste a granularidade. Repita concedendo apenas em uma lista (com Lists.SelectedOperations.Selected) e confirme que o app lê aquela lista, mas não as demais do mesmo site.
  8. Reverta. Rode revoke-resource-specific-permissions.ps1 e depois remove-selected-permissions.ps1, revalidando com os mesmos GET que o acesso desapareceu.
Dica de token para testar rápido

Para gerar um token app-only de teste sem escrever código, use o fluxo de client credentials com a sua identidade e chame o Graph pelo Graph Explorer ou pelo Invoke-RestMethod. O importante é que o token carregue o escopo *.Selected — caso contrário, mesmo com o grant de recurso, o acesso não acontece (é a terceira condição do modelo mental).

9. Revogando: os dois “kill switches”

Lembra dos dois pontos de corte? Eles se traduzem em dois scripts, com efeitos diferentes — e é crucial entender a diferença:

AçãoScriptEfeito
Remover grant de recursorevoke-resource-specific-permissions.ps1Corta o acesso àquele recurso. Revogar uma lista derruba o acesso à lista e a todos os itens dela, mesmo os com permissão explícita.
Remover o escopo (app role)remove-selected-permissions.ps1Corta o acesso a qualquer recurso concedido via aquele escopo — é o “kill switch” no nível do tenant.
Comportamento que surpreende Se um app tem Sites.* e Lists.*, com acesso a um site e a uma lista dele, e você revoga só o consentimento de Sites.*, o app mantém o acesso àquela lista (concedido via Lists.*). Os escopos são independentes: revogar um não derruba automaticamente o que foi concedido pelo outro.

10. Correção de mitos

Mito 1 “Consenti Sites.Selected, então o app já lê o site.” Falso. Sem o grant de recurso (Etapa 2) e sem o escopo no token, não há acesso. As três condições são obrigatórias e independentes.
Mito 2 Sites.Selected só existe no Microsoft Graph.” Falso. Existe também na API REST do SharePoint (App ID ...0ff1-ce00...). Os escopos granulares (lista/item/arquivo), esses sim, só existem no Graph.
Mito 3 “Preciso conceder o papel duas vezes se uso Graph e SharePoint no mesmo site.” Falso. O grant de recurso é único e serve às duas APIs. Conceda o papel uma vez (o mais permissivo necessário).
Mito 4 “Permissão de site quebra a herança, como qualquer permissão granular.” Falso. O nível de site é a raiz da herança e não a quebra. Quem quebra herança — e consome unique security scopes — são os grants em lista, item e arquivo.
Mito 5 “Na managed identity, uso o mesmo ID nas duas etapas.” Falso. Etapa 1 usa o Object ID; Etapa 2 usa o Application ID. Confundi-los faz o script “passar” sem conceder nada.

11. Opinião do Arquiteto

Os escopos *.Selected são, na minha visão, uma das melhorias de segurança mais subestimadas do ecossistema Microsoft 365 — e ao mesmo tempo uma das mais mal utilizadas. O motivo é humano, não técnico: dar Sites.Read.All “resolve na hora”, enquanto o fluxo de três etapas exige disciplina e um pouco mais de trabalho do admin. É exatamente aí que mora o valor. Todo escopo .All que você elimina é uma superfície de ataque que deixa de existir no dia em que aquele app for comprometido.

Minha recomendação prática: (1) padronize managed identity em vez de segredos/certificados sempre que a carga rodar no Azure; (2) escolha o escopo mais alto que ainda respeite o menor privilégio — na dúvida entre lista e site, e com volume alto de itens, fique no site para não estourar limites de escopo único; (3) prefira delegado a app-only quando houver usuário no fluxo; e (4) trate os scripts de revogação como parte do runbook, não como um “se um dia precisar”. Least privilege sem um caminho de revogação testado é meia segurança.

Por fim: automatize a Etapa 1 e a Etapa 2 num único runbook entregável ao admin do cliente, com os IDs parametrizados. Quanto menos fricção para o admin fazer a coisa certa, mais provável que a coisa certa seja feita.


Referências oficiais

Créditos: os scripts de referência foram criados por Laura Kokkarinen (Microsoft MVP) e aqui adaptados, comentados em português e expandidos com laboratório e correção de mitos. Todo o conteúdo técnico foi conferido contra a documentação oficial do Microsoft Learn.

Este conteúdo é gratuito e sempre será. Valide qualquer comando em ambiente de teste antes de aplicar em produção, e confira as políticas da sua organização.

Entra IDMicrosoft GraphSharePoint Online Managed IdentityPnP.PowerShellLeast Privilege Sites.SelectedSegurança de Aplicação

Deixe uma resposta