mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
0a51182eed
The pre-0.28 ExpirePreAuthKey RPC takes a uint64 `user` field plus the
key string. The API layer reads that uint64 from `key.user?.id`, but
the action was wrapping the form's user_id as `{ name: user }` — so
.id was always undefined and the wire request sent an empty string,
which Headscale rejects with "proto: invalid value for uint64 field
user". 0.28+ is unaffected because the new expire endpoint only reads
`key.id` (the stable preauthkey id).
Pass `{ id: user }` so the numeric Headscale user id reaches the wire.
Amp-Thread-ID: https://ampcode.com/threads/T-019e7ae4-6862-760c-a3e7-239350eab71d
Co-authored-by: Amp <amp@ampcode.com>
133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
import { data } from "react-router";
|
|
|
|
import { getOidcSubject } from "~/server/web/headscale-identity";
|
|
import { Capabilities } from "~/server/web/roles";
|
|
import type { PreAuthKey } from "~/types";
|
|
|
|
import type { Route } from "./+types/overview";
|
|
|
|
export async function authKeysAction({ request, context }: Route.ActionArgs) {
|
|
const { principal, api } = await context.apiForRequest(request);
|
|
|
|
const canGenerateAny = context.auth.can(principal, Capabilities.generate_authkeys);
|
|
const canGenerateOwn = context.auth.can(principal, Capabilities.generate_own_authkeys);
|
|
|
|
if (!canGenerateAny && !canGenerateOwn) {
|
|
throw data("You do not have permission to manage pre-auth keys", {
|
|
status: 403,
|
|
});
|
|
}
|
|
|
|
async function checkSelfServiceOwnership(userId: string) {
|
|
if (canGenerateAny || !canGenerateOwn) return;
|
|
const [targetUser] = await api.users.list({ id: userId });
|
|
if (!targetUser) {
|
|
throw data("User not found.", { status: 404 });
|
|
}
|
|
const targetSubject = getOidcSubject(targetUser);
|
|
if (principal.kind !== "oidc" || targetSubject !== principal.user.subject) {
|
|
throw data("You do not have permission to manage this user's pre-auth keys", {
|
|
status: 403,
|
|
});
|
|
}
|
|
}
|
|
|
|
const formData = await request.formData();
|
|
const action = formData.get("action_id")?.toString();
|
|
if (!action) {
|
|
throw data("Missing `action_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
switch (action) {
|
|
case "add_preauthkey": {
|
|
const user = formData.get("user_id")?.toString() || null;
|
|
const aclTagsRaw = formData.get("acl_tags")?.toString() || "";
|
|
const aclTags = aclTagsRaw
|
|
.split(",")
|
|
.map((t) => t.trim())
|
|
.filter((t) => t.length > 0);
|
|
|
|
if (!user && aclTags.length === 0) {
|
|
return data("Must specify either a user or ACL tags.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
if (user) {
|
|
await checkSelfServiceOwnership(user);
|
|
}
|
|
|
|
const expiry = formData.get("expiry")?.toString();
|
|
if (!expiry) {
|
|
return data("Missing `expiry` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const reusable = formData.get("reusable")?.toString();
|
|
if (!reusable) {
|
|
return data("Missing `reusable` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const ephemeral = formData.get("ephemeral")?.toString();
|
|
if (!ephemeral) {
|
|
return data("Missing `ephemeral` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const day = Number(expiry.toString().split(" ")[0]);
|
|
const date = new Date();
|
|
date.setDate(date.getDate() + day);
|
|
|
|
const key = await api.preAuthKeys.create({
|
|
user,
|
|
ephemeral: ephemeral === "on",
|
|
reusable: reusable === "on",
|
|
expiration: date,
|
|
aclTags: aclTags.length > 0 ? aclTags : null,
|
|
});
|
|
|
|
return data({ success: true as const, key: key.key });
|
|
}
|
|
|
|
case "expire_preauthkey": {
|
|
const keyId = formData.get("key_id")?.toString();
|
|
const key = formData.get("key")?.toString();
|
|
if (!keyId || !key) {
|
|
return data("Missing `key_id` or `key` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
const user = formData.get("user_id")?.toString();
|
|
if (!user) {
|
|
return data("Missing `user_id` in the form data.", {
|
|
status: 400,
|
|
});
|
|
}
|
|
|
|
await checkSelfServiceOwnership(user);
|
|
// `user` here is the Headscale numeric user id (form field is wired
|
|
// from User.id). Pre-0.28 expire posts a uint64 `user` field, which
|
|
// the API layer reads from `key.user?.id`. Headscale 0.28+ only
|
|
// looks at `key.id` (the stable preauthkey id).
|
|
await api.preAuthKeys.expire({
|
|
id: keyId,
|
|
key,
|
|
user: { id: user },
|
|
} as unknown as PreAuthKey);
|
|
return data("Pre-auth key expired");
|
|
}
|
|
|
|
default:
|
|
return data("Invalid action", {
|
|
status: 400,
|
|
});
|
|
}
|
|
}
|