Créer une App Registration avec Sites.Selected pour SharePoint
Cette documentation décrit la procédure complète pour :
- Créer une App Registration dans Microsoft Entra ID (anciennement Azure AD).
- Lui accorder la permission applicative Microsoft Graph
Sites.Selected. - Lui attribuer un niveau d’accès (
read,write,manageoufullcontrol) sur un site SharePoint précis. Le niveau utilisé par défaut dans cette documentation estread. - 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ément | Détail |
|---|---|
| Rôle pour créer l'app | Application Administrator, Cloud Application Administrator ou Global Administrator |
| Rôle pour le consentement admin | Privileged Role Administrator ou Global Administrator |
| Pour attribuer un site | Une identité (app ou compte admin SharePoint) disposant de Sites.FullControl.All |
| Outils | Navigateur (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
- Ouvrez le portail Azure ou le centre d’administration Microsoft Entra.
- Recherchez Microsoft Entra ID, puis ouvrez le service.
- Cliquez sur Add > App registration.
-
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).
- Name : ex.
-
Cliquez sur Register.
Récupérer les identifiants de base
Sur la page Overview de l'application, noter :
| Identifiant | Où le trouver | Usage |
|---|---|---|
| Application (client) ID | Overview | Authentification |
| Directory (tenant) ID | Overview | Authentification |
3. Ajouter la permission Sites.Selected
- Dans l'app → menu API permissions → Add a permission.
- Choisir Microsoft Graph.
- Sélectionnez Application permissions et non Delegated permissions, car il s’agit d’un accès machine-to-machine.
- Recherchez
Sites.Selected, cochez la permission, puis cliquez sur Add permissions.
- Cliquez sur Grant admin consent for [tenant].
- Confirmez l’opération lorsque le portail le demande.
La colonne Status doit alors afficher une coche verte avec la mention Granted for [tenant].
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.
- Depuis Overview, cliquez sur Add a certificate or secret. Vous pouvez aussi ouvrir Manage > Certificates & secrets.
- Dans l’onglet Client secrets, cliquez sur New client secret.
- Saisissez une description, choisissez une durée d’expiration conforme à votre politique, puis cliquez sur Add.
- Copiez immédiatement la colonne Value, et non le Secret ID.
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ément | Description |
|---|---|
tenantId | Directory (tenant) ID |
clientId | Application (client) ID |
clientSecret | Valeur du secret |
5. Préparer l’attribution du site
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ôle | Accès |
|---|---|
read | Lecture seule (Il s’agit du mode attribué par défaut dans cette doc) |
write | Lecture + écriture |
manage | Gestion (inclut write + certaines actions de gestion) |
fullcontrol | Contrô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.
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.Allattribue la permission à l’application cible qui possèdeSites.Selected. Les identifiantsADMIN_CLIENT_IDetADMIN_CLIENT_SECRETsont distincts deTARGET_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.
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 :
| Commande | Effet |
|---|---|
python grant_sharepoint_permissions.py | Liste les permissions de l'app (défaut = list) |
python grant_sharepoint_permissions.py list | Idem |
python grant_sharepoint_permissions.py grant | Attribue les rôles de GRANT_ROLES à TARGET_CLIENT_ID |
python grant_sharepoint_permissions.py revoke | Ré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 |
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_ROLESdans le.env(read,read,write,manage,fullcontrol). - Auto-attribution (Cas B) : mettre les mêmes valeurs pour
ADMIN_CLIENT_ID/SECRETetTARGET_CLIENT_ID(l'app doit alors avoirSites.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ément | Source | Sensible ? |
|---|---|---|
| Tenant ID | Overview de l'app | Non |
| Client ID | Overview de l'app | Non |
| Client Secret / Certificat | Certificates & secrets | Oui à stocker en coffre-fort (Key Vault) |
| Site ID | Appel Graph GET /sites/... | Non |
| Permission ID | Réponse du POST /permissions | Non, mais utile |