Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | 1x 1x 1x 1x 1x | /**
* Middlewares de contexte groupe — Resolution, permissions et quotas.
*
* Trois middlewares principaux :
* - requireGroupContext() — resout le groupe actif via X-Group-Id
* - requireGroupPermission(zone, minLevel) — verifie le niveau d'acces
* - requireGroupServer() — verifie que le serveur est lie au groupe
*
* Deux middlewares de quotas :
* - checkSeatQuota() — verifie le quota de membres par groupe
* - checkGroupsPerServerQuota() — verifie le quota de groupes par serveur
*
* @module middleware/groupContext
*/
const { resolveGroupContext, satisfiesLevel, VALID_ZONES } = require('../helpers/groupAccess');
const { pool: db } = require('../db');
const { getPlanLimits } = require('../config/plans');
/**
* Resout le contexte de groupe a partir du header X-Group-Id.
*
* Comportement :
* - X-Group-Id present : validation obligatoire
* - X-Group-Id absent + owner : mode global (req.access = null)
* - X-Group-Id absent + membre : 403
*
* @returns {Function} Express middleware
*/
function requireGroupContext() {
return async (req, res, next) => {
try {
const groupId = req.headers['x-group-id'];
if (!groupId) {
// Pas de header — mode global owner autorise, membre refuse
req.access = null;
return next();
}
const access = await resolveGroupContext(req.user.id, groupId);
if (!access) {
return res.status(403).json({ error: 'Acces refuse a ce groupe.' });
}
req.access = access;
next();
} catch (err) {
next(err);
}
};
}
/**
* Verifie que l'utilisateur a le niveau de permission requis sur une zone.
*
* L'owner bypass toutes les verifications.
* En mode global (req.access = null), passe directement (owner uniquement).
*
* @param {string} zone — Zone fonctionnelle (ex: 'docker', 'logs')
* @param {string} minLevel — Niveau minimum ('read' ou 'write')
* @returns {Function} Express middleware
*/
function requireGroupPermission(zone, minLevel) {
return (req, res, next) => {
// Mode global owner — pas de filtrage groupe
if (!req.access) return next();
// Owner du groupe — bypass tout
if (req.access.isOwner) return next();
// Zone inconnue — refus
if (!VALID_ZONES.includes(zone)) {
return res.status(403).json({ error: 'Zone inconnue.' });
}
const actual = req.access.permissions[zone] || 'none';
if (!satisfiesLevel(actual, minLevel)) {
return res.status(403).json({
error: `Permission insuffisante : ${zone} requiert ${minLevel}.`,
code: 'GROUP_PERMISSION_DENIED',
zone,
required: minLevel,
actual,
});
}
next();
};
}
/**
* Verifie que le serveur de la route est lie au groupe actif.
*
* Lit `req.params.serverId` et verifie contre `req.access.serverIds`.
* En mode global (req.access = null), verifie la propriete classique.
*
* @returns {Function} Express middleware
*/
function requireGroupServer() {
return async (req, res, next) => {
try {
const serverId = req.params.serverId;
if (!serverId) return next();
// Mode global owner — pas de verification ici, les handlers existants
// font deja leur propre check ownsServer/user_id
if (!req.access) return next();
// Mode groupe — verifier le lien groupe/serveur
if (!req.access.serverIds.includes(serverId)) {
return res.status(403).json({ error: 'Serveur non lie a ce groupe.' });
}
next();
} catch (err) {
next(err);
}
};
}
/**
* Verifie le quota de seats (membres + invitations actives) pour un groupe.
*
* @returns {Function} Express middleware
*/
function checkSeatQuota() {
return async (req, res, next) => {
try {
const groupId = req.params.groupId || req.access?.groupId;
if (!groupId) return next();
// Charger le plan de l'owner du groupe
const { rows: groupRows } = await db.query(
`SELECT ag.owner_user_id, u.plan
FROM access_groups ag
JOIN users u ON u.id = ag.owner_user_id
WHERE ag.id = $1`,
[groupId],
);
if (groupRows.length === 0) return res.status(404).json({ error: 'Groupe introuvable.' });
const limits = await getPlanLimits(groupRows[0].plan);
const maxUsers = limits.users;
if (maxUsers === Infinity) return next();
// Compter membres actifs + invitations non expirees
const { rows } = await db.query(
`SELECT
(SELECT COUNT(*) FROM access_group_members WHERE group_id = $1 AND status = 'active') +
(SELECT COUNT(*) FROM access_group_invitations WHERE group_id = $1 AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > NOW())
AS total`,
[groupId],
);
const total = Number.parseInt(rows[0].total);
if (total >= maxUsers) {
return res.status(403).json({
error: `Limite de membres atteinte : ${total}/${maxUsers} (plan ${groupRows[0].plan}).`,
code: 'PLAN_QUOTA_EXCEEDED',
resource: 'users',
current: total,
limit: maxUsers,
current_plan: groupRows[0].plan,
upgrade_url: '/settings#billing',
});
}
next();
} catch (err) {
next(err);
}
};
}
/**
* Verifie le quota de groupes par serveur selon le plan de l'owner.
*
* @param {string} serverId — ID du serveur a verifier (ou depuis req.body.server_id)
* @returns {Function} Express middleware
*/
function checkGroupsPerServerQuota() {
return async (req, res, next) => {
try {
const plan = req.user?.plan || 'free';
const limits = await getPlanLimits(plan);
const maxGroups = limits.groups_per_server;
if (maxGroups === 0) {
return res.status(403).json({
error: 'Les groupes ne sont pas disponibles pour votre plan.',
code: 'PLAN_FEATURE_REQUIRED',
feature: 'groups_per_server',
current_plan: plan,
required_plan: 'business',
upgrade_url: '/settings#billing',
});
}
if (maxGroups === Infinity || maxGroups === -1) return next();
// Compter les groupes existants pour les serveurs cibles
const serverIds = req.body?.server_ids || (req.body?.server_id ? [req.body.server_id] : []);
for (const serverId of serverIds) {
const { rows } = await db.query(
`SELECT COUNT(*) FROM access_group_servers ags
JOIN access_groups ag ON ag.id = ags.group_id
WHERE ags.server_id = $1`,
[serverId],
);
const count = Number.parseInt(rows[0].count);
if (count >= maxGroups) {
return res.status(403).json({
error: `Limite de groupes par serveur atteinte : ${count}/${maxGroups} (plan ${plan}).`,
code: 'PLAN_QUOTA_EXCEEDED',
resource: 'groups_per_server',
current: count,
limit: maxGroups,
current_plan: plan,
upgrade_url: '/settings#billing',
});
}
}
next();
} catch (err) {
next(err);
}
};
}
module.exports = {
requireGroupContext,
requireGroupPermission,
requireGroupServer,
checkSeatQuota,
checkGroupsPerServerQuota,
};
|