Fonction
Une fonction est une responsabilité transverse attribuée à un utilisateur, distincte de son profil. Elle répond à la question « que cette personne a-t-elle le droit d'administrer, et où ? ».
C'est une distinction essentielle : entcore ne connaît que cinq profils (Teacher, Personnel,
Student, Relative, Guest). Toute population qui ne se réduit pas à l'un d'eux doit être
modélisée par une fonction.
Fonctions disponibles
Code (externalId) | name | Profil porteur | Rôle |
|---|---|---|---|
SUPER_ADMIN | SuperAdmin | — | Administrateur central, portée globale |
ADMIN_LOCAL | AdminLocal | Personnel | Administrateur d'un ou plusieurs établissements |
CLASS_ADMIN | ClassAdmin | Teacher | Administrateur d'une classe |
ADMIN_INSPECTION | Inspection | Personnel | Non-enseignants des services académiques : corps d'inspection, personnels détachés du rectorat |
ADMIN_COLLECTIVITE | Collectivite | Personnel | Non-enseignants d'une collectivité locale : techniciens et ouvriers de service |
Les deux dernières correspondent aux profils transversaux du SDET. Elles existent en tant que
fonctions et non en tant que profils parce que, du point de vue d'entcore, ces deux populations
sont indistinctement des Personnel.
Le champ AAF qui sépare ces populations, categoriePersonne, est bien importé — le mapping
PersEducNat.json le range dans l'attribut category — mais il n'est ensuite consommé que par
l'export ELIOT. Rien n'en subsiste dans Neo4j. De même, PersonnelImportProcessing.detectProfile()
ne peut retourner que PROFILE_TEACHER ou PROFILE_PERSONNEL. La fonction est donc le seul
support durable et interrogeable de cette distinction.
Structure du nœud
Un nœud Function est rattaché à un Profile par la relation COMPOSE, et à un
utilisateur par la relation HAS_FUNCTION.
(:Profile)<-[:COMPOSE]-(:Function)<-[:HAS_FUNCTION]-(:User)
| Propriété | Description |
|---|---|
externalId | Code de la fonction (ADMIN_LOCAL, ADMIN_INSPECTION…). C'est la clé d'appariement. |
id | Identifiant technique |
name | Libellé, utilisé pour nommer les FunctionGroup générés |

Le périmètre : rf.scope
La relation HAS_FUNCTION porte une propriété scope : la liste des identifiants
d'établissements sur lesquels la fonction s'exerce. SUPER_ADMIN n'en a pas — sa portée est
globale.
À l'octroi (POST /directory/user/function/:userId), le paramètre inherit détermine comment ce
périmètre est calculé :
inherit | Effet |
|---|---|
| (vide) | Le scope est exactement la liste fournie |
s | Le scope est étendu à tous les établissements rattachés, en parcourant HAS_ATTACHMENT*0.. |
sc | Comme s, en y ajoutant les classes (BELONGS*0..1) |
C'est le mécanisme qui permet de couvrir un réseau entier — un département, une circonscription — en désignant sa seule structure parente.
inherit: "s" matérialise la liste des descendants au moment de l'octroi et l'écrit dans
rf.scope. Un établissement rattaché au parent après coup n'y figure donc pas.
La conséquence est plus sévère qu'une simple absence de couverture : le contrôle
AdmlOfStructures avec inherit=true exige que tous les descendants soient dans le scope
(LENGTH(FILTER(sId IN sIds WHERE sId IN {scope})) = LENGTH(sIds)). Un seul enfant manquant fait
donc échouer le contrôle en entier, et l'utilisateur perd l'accès au parent qu'il détenait.
Il faut réaligner le périmètre après chaque rattachement, en rejouant l'octroi (les requêtes
d'addFunction sont des MERGE, l'opération est donc sans danger).
De la fonction au droit effectif
Une fonction, à elle seule, ne confère aucun droit. Elle sert deux choses : les filtres de
sécurité qui l'interrogent directement (AdmlOfStructures, AdminFilter…), et surtout la
génération de groupes qui, eux, portent les droits.
Quand une fonction est octroyée avec un scope, User.addFunction crée un FunctionGroup par
établissement du périmètre :
MERGE (fg:Group:FunctionGroup { externalId : s.id + '-' + {functionCode}})
ON CREATE SET fg.name = s.name + '-' + f.name, fg.filter = f.name
CREATE UNIQUE s<-[:DEPENDS]-fg<-[:IN {source:'MANUAL'}]-u
Les droits applicatifs se posent ensuite sur ces groupes, via des rôles :
Sur un réseau de 200 collèges, un octroi produit 200 FunctionGroup. Comme les rôles s'attribuent
groupe par groupe, l'opération n'est pas réalisable à la main : elle doit être scriptée.
Requêtes
Toutes les fonctions et leur profil porteur
MATCH (p:Profile)<-[:COMPOSE]-(f:Function)
RETURN p.name AS profil, f.externalId AS code, f.name AS nom
ORDER BY f.externalId
Tous les utilisateurs avec le rôle ADMIN_LOCAL
MATCH (u:User)-[:HAS_FUNCTION]->(f:Function)
WHERE f.name = "AdminLocal"
RETURN u.id AS userId, u.login AS login, f.name AS functionName
Périmètre d'un utilisateur pour une fonction
MATCH (u:User {id: {userId}})-[rf:HAS_FUNCTION]->(f:Function {externalId: {code}})
RETURN f.externalId AS fonction, LENGTH(rf.scope) AS nbEtablissements, rf.scope AS scope
Droits effectifs apportés par une fonction
Utile pour vérifier qu'une fonction n'accorde ni plus ni moins que prévu.
MATCH (u:User {id: {userId}})-[:IN]->(fg:FunctionGroup)-[:AUTHORIZED]->(r:Role)-[:AUTHORIZE]->(a:Action)
WHERE fg.externalId ENDS WITH {code}
RETURN DISTINCT r.name AS role, a.displayName AS action
ORDER BY role, action
Racines d'un périmètre déjà octroyé
rf.scope ne contient que la liste aplatie : la structure parente d'origine n'est stockée nulle
part. On la retrouve en prenant les établissements du scope dont aucun parent n'appartient
lui-même au scope.
MATCH (u:User {id: {userId}})-[rf:HAS_FUNCTION]->(:Function {externalId: {code}})
WITH rf.scope AS scope
MATCH (s:Structure) WHERE s.id IN scope
OPTIONAL MATCH (s)-[:HAS_ATTACHMENT]->(p:Structure)
WITH scope, s, COLLECT(p.id) AS parents
WHERE NONE(pid IN parents WHERE pid IN scope)
RETURN COLLECT(s.id) AS racines
Établissements rattachés absents d'un périmètre
Détecte les fonctions à réaligner après un rattachement.
MATCH (u:User)-[rf:HAS_FUNCTION]->(f:Function {externalId: {code}})
WITH u, f, rf.scope AS scope
MATCH (s:Structure) WHERE s.id IN scope
MATCH (s)<-[:HAS_ATTACHMENT]-(enfant:Structure)
WHERE NOT enfant.id IN scope
RETURN u.login AS utilisateur, COLLECT(DISTINCT enfant.name) AS horsPerimetre
Créer une nouvelle fonction
Le nœud doit être rattaché à un profil, faute de quoi l'octroi échoue silencieusement :
addFunction fait un OPTIONAL MATCH sur la fonction et, si elle est absente, ne rattache rien
sans lever d'erreur.
MATCH (p:Profile {externalId: 'PROFILE_PERSONNEL'})
CREATE p<-[:COMPOSE]-(f:Function {externalId: 'ADMIN_INSPECTION', id: 'admin-inspection', name: 'Inspection'})
RETURN f.id AS id
C'est la transposition de Profile.createFunction (module feeder), qui crée de la même manière
ADMIN_LOCAL sous Personnel et CLASS_ADMIN sous Teacher.
Ensuite, l'octroi passe par l'API standard, aucune modification d'entcore n'étant nécessaire —
AddFunctionFilter accepte tout code non vide autre que SUPER_ADMIN, dès lors que l'appelant est
administrateur du périmètre demandé :
POST /directory/user/function/:userId
{ "functionCode": "ADMIN_INSPECTION", "scope": ["<id-structure-parente>"], "inherit": "s" }
Les niveaux livrés par les modules (Lecture, Gestion, Administration) ne correspondent pas
toujours au besoin. Pour le cahier de textes par exemple, Lecture et Gestion ne portent pas
administrator.visa.read, alors qu'Administration ajoute session.manage et homework.manage,
c'est-à-dire l'écriture dans les cahiers de textes des enseignants. Un rôle sur mesure
(POST /appregistry/role) est souvent préférable à un niveau approchant. Vérifiez toujours le
résultat avec la requête « Droits effectifs » ci-dessus.