Skip to content

Comment construire la gestion des membres de groupe avec NENE2 ​

Ce guide explique comment construire un système de groupe où les utilisateurs créent des groupes, invitent des membres avec des rôles (owner/admin/member), gèrent les appartenances et contrôlent les promotions de rôles.

Essai sur le terrain : FT138
Version NENE2 : ^1.5
Sujets couverts : appartenance basée sur les rôles, auto-adhésion du propriétaire, auto-départ, piège du mot réservé MySQL (groups), évaluation des vulnérabilités


Ce que nous construisons ​

  • POST /groups — créer un groupe (le créateur devient propriétaire)
  • GET /groups/{groupId}/members — lister les membres (membres uniquement)
  • POST /groups/{groupId}/members — ajouter un membre (owner/admin uniquement, rôle : member ou admin)
  • DELETE /groups/{groupId}/members/{userId} — supprimer un membre (owner/admin peut supprimer les autres ; n'importe qui peut s'auto-départ)
  • PUT /groups/{groupId}/members/{userId}/role — changer le rôle (owner uniquement)

Schéma de base de données — IMPORTANT : éviter groups comme nom de table ​

groups est un mot réservé en MySQL (utilisé dans GROUP BY). Utiliser user_groups à la place.

sql
-- SQLite
CREATE TABLE user_groups (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    name       TEXT    NOT NULL,
    owner_id   INTEGER NOT NULL,
    created_at TEXT    NOT NULL,
    FOREIGN KEY (owner_id) REFERENCES users(id)
);

CREATE TABLE memberships (
    id        INTEGER PRIMARY KEY AUTOINCREMENT,
    group_id  INTEGER NOT NULL,
    user_id   INTEGER NOT NULL,
    role      TEXT    NOT NULL DEFAULT 'member',
    joined_at TEXT    NOT NULL,
    UNIQUE (group_id, user_id),
    CHECK (role IN ('owner', 'admin', 'member')),
    FOREIGN KEY (group_id) REFERENCES user_groups(id),
    FOREIGN KEY (user_id)  REFERENCES users(id)
);
sql
-- MySQL
CREATE TABLE user_groups (
    id         INT          NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name       VARCHAR(255) NOT NULL,
    owner_id   INT          NOT NULL,
    created_at VARCHAR(32)  NOT NULL,
    FOREIGN KEY (owner_id) REFERENCES users(id)
) ENGINE=InnoDB;

CREATE TABLE memberships (
    id        INT         NOT NULL AUTO_INCREMENT PRIMARY KEY,
    group_id  INT         NOT NULL,
    user_id   INT         NOT NULL,
    role      VARCHAR(16) NOT NULL DEFAULT 'member',
    joined_at VARCHAR(32) NOT NULL,
    UNIQUE KEY uq_group_user (group_id, user_id),
    CONSTRAINT chk_role CHECK (role IN ('owner', 'admin', 'member')),
    FOREIGN KEY (group_id) REFERENCES user_groups(id),
    FOREIGN KEY (user_id)  REFERENCES users(id)
) ENGINE=InnoDB;

Enum de rôle avec méthodes de capacité ​

php
enum MemberRole: string
{
    case Owner  = 'owner';
    case Admin  = 'admin';
    case Member = 'member';

    public function canManageMembers(): bool
    {
        return $this === self::Owner || $this === self::Admin;
    }

    public function canChangeRoles(): bool
    {
        return $this === self::Owner;
    }
}

Les méthodes de capacité sur l'enum gardent la logique d'autorisation en dehors des gestionnaires.


Auto-adhésion du propriétaire à la création du groupe ​

Quand un groupe est créé, le propriétaire est automatiquement ajouté comme membre avec le rôle owner :

php
public function createGroup(string $name, int $ownerId, string $now): int
{
    $this->executor->execute(
        'INSERT INTO user_groups (name, owner_id, created_at) VALUES (?, ?, ?)',
        [$name, $ownerId, $now],
    );

    $groupId = (int) $this->executor->lastInsertId();

    // Le propriétaire est automatiquement membre avec le rôle 'owner'
    $this->executor->execute(
        'INSERT INTO memberships (group_id, user_id, role, joined_at) VALUES (?, ?, ?, ?)',
        [$groupId, $ownerId, 'owner', $now],
    );

    return $groupId;
}

Gestionnaire d'ajout de membre — validation du rôle ​

Le rôle owner ne peut pas être attribué via l'API add-member. Pattern TokenScope::tryFrom() appliqué à MemberRole::tryFrom() :

php
$role = MemberRole::tryFrom($roleValue);

if ($role === null || $role === MemberRole::Owner) {
    return $this->responseFactory->create(['error' => 'role must be member or admin'], 422);
}

Supprimer un membre — auto-départ et suppression par admin ​

Un membre peut quitter son propre groupe (auto-départ) sans droits admin. Les admins peuvent supprimer les autres. Le propriétaire ne peut jamais être supprimé :

php
$isSelfLeave = $actorId === $userId;

if (!$isSelfLeave && !$actorRole->canManageMembers()) {
    return $this->responseFactory->create(['error' => 'only owner or admin can remove members'], 403);
}

$targetRole = MemberRole::tryFrom($targetMembership['role']) ?? MemberRole::Member;

if ($targetRole === MemberRole::Owner) {
    return $this->responseFactory->create(['error' => 'cannot remove the group owner'], 422);
}

Démontage FK MySQL — l'ordre est important ​

Lors de la réinitialisation MySQL dans les tests, supprimer les tables dépendantes de FK d'abord avec FOREIGN_KEY_CHECKS = 0 :

php
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
$this->pdo->exec('DROP TABLE IF EXISTS memberships');
$this->pdo->exec('DROP TABLE IF EXISTS user_groups');
$this->pdo->exec('DROP TABLE IF EXISTS users');
$this->pdo->exec('SET FOREIGN_KEY_CHECKS = 1');

Évaluation des vulnérabilités (FT138) ​

Douze tests de vulnérabilité vérifient :

IDAttaqueRésultat attenduRésultat
VULN-AIDOR : non-membre lit la liste des membres403Pass
VULN-BIDOR : non-membre ajoute un membre403Pass
VULN-CMembre ordinaire essaie d'ajouter quelqu'un403Pass
VULN-DAdmin essaie de définir le rôle ownerpas 200Pass
VULN-EMembre essaie de se promouvoir en admin403Pass
VULN-FSupprimer le propriétaire du groupe422Pass
VULN-GX-User-Id manquant à la créationpas 201Pass
VULN-HX-User-Id non numériquepas 200Pass
VULN-IInjection SQL dans le nom de groupe201 (verbatim)Pass
VULN-JOpération de membre inter-groupe403Pass
VULN-KID de groupe négatif404Pass
VULN-LL'admin ne peut pas changer les rôles403Pass

Les 12 tests de vulnérabilité passent. Aucune vulnérabilité trouvée.


Pièges courants ​

PiègeCorrection
Utiliser groups comme nom de table en MySQLUtiliser user_groups — groups est un mot réservé MySQL
Propriétaire non auto-ajouté aux appartenancesINSERT l'appartenance owner dans createGroup()
Admin pouvant changer les rôlescanChangeRoles() retourne true uniquement pour Owner
Autoriser le rôle owner via l'API add-memberRejeter role === MemberRole::Owner → 422
Non-membre contournant 403 avec acteur manquantVérifier findMembership(groupId, actorId) !== null
DROP TABLE MySQL échoue avec les contraintes FKSET FOREIGN_KEY_CHECKS = 0 avant DROP

Publié sous licence MIT.