Files
2026-06-20 12:38:09 -04:00

91 lines
3.0 KiB
TypeScript

import { data } from "react-router";
import { authContext, requestApiContext } from "~/server/context";
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 auth = context.get(authContext);
const getRequestApi = context.get(requestApiContext);
const principal = await auth.require(request);
const check = 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 { api } = await getRequestApi(request);
try {
const { policy, updatedAt } = await api.policy.set(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;
}
// Policy parse errors carry one of two prefixes (HuJSON syntax vs.
// structural unmarshal). Headscale 0.27.0+ uses these forms; older
// releases are no longer supported.
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,
);
}
}
// Otherwise, this is a Headscale error that we can just propagate.
throw error;
}
}