Parent : [[INDEX-FIVEM-FRAMEWORK]]
FiveM - Validation ESX Job : Permissions metier server-authoritative et hierarchie
Probleme
Dans ESX, les jobs sont stockes dans la base de donnees et charges cote client. Un joueur peut spoof son job cote client pour acceder a des fonctionnalites reservees (police, EMS, mecanicien). La validation du job doit etre exclusivement server-side, avec une hierarchie de grade pour les permissions granulaires.
Contexte
ESX (EssentialMode Extended) est le framework le plus utilise sur FiveM :
ESX.PlayerData.jobcote client (modifiable par un cheateur)xPlayer.getJob()cote serveur (source de verite)- Grades :
boss,lieutenant,officer,recruit(hierarchie par job) - Jobs multiples possibles (job principal + job secondaire avec ESX v2+)
Solution
1. Validation server-side du job
-- server.lua
RegisterNetEvent('police:arrest')
AddEventHandler('police:arrest', function(targetId)
local src = source
local xPlayer = ESX.GetPlayerFromId(src)
-- Validation 1 : Le joueur existe
if not xPlayer then
print(('[%s] Arrest attempt from invalid player'):format(src))
return
end
-- Validation 2 : Le joueur a bien le job police
if xPlayer.getJob().name ~= 'police' then
print(('[%s] Arrest attempt without police job (has %s)'):format(src, xPlayer.getJob().name))
logSuspiciousActivity(src, 'arrest_without_job')
return
end
-- Validation 3 : Le joueur a le grade minimum requis
local minGrade = 2
if xPlayer.getJob().grade < minGrade then
TriggerClientEvent('esx:showNotification', src, 'Grade insuffisant pour les arrestations')
return
end
-- Validation 4 : Distance physique
local srcPed = GetPlayerPed(src)
local targetPed = GetPlayerPed(targetId)
local distance = #(GetEntityCoords(srcPed) - GetEntityCoords(targetPed))
if distance > 5.0 then
print(('[%s] Arrest attempt from %.1fm away'):format(src, distance))
return
end
executeArrest(src, targetId, xPlayer.getJob().grade)
end)
2. Systeme de permissions granulaire par job et grade
JobPermissions = {
police = {
ranks = {
[0] = { label = 'Recrue', canPatrol = true, canArrest = false, canFine = false, canImpound = false },
[1] = { label = 'Officier', canPatrol = true, canArrest = true, canFine = true, canImpound = false },
[2] = { label = 'Sergent', canPatrol = true, canArrest = true, canFine = true, canImpound = true },
[3] = { label = 'Lieutenant', canPatrol = true, canArrest = true, canFine = true, canImpound = true, canManage = true },
[4] = { label = 'Chef', canPatrol = true, canArrest = true, canFine = true, canImpound = true, canManage = true, canHire = true },
}
},
ambulance = {
ranks = {
[0] = { label = 'Stagiaire', canTreat = true, canRevive = false, canTransport = false },
[1] = { label = 'Infirmier', canTreat = true, canRevive = true, canTransport = true },
[2] = { label = 'Medecin', canTreat = true, canRevive = true, canTransport = true, canSurgery = true },
[3] = { label = 'Chef', canTreat = true, canRevive = true, canTransport = true, canSurgery = true, canManage = true },
}
},
}
function hasPermission(playerId, action)
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then return false end
local job = xPlayer.getJob().name
local grade = xPlayer.getJob().grade
local jobConfig = JobPermissions[job]
if not jobConfig then return false end
local rankConfig = jobConfig.ranks[grade]
if not rankConfig then return false end
return rankConfig[action] == true
end
3. Cache cote serveur pour eviter les requetes DB excessives
local jobCache = {}
local cacheTTL = 30000
function getPlayerJobCached(playerId)
local now = GetGameTimer()
if jobCache[playerId] and (now - jobCache[playerId].timestamp) < cacheTTL then
return jobCache[playerId].job
end
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then
jobCache[playerId] = nil
return nil
end
local job = xPlayer.getJob()
jobCache[playerId] = { job = job, timestamp = now }
return job
end
AddEventHandler('esx:setJob', function(source, newJob, lastJob)
jobCache[source] = { job = newJob, timestamp = GetGameTimer() }
end)
Citizen.CreateThread(function()
while true do
Citizen.Wait(60000)
local now = GetGameTimer()
for playerId, cache in pairs(jobCache) do
if not GetPlayerName(playerId) or (now - cache.timestamp) > cacheTTL * 2 then
jobCache[playerId] = nil
end
end
end
end)
4. Verification de la presence en service (on-duty)
local onDutyPlayers = {}
function setOnDuty(playerId, isOnDuty)
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer then return false end
local job = xPlayer.getJob()
local dutyJobs = { 'police', 'ambulance', 'mechanic' }
if not hasValue(dutyJobs, job.name) then
return false, 'Ce metier n\'a pas de systeme de service'
end
onDutyPlayers[playerId] = { job = job.name, onDuty = isOnDuty, timestamp = os.time() }
Player(playerId).state:set('onDuty', isOnDuty, true)
return true
end
function isOnDuty(playerId)
local state = onDutyPlayers[playerId]
if not state then return false end
local xPlayer = ESX.GetPlayerFromId(playerId)
if not xPlayer or xPlayer.getJob().name ~= state.job then
onDutyPlayers[playerId] = nil
return false
end
return state.onDuty
end
Checklist de validation
- Job verifie avec
ESX.GetPlayerFromId(source).getJob()server-side uniquement - Jamais de trust du
ESX.PlayerData.jobcote client pour les actions critiques - Grades definis avec permissions granulaires (canArrest, canTreat, etc.)
- Distance physique validee server-side pour les interactions
- Jobs multiples : le bon job actif est identifie pour chaque action
- Cache serveur pour les requetes frequentes
- Systeme on-duty pour les jobs necessitant une presence en service
- Invalidation du cache quand le job change (
esx:setJob) - Logs pour les changements de job et les tentatives de permission denied
- Pas de logique metier cote client basee sur le job
- Les boutons/UI NUI sont affiches selon le job, mais l'action est validee server-side
References
- ESX Documentation : https://esx-framework.github.io/