import { data } from "react-router"; import { isDataWithApiError } from "~/server/headscale/api/error-client"; import { Capabilities } from "~/server/web/roles"; import type { Route } from "./+types/overview"; // We only check capabilities here and assume it is writable // If it isn't, it'll gracefully error anyways, since this means some // fishy client manipulation is happening. export async function aclAction({ request, context }: Route.ActionArgs) { const principal = await context.auth.require(request); const check = context.auth.can(principal, Capabilities.write_policy); if (!check) { throw data("You do not have permission to write to the ACL policy", { status: 403, }); } // Try to write to the ACL policy via the API or via config file (TODO). const formData = await request.formData(); const policyData = formData.get("policy")?.toString(); if (!policyData) { throw data("Missing `policy` in the form data.", { status: 400, }); } const apiKey = context.auth.getHeadscaleApiKey(principal, context.oidc?.apiKey); const api = context.hsApi.getRuntimeClient(apiKey); try { const { policy, updatedAt } = await api.setPolicy(policyData); return data({ success: true, error: undefined, policy, updatedAt, }); } catch (error) { if (isDataWithApiError(error)) { const rawData = error.data.rawData; // https://github.com/juanfont/headscale/blob/c4600346f9c29b514dc9725ac103efb9d0381f23/hscontrol/types/policy.go#L11 if (rawData.includes("update is disabled")) { throw data("Policy is not writable", { status: 403 }); } const message = error.data.data != null && "message" in error.data.data && typeof error.data.data.message === "string" ? error.data.data.message : undefined; if (message == null) { throw error; } // Starting in Headscale 0.27.0 the ACLs parsing was changed meaning // we need to reference other error messages based on API version. if (context.hsApi.clientHelpers.isAtleast("0.27.0")) { if (message.includes("parsing HuJSON:")) { const cutIndex = message.indexOf("parsing HuJSON:"); const trimmed = cutIndex > -1 ? `Syntax error: ${message.slice(cutIndex + 16).trim()}` : message; return data( { success: false, error: trimmed, policy: undefined, updatedAt: undefined, }, 400, ); } if (message.includes("parsing policy from bytes:")) { const cutIndex = message.indexOf("parsing policy from bytes:"); const trimmed = cutIndex > -1 ? `Syntax error: ${message.slice(cutIndex + 26).trim()}` : message; return data( { success: false, error: trimmed, policy: undefined, updatedAt: undefined, }, 400, ); } } else { // Pre-0.27.0 error messages if (message.includes("parsing hujson")) { const cutIndex = message.indexOf("err: hujson:"); const trimmed = cutIndex > -1 ? `Syntax error: ${message.slice(cutIndex + 12)}` : message; return data( { success: false, error: trimmed, policy: undefined, updatedAt: undefined, }, 400, ); } if (message.includes("unmarshalling policy")) { const cutIndex = message.indexOf("err:"); const trimmed = cutIndex > -1 ? `Syntax error: ${message.slice(cutIndex + 5)}` : message; return data( { success: false, error: trimmed, policy: undefined, updatedAt: undefined, }, 400, ); } } } // Otherwise, this is a Headscale error that we can just propagate. throw error; } }