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.
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.
Índice
- O modelo mental: por que
*.Selectedexige três etapas - Os quatro escopos e quando usar cada um
- Microsoft Graph vs. API REST do SharePoint
- Papéis (roles) e o que cada um permite
- Etapa 1 — Conceder o escopo
*.Selectedà managed identity - Etapa 2 — Conceder acesso aos recursos específicos
- O detalhe que quebra produção: herança e limites de escopo único
- Laboratório: monte, teste e valide você mesmo
- Revogando: os dois “kill switches”
- Correção de mitos
- Opinião do Arquiteto
- Referências oficiais
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 .../permissionscom um papel (role). É a Etapa 2. - O app apresenta um token válido contendo o escopo
*.Selectedna chamada ao recurso.
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.
| Escopo | Nível de acesso | Use quando… |
|---|---|---|
Sites.Selected | Coleção de site inteira | o app precisa operar em um site inteiro (ou você vai usar a API REST do SharePoint) |
Lists.SelectedOperations.Selected | Uma lista/biblioteca específica | o app só toca em uma lista ou biblioteca de documentos |
ListItems.SelectedOperations.Selected | Item de lista, pasta ou arquivo | o app opera sobre itens/pastas/arquivos específicos (o mais abrangente dos granulares) |
Files.SelectedOperations.Selected | Arquivo ou pasta de biblioteca | o app só manipula arquivos dentro de bibliotecas de documentos |
Nomenclatura que confunde Você vai verSites.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.).
| API | App ID (fixo em todos os tenants) | Escopos *.Selected suportados |
|---|---|---|
| Microsoft Graph | 00000003-0000-0000-c000-000000000000 | Todos os quatro |
| SharePoint Online REST | 00000003-0000-0ff1-ce00-000000000000 | Apenas Sites.Selected |
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.
| Papel | O que permite |
|---|---|
read | Ler metadados e conteúdo do recurso. |
write | Ler e modificar metadados e conteúdo. |
owner | Papel de proprietário do recurso. |
fullcontrol | Controle 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 recurso | Permissões necessárias para gerenciar |
|---|---|
| Site | Sites.FullControl.All |
| Lista | Sites.FullControl.All, ou Sites.Selected+FullControl/Owner |
| Item de lista | os acima, ou Lists.SelectedOperations.Selected+FullControl/Owner |
| Arquivo | os 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.
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
}
}
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
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.
- 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.
- Rode a Etapa 1 (
grant-selected-permissions.ps1). Depois valide o app role no PowerShell:Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $objectId | Select-Object AppRoleId, ResourceDisplayName - 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. - Rode a Etapa 2 (
grant-resource-specific-permissions.ps1) concedendoreadem um site. - Comprove o acesso concedido. Liste as permissões do site pelo Graph e confirme que sua identidade aparece:
Em seguida, leia o site — agora deve retornarGET https://graph.microsoft.com/v1.0/sites/{siteId}/permissions200. - 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. - 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. - Reverta. Rode
revoke-resource-specific-permissions.ps1e depoisremove-selected-permissions.ps1, revalidando com os mesmosGETque o acesso desapareceu.
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ção | Script | Efeito |
|---|---|---|
| Remover grant de recurso | revoke-resource-specific-permissions.ps1 | Corta 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.ps1 | Corta o acesso a qualquer recurso concedido via aquele escopo — é o “kill switch” no nível do tenant. |
Comportamento que surpreende Se um app temSites.*eLists.*, com acesso a um site e a uma lista dele, e você revoga só o consentimento deSites.*, o app mantém o acesso àquela lista (concedido viaLists.*). 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.Selectedsó 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
- Overview of Selected permissions in OneDrive and SharePoint — Microsoft Learn
- Create permission on a site — Microsoft Graph API
- Create permission on a list — Microsoft Graph API
- Create permission on a listItem — Microsoft Graph API
- Create permission on a driveItem — Microsoft Graph API
- SharePoint Online limits — Unique security scopes por lista/biblioteca
- Post original que inspirou este guia: Laura Kokkarinen — How to set up Microsoft Graph and SharePoint Online *.Selected API permissions
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.