sistema-de-chamados/convex/usbPolicy.ts
rever-tecnologia 6007cf6740 Fix USB policy token hash bug
The getPendingUsbPolicy and reportUsbPolicyStatus functions were
comparing the plain token against the tokenHash in the database,
which would never match. Now properly hashing the token before
database lookup.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 09:12:30 -03:00

313 lines
8.5 KiB
TypeScript

import { v } from "convex/values"
import { mutation, query } from "./_generated/server"
import type { Id, Doc } from "./_generated/dataModel"
import { sha256 } from "@noble/hashes/sha256"
const DEFAULT_TENANT_ID = "default"
function toHex(input: Uint8Array) {
return Array.from(input)
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
}
function hashToken(token: string) {
return toHex(sha256(token))
}
export const USB_POLICY_VALUES = ["ALLOW", "BLOCK_ALL", "READONLY"] as const
export type UsbPolicyValue = (typeof USB_POLICY_VALUES)[number]
export const USB_POLICY_STATUS = ["PENDING", "APPLIED", "FAILED"] as const
export type UsbPolicyStatus = (typeof USB_POLICY_STATUS)[number]
export const setUsbPolicy = mutation({
args: {
machineId: v.id("machines"),
policy: v.string(),
actorId: v.optional(v.id("users")),
actorEmail: v.optional(v.string()),
actorName: v.optional(v.string()),
},
handler: async (ctx, args) => {
const machine = await ctx.db.get(args.machineId)
if (!machine) {
throw new Error("Dispositivo nao encontrado")
}
if (!USB_POLICY_VALUES.includes(args.policy as UsbPolicyValue)) {
throw new Error(`Politica USB invalida: ${args.policy}. Valores validos: ${USB_POLICY_VALUES.join(", ")}`)
}
const now = Date.now()
const oldPolicy = machine.usbPolicy ?? "ALLOW"
await ctx.db.patch(args.machineId, {
usbPolicy: args.policy,
usbPolicyStatus: "PENDING",
usbPolicyError: undefined,
usbPolicyAppliedAt: now,
updatedAt: now,
})
await ctx.db.insert("usbPolicyEvents", {
tenantId: machine.tenantId,
machineId: args.machineId,
actorId: args.actorId,
actorEmail: args.actorEmail,
actorName: args.actorName,
oldPolicy,
newPolicy: args.policy,
status: "PENDING",
createdAt: now,
})
return { ok: true, policy: args.policy, status: "PENDING" }
},
})
export const reportUsbPolicyStatus = mutation({
args: {
machineToken: v.string(),
status: v.string(),
error: v.optional(v.string()),
currentPolicy: v.optional(v.string()),
},
handler: async (ctx, args) => {
const tokenHash = hashToken(args.machineToken)
const tokenRecord = await ctx.db
.query("machineTokens")
.withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
.first()
if (!tokenRecord || tokenRecord.revoked) {
throw new Error("Token de maquina invalido ou revogado")
}
if (tokenRecord.expiresAt < Date.now()) {
throw new Error("Token de maquina expirado")
}
const machine = await ctx.db.get(tokenRecord.machineId)
if (!machine) {
throw new Error("Dispositivo nao encontrado")
}
if (!USB_POLICY_STATUS.includes(args.status as UsbPolicyStatus)) {
throw new Error(`Status de politica USB invalido: ${args.status}`)
}
const now = Date.now()
await ctx.db.patch(machine._id, {
usbPolicyStatus: args.status,
usbPolicyError: args.error,
usbPolicyReportedAt: now,
updatedAt: now,
})
const latestEvent = await ctx.db
.query("usbPolicyEvents")
.withIndex("by_machine_created", (q) => q.eq("machineId", machine._id))
.order("desc")
.first()
if (latestEvent && latestEvent.status === "PENDING") {
await ctx.db.patch(latestEvent._id, {
status: args.status,
error: args.error,
appliedAt: args.status === "APPLIED" ? now : undefined,
})
}
return { ok: true }
},
})
export const getUsbPolicy = query({
args: {
machineId: v.id("machines"),
},
handler: async (ctx, args) => {
const machine = await ctx.db.get(args.machineId)
if (!machine) {
return null
}
return {
policy: machine.usbPolicy ?? "ALLOW",
status: machine.usbPolicyStatus ?? null,
error: machine.usbPolicyError ?? null,
appliedAt: machine.usbPolicyAppliedAt ?? null,
reportedAt: machine.usbPolicyReportedAt ?? null,
}
},
})
export const getPendingUsbPolicy = query({
args: {
machineToken: v.string(),
},
handler: async (ctx, args) => {
const tokenHash = hashToken(args.machineToken)
const tokenRecord = await ctx.db
.query("machineTokens")
.withIndex("by_token_hash", (q) => q.eq("tokenHash", tokenHash))
.first()
if (!tokenRecord || tokenRecord.revoked || tokenRecord.expiresAt < Date.now()) {
return null
}
const machine = await ctx.db.get(tokenRecord.machineId)
if (!machine) {
return null
}
if (machine.usbPolicyStatus === "PENDING") {
return {
policy: machine.usbPolicy ?? "ALLOW",
appliedAt: machine.usbPolicyAppliedAt,
}
}
return null
},
})
export const listUsbPolicyEvents = query({
args: {
machineId: v.id("machines"),
limit: v.optional(v.number()),
},
handler: async (ctx, args) => {
const limit = args.limit ?? 50
const events = await ctx.db
.query("usbPolicyEvents")
.withIndex("by_machine_created", (q) => q.eq("machineId", args.machineId))
.order("desc")
.take(limit)
return events.map((event) => ({
id: event._id,
oldPolicy: event.oldPolicy,
newPolicy: event.newPolicy,
status: event.status,
error: event.error,
actorEmail: event.actorEmail,
actorName: event.actorName,
createdAt: event.createdAt,
appliedAt: event.appliedAt,
}))
},
})
export const bulkSetUsbPolicy = mutation({
args: {
machineIds: v.array(v.id("machines")),
policy: v.string(),
actorId: v.optional(v.id("users")),
actorEmail: v.optional(v.string()),
actorName: v.optional(v.string()),
},
handler: async (ctx, args) => {
if (!USB_POLICY_VALUES.includes(args.policy as UsbPolicyValue)) {
throw new Error(`Politica USB invalida: ${args.policy}`)
}
const now = Date.now()
const results: Array<{ machineId: Id<"machines">; success: boolean; error?: string }> = []
for (const machineId of args.machineIds) {
try {
const machine = await ctx.db.get(machineId)
if (!machine) {
results.push({ machineId, success: false, error: "Dispositivo nao encontrado" })
continue
}
const oldPolicy = machine.usbPolicy ?? "ALLOW"
await ctx.db.patch(machineId, {
usbPolicy: args.policy,
usbPolicyStatus: "PENDING",
usbPolicyError: undefined,
usbPolicyAppliedAt: now,
updatedAt: now,
})
await ctx.db.insert("usbPolicyEvents", {
tenantId: machine.tenantId,
machineId,
actorId: args.actorId,
actorEmail: args.actorEmail,
actorName: args.actorName,
oldPolicy,
newPolicy: args.policy,
status: "PENDING",
createdAt: now,
})
results.push({ machineId, success: true })
} catch (err) {
results.push({ machineId, success: false, error: String(err) })
}
}
return { results, total: args.machineIds.length, successful: results.filter((r) => r.success).length }
},
})
/**
* Cleanup de policies USB pendentes por mais de 1 hora.
* Marca como FAILED com mensagem de timeout.
*/
export const cleanupStalePendingPolicies = mutation({
args: {
staleThresholdMs: v.optional(v.number()),
},
handler: async (ctx, args) => {
const thresholdMs = args.staleThresholdMs ?? 3600000 // 1 hora por padrao
const now = Date.now()
const cutoff = now - thresholdMs
// Buscar maquinas com status PENDING e appliedAt antigo
const allMachines = await ctx.db.query("machines").collect()
const staleMachines = allMachines.filter(
(m) =>
m.usbPolicyStatus === "PENDING" &&
m.usbPolicyAppliedAt !== undefined &&
m.usbPolicyAppliedAt < cutoff
)
let cleaned = 0
for (const machine of staleMachines) {
await ctx.db.patch(machine._id, {
usbPolicyStatus: "FAILED",
usbPolicyError: "Timeout: Agent nao reportou status apos 1 hora. Verifique se o agent esta ativo.",
updatedAt: now,
})
// Atualizar evento correspondente
const latestEvent = await ctx.db
.query("usbPolicyEvents")
.withIndex("by_machine_created", (q) => q.eq("machineId", machine._id))
.order("desc")
.first()
if (latestEvent && latestEvent.status === "PENDING") {
await ctx.db.patch(latestEvent._id, {
status: "FAILED",
error: "Timeout automatico",
})
}
cleaned++
}
return { cleaned, checked: allMachines.length }
},
})