Aller au contenu principal

Créer une App Registration avec Sites.Selected pour SharePoint

Cette documentation décrit la procédure complète pour :

  1. Créer une App Registration dans Microsoft Entra ID (anciennement Azure AD).
  2. Lui accorder la permission applicative Microsoft Graph Sites.Selected.
  3. Lui attribuer un niveau d’accès (read, write, manage ou fullcontrol) sur un site SharePoint précis. Le niveau utilisé par défaut dans cette documentation est read.
  4. Récupérer les identifiants nécessaires : Tenant ID, Client ID, secret et Site ID.

1. Concepts et prérequis

Pourquoi Sites.Selected ?

Les permissions classiques (Sites.Read.All, Sites.ReadWrite.All, Sites.FullControl.All) donnent accès à tous les sites SharePoint du tenant. C'est rarement souhaitable.

Sites.Selected ne donne aucun accès par défaut. L'application n'accède qu'aux sites qui lui sont explicitement attribués, avec le niveau de droit choisi. C'est le modèle du moindre privilège recommandé.

Prérequis

ÉlémentDétail
Rôle pour créer l'appApplication Administrator, Cloud Application Administrator ou Global Administrator
Rôle pour le consentement adminPrivileged Role Administrator ou Global Administrator
Pour attribuer un siteUne identité (app ou compte admin SharePoint) disposant de Sites.FullControl.All
OutilsNavigateur (portail Entra) + l'un de : Graph Explorer, PowerShell (PnP ou MSGraph), ou une requête HTTP (Postman/curl)

Dans cette procédure, le compte utilisé possède le rôle Global Administrator.

2. Créer l'App Registration

Via le portail Entra

  1. Ouvrez le portail Azure ou le centre d’administration Microsoft Entra.
  2. Recherchez Microsoft Entra ID, puis ouvrez le service.
Recherche et ouverture de Microsoft Entra ID dans le portail Azure
  1. Cliquez sur Add > App registration.
Ajout d’une App registration depuis Microsoft Entra ID
  1. Renseignez les champs suivants :

    • Name : ex. svc-sharepoint-monapp
    • Supported account types : Single tenant only (single tenant), sauf besoin multi-tenant.
    • Redirect URI : laisser vide (inutile pour un flux applicatif/client credentials).
  2. Cliquez sur Register.

Formulaire d’enregistrement de l’application SharePoint

Récupérer les identifiants de base

Sur la page Overview de l'application, noter :

Application créée et identifiants Client ID et Tenant ID à récupérer
IdentifiantOù le trouverUsage
Application (client) IDOverviewAuthentification
Directory (tenant) IDOverviewAuthentification

3. Ajouter la permission Sites.Selected

  1. Dans l'app → menu API permissions → Add a permission.
  2. Choisir Microsoft Graph.
Ajout d’une permission Microsoft Graph à l’application
  1. Sélectionnez Application permissions et non Delegated permissions, car il s’agit d’un accès machine-to-machine.
  2. Recherchez Sites.Selected, cochez la permission, puis cliquez sur Add permissions.
Sélection de la permission applicative Sites.Selected
  1. Cliquez sur Grant admin consent for [tenant].
Bouton permettant d’accorder le consentement administrateur pour Sites.Selected
  1. Confirmez l’opération lorsque le portail le demande.
Confirmation du consentement administrateur dans Microsoft Entra ID

La colonne Status doit alors afficher une coche verte avec la mention Granted for [tenant].

Permission Sites.Selected accordée avec le consentement administrateur
Consentement obligatoire

Sans le consentement administrateur, la permission est configurée mais reste inactive.

4. Créer un secret (ou certificat)

L’application a besoin d’un moyen de s’authentifier. Un certificat est préférable en production ; cette procédure utilise un secret client.

  1. Depuis Overview, cliquez sur Add a certificate or secret. Vous pouvez aussi ouvrir Manage > Certificates & secrets.
Accès à la création d’un certificat ou d’un secret client
  1. Dans l’onglet Client secrets, cliquez sur New client secret.
  2. Saisissez une description, choisissez une durée d’expiration conforme à votre politique, puis cliquez sur Add.
Création d’un secret client avec sa description et sa durée d’expiration
  1. Copiez immédiatement la colonne Value, et non le Secret ID.
Copie immédiate de la valeur du secret client
Valeur affichée une seule fois

La valeur du secret n’est plus affichée après avoir quitté cette page. Conservez-la dans un coffre-fort de secrets, par exemple Azure Key Vault.

Récapitulatif des éléments d'authentification

ÉlémentDescription
tenantIdDirectory (tenant) ID
clientIdApplication (client) ID
clientSecretValeur du secret

5. Préparer l’attribution du site

Autorisation requise

Cette opération doit être effectuée par une identité disposant de Sites.FullControl.All, par exemple une application d’administration dédiée.

Niveaux de droits (roles) disponibles

RôleAccès
readLecture seule (Il s’agit du mode attribué par défaut dans cette doc)
writeLecture + écriture
manageGestion (inclut write + certaines actions de gestion)
fullcontrolContrôle total (équivalent propriétaire)

6. Attribuer le site avec le script Python

Ce script automatise trois étapes : obtention d’un jeton, résolution du site-id, puis attribution de la permission.

Identité qui exécute l’attribution

L’appel POST /sites/{site-id}/permissions exige que l’identité authentifiée dispose de Sites.FullControl.All.

Deux approches sont possibles :

  • Application d’administration dédiée — recommandée : une application « bootstrap » possédant Sites.FullControl.All attribue la permission à l’application cible qui possède Sites.Selected. Les identifiants ADMIN_CLIENT_ID et ADMIN_CLIENT_SECRET sont distincts de TARGET_CLIENT_ID.
  • Auto-attribution — temporaire uniquement : l’application cible reçoit provisoirement Sites.FullControl.All, s’attribue l’accès au site, puis cette autorisation globale est retirée. Cette approche réduit temporairement l’intérêt du moindre privilège.

Cette procédure utilise une application d’administration dédiée. Créez une seconde App Registration en suivant les étapes précédentes, puis ajoutez-lui la permission applicative Sites.FullControl.All.

Ajout de la permission applicative Sites.FullControl.All à l’application d’administration

Accordez le consentement administrateur, créez son secret, puis récupérez son Tenant ID, son Client ID et la valeur du secret.

Créez ensuite les trois fichiers suivants dans votre environnement Python.

.env

TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Identité qui exécute le grant (doit avoir Sites.FullControl.All)
ADMIN_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
ADMIN_CLIENT_SECRET=le-secret-de-l-app-admin
# App bénéficiaire (celle qui a Sites.Selected)
TARGET_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
TARGET_APP_NAME=svc-sharepoint-monapp
SITE_URL=https://contoso.sharepoint.com/sites/Finance
GRANT_ROLES=read

grant_sharepoint_permissions.py

"""
Gère les permissions (read/write/manage/fullcontrol) d'une app Azure AD
sur un site SharePoint précis, via Microsoft Graph (Sites.Selected).

Actions disponibles :
list – liste toutes les permissions du site
grant – attribue les rôles définis dans GRANT_ROLES à l'app cible
revoke – révoque la permission de l'app cible (par permission-id ou par client-id)

Usage :
python grant_sharepoint_permissions.py list
python grant_sharepoint_permissions.py grant
python grant_sharepoint_permissions.py revoke
python grant_sharepoint_permissions.py revoke --permission-id <id>
"""
import os
import sys
import json
import argparse
import requests
from dotenv import load_dotenv

load_dotenv()

TENANT_ID = os.environ["TENANT_ID"].strip()
ADMIN_CLIENT_ID = os.environ["ADMIN_CLIENT_ID"].strip()
ADMIN_CLIENT_SECRET = os.environ["ADMIN_CLIENT_SECRET"].strip()
TARGET_CLIENT_ID = os.environ["TARGET_CLIENT_ID"].strip()
TARGET_APP_NAME = os.environ.get("TARGET_APP_NAME", "svc-sharepoint-app").strip()
SITE_URL = os.environ["SITE_URL"].strip()
GRANT_ROLES = [r.strip() for r in os.environ.get("GRANT_ROLES", "read,write").split(",") if r.strip()]

# Déduit l'hôte + le chemin depuis SITE_URL
# ex. https://contoso.sharepoint.com/sites/Finance
_url_parts = SITE_URL.replace("https://", "").split("/", 1)
SP_HOST = _url_parts[0] # contoso.sharepoint.com
SP_SITE_PATH = _url_parts[1] if len(_url_parts) > 1 else "" # sites/Finance

GRAPH = "https://graph.microsoft.com/v1.0"


# ── Étape 1 : jeton d'accès ──────────────────────────────────────────────────
def get_token() -> str:
print("→ Récupération du jeton d'accès …")
url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
resp = requests.post(url, data={
"grant_type": "client_credentials",
"client_id": ADMIN_CLIENT_ID,
"client_secret": ADMIN_CLIENT_SECRET,
"scope": "https://graph.microsoft.com/.default",
})
resp.raise_for_status()
print(" ✓ Jeton obtenu")
return resp.json()["access_token"]


# ── Étape 2 : résolution du site-id ─────────────────────────────────────────
def get_site_id(token: str) -> str:
print(f"→ Résolution du site-id pour {SP_HOST}/{SP_SITE_PATH} …")
url = f"{GRAPH}/sites/{SP_HOST}:/{SP_SITE_PATH}"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
resp.raise_for_status()
site_id = resp.json()["id"]
print(f" ✓ site-id = {site_id}")
return site_id


# ── Action : list ────────────────────────────────────────────────────────────
def list_permissions(token: str, site_id: str) -> None:
print(f"→ Liste des permissions accordées à l'app {TARGET_CLIENT_ID} ({TARGET_APP_NAME}) …")
url = f"{GRAPH}/sites/{site_id}/permissions"
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
sys.exit(1)

all_permissions = resp.json().get("value", [])

# Filtre : uniquement les permissions dont l'application correspond à TARGET_CLIENT_ID
matched = []
for perm in all_permissions:
identities = perm.get("grantedToIdentities") or []
if not identities and perm.get("grantedTo"):
identities = [perm["grantedTo"]]
for identity in identities:
app = identity.get("application") or {}
if app.get("id") == TARGET_CLIENT_ID:
matched.append(perm)
break

if not matched:
print(f" (aucune permission trouvée pour l'app {TARGET_CLIENT_ID})")
return

print(f" ✓ {len(matched)} permission(s) trouvée(s) pour cette app :\n")
for perm in matched:
perm_id = perm.get("id", "—")
roles = ", ".join(perm.get("roles", []))
identities = perm.get("grantedToIdentities") or []
if not identities and perm.get("grantedTo"):
identities = [perm["grantedTo"]]
for identity in identities:
app = identity.get("application") or {}
name = app.get("displayName") or "—"
cid = app.get("id") or "—"
print(f" ┌ permission id : {perm_id}")
print(f" │ rôles : {roles}")
print(f" │ nom : {name}")
print(f" └ client id : {cid}")
print()


# ── Action : grant ───────────────────────────────────────────────────────────
def grant_permissions(token: str, site_id: str) -> dict:
print(f"→ Attribution {GRANT_ROLES} à l'app {TARGET_CLIENT_ID} ({TARGET_APP_NAME}) …")
url = f"{GRAPH}/sites/{site_id}/permissions"
payload = {
"roles": GRANT_ROLES,
"grantedToIdentities": [
{
"application": {
"id": TARGET_CLIENT_ID,
"displayName": TARGET_APP_NAME,
}
}
],
}
resp = requests.post(
url,
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=payload,
)
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
sys.exit(1)

result = resp.json()
print(" ✓ Permission attribuée avec succès !")
print(f" → permission id = {result.get('id')}")
print(json.dumps(result, indent=2))
return result


# ── Action : revoke ──────────────────────────────────────────────────────────
def revoke_permissions(token: str, site_id: str, permission_id: str | None = None) -> None:
"""
Révoque une permission.
- Si permission_id est fourni, supprime directement cette permission.
- Sinon, cherche parmi toutes les permissions celle(s) dont le client-id
correspond à TARGET_CLIENT_ID et les supprime.
"""
headers = {"Authorization": f"Bearer {token}"}

if permission_id:
perm_ids_to_delete = [permission_id]
else:
print(f"→ Recherche des permissions de l'app {TARGET_CLIENT_ID} …")
url = f"{GRAPH}/sites/{site_id}/permissions"
resp = requests.get(url, headers=headers)
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
print(json.dumps(resp.json(), indent=2))
sys.exit(1)

permissions = resp.json().get("value", [])
perm_ids_to_delete = []
for perm in permissions:
identities = perm.get("grantedToIdentities") or []
if not identities and perm.get("grantedTo"):
identities = [perm["grantedTo"]]
for identity in identities:
app = identity.get("application") or {}
if app.get("id") == TARGET_CLIENT_ID:
perm_ids_to_delete.append(perm["id"])
break

if not perm_ids_to_delete:
print(f" ✗ Aucune permission trouvée pour l'app {TARGET_CLIENT_ID} sur ce site.")
sys.exit(1)

print(f" ✓ {len(perm_ids_to_delete)} permission(s) à révoquer : {perm_ids_to_delete}")

for pid in perm_ids_to_delete:
print(f"→ Révocation de la permission {pid} …")
url = f"{GRAPH}/sites/{site_id}/permissions/{pid}"
resp = requests.delete(url, headers=headers)
try:
resp.raise_for_status()
except requests.HTTPError:
print(f"\n ✗ HTTP {resp.status_code}")
try:
print(json.dumps(resp.json(), indent=2))
except Exception:
print(resp.text)
sys.exit(1)
print(f" ✓ Permission {pid} révoquée avec succès !")


# ── Point d'entrée ───────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Gère les permissions Sites.Selected sur un site SharePoint via Microsoft Graph.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples :
python grant_sharepoint_permissions.py list
python grant_sharepoint_permissions.py grant
python grant_sharepoint_permissions.py revoke
python grant_sharepoint_permissions.py revoke --permission-id abc123
""",
)
parser.add_argument(
"action",
nargs="?",
default="list",
choices=["list", "grant", "revoke"],
help="Action à effectuer : list (défaut) | grant | revoke",
)
parser.add_argument(
"--permission-id",
dest="permission_id",
default=None,
help="(revoke uniquement) ID de la permission à révoquer. "
"Si omis, toutes les permissions de TARGET_CLIENT_ID sont révoquées.",
)
args = parser.parse_args()

tok = get_token()
sid = get_site_id(tok)

if args.action == "list":
list_permissions(tok, sid)
elif args.action == "grant":
grant_permissions(tok, sid)
elif args.action == "revoke":
revoke_permissions(tok, sid, permission_id=args.permission_id)


if __name__ == "__main__":
main()

requirements.txt

requests
python-dotenv

Installation

L’installation se fait dans un environnement virtuel (venv) dédié, puis avec pip ou pip3, selon votre système.

# 1) Créer l'environnement virtuel
python3 -m venv .venv # ou : python -m venv .venv

# 2) Activer le venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windows (PowerShell / CMD)

# 3) Installer les dépendances depuis requirements.txt
pip install -r requirements.txt # ou : pip3 install -r requirements.txt

# 4) Lancer le script (sans argument = action « list » par défaut)
python grant_sharepoint_permissions.py # ou : python3 grant_sharepoint_permissions.py

Sous-commandes disponibles

Le script accepte une action en argument. list est l’action par défaut : lancer le script sans argument équivaut à list.

# Ces deux commandes sont identiques
python grant_sharepoint_permissions.py
python grant_sharepoint_permissions.py list

Récapitulatif complet des commandes :

CommandeEffet
python grant_sharepoint_permissions.pyListe les permissions de l'app (défaut = list)
python grant_sharepoint_permissions.py listIdem
python grant_sharepoint_permissions.py grantAttribue les rôles de GRANT_ROLES à TARGET_CLIENT_ID
python grant_sharepoint_permissions.py revokeRévoque toutes les permissions de TARGET_CLIENT_ID
python grant_sharepoint_permissions.py revoke --permission-id <id>Révoque une permission spécifique par son id
Retrouver un identifiant de permission

Lancez d’abord list pour récupérer les permission id avant de révoquer une permission précise.

Variantes utiles

  • Changer le niveau de droit : modifier GRANT_ROLES dans le .env (read, read,write, manage, fullcontrol).
  • Auto-attribution (Cas B) : mettre les mêmes valeurs pour ADMIN_CLIENT_ID/SECRET et TARGET_CLIENT_ID (l'app doit alors avoir Sites.FullControl.All).

7. Dépannage

AADSTS7000215 — invalid client secret

Le secret est erroné ou expiré. Vérifier qu'on a copié la Value du secret (et non le Secret ID), qu'il n'a pas expiré, et qu'il n'y a pas d'espace parasite (d'où les .strip() dans le script).

403 Forbidden sur un appel au site

Le jeton est valide mais l'app n'a pas (ou pas le bon niveau de) permission sur ce site. Vérifier avec la sous-commande list, puis (ré)attribuer avec grant. Rappel : Sites.Selected ne donne accès qu'aux sites explicitement attribués.

Authorization_RequestDenied lors du grant

L'identité qui exécute l'attribution n'a pas Sites.FullControl.All. C'est l'app admin (ADMIN_CLIENT_ID) qui doit disposer de cette permission applicative avec consentement administrateur, pas l'app cible.

8. Récapitulatif des éléments à conserver

ÉlémentSourceSensible ?
Tenant IDOverview de l'appNon
Client IDOverview de l'appNon
Client Secret / CertificatCertificates & secretsOui à stocker en coffre-fort (Key Vault)
Site IDAppel Graph GET /sites/...Non
Permission IDRéponse du POST /permissionsNon, mais utile