Skip to main content

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 ? ».

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)nameProfil porteurRôle
SUPER_ADMINSuperAdminAdministrateur central, portée globale
ADMIN_LOCALAdminLocalPersonnelAdministrateur d'un ou plusieurs établissements
CLASS_ADMINClassAdminTeacherAdministrateur d'une classe
ADMIN_INSPECTIONInspectionPersonnelNon-enseignants des services académiques : corps d'inspection, personnels détachés du rectorat
ADMIN_COLLECTIVITECollectivitePersonnelNon-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.

Pourquoi le SDET ne suffit pas à les distinguer

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
externalIdCode de la fonction (ADMIN_LOCAL, ADMIN_INSPECTION…). C'est la clé d'appariement.
idIdentifiant technique
nameLibellé, utilisé pour nommer les FunctionGroup générés

Liste des admins local

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é :

inheritEffet
(vide)Le scope est exactement la liste fournie
sLe scope est étendu à tous les établissements rattachés, en parcourant HAS_ATTACHMENT*0..
scComme 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.

Le périmètre est figé à l'octroi

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 :

Un groupe par établissement

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" }
Choisir le rôle avec soin

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.