mirror of
https://github.com/tale/headplane.git
synced 2026-08-13 07:06:51 +00:00
91 lines
3.0 KiB
TypeScript
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;
|
|
}
|
|
}
|