mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
d4eee702e9
Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019e5550-4435-7118-8393-cdcc97042178
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.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;
|
|
}
|
|
}
|