mirror of
https://github.com/tale/headplane.git
synced 2026-08-25 11:56:49 +00:00
0512565f8e
Apparently I didn't use my brain cells and rely on the /version endpoint that Headscale has exposed since 0.26 (our lowest supported version). Switching to that significantly simplifies the API surface.
129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
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 { api } = await context.apiForRequest(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;
|
|
}
|
|
|
|
// 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.headscale.capabilities.policyErrorsUseModernFormat) {
|
|
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;
|
|
}
|
|
}
|