Explorer
KNOW-PAT-083

FiveM - Validation ESX Job : Permissions metier server-authoritative et hierarchie

Domaine
framework
Type
pattern
Priorité
P1

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.job cote 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.job cote 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