feat(auth): support reverse-proxy driven proxy auth

Closes HP-353.
This commit is contained in:
Aarnav Tale
2026-06-20 11:40:46 -04:00
parent c7822e3ec2
commit 96f2721272
24 changed files with 837 additions and 75 deletions
+5 -1
View File
@@ -1,5 +1,6 @@
import { data } from "react-router";
import { isUserPrincipal } from "~/server/web/auth";
import { getOidcSubject } from "~/server/web/headscale-identity";
import { Capabilities } from "~/server/web/roles";
import type { PreAuthKey } from "~/types";
@@ -25,7 +26,10 @@ export async function authKeysAction({ request, context }: Route.ActionArgs) {
throw data("User not found.", { status: 404 });
}
const targetSubject = getOidcSubject(targetUser);
if (principal.kind !== "oidc" || targetSubject !== principal.user.subject) {
const ownsTarget =
isUserPrincipal(principal) &&
(principal.user.headscaleUserId === userId || targetSubject === principal.user.subject);
if (!ownsTarget) {
throw data("You do not have permission to manage this user's pre-auth keys", {
status: 403,
});
@@ -18,10 +18,22 @@ interface AddAuthKeyProps {
users: User[];
url: string;
selfServiceOnly: boolean;
currentHeadscaleUserId?: string;
currentSubject?: string;
}
function findCurrentUser(users: User[], subject: string | undefined): User | undefined {
function findCurrentUser(
users: User[],
headscaleUserId: string | undefined,
subject: string | undefined,
): User | undefined {
if (headscaleUserId) {
const linked = users.find((u) => u.id === headscaleUserId);
if (linked) {
return linked;
}
}
if (!subject) {
return undefined;
}
@@ -38,6 +50,7 @@ export default function AddAuthKey({
users,
url,
selfServiceOnly,
currentHeadscaleUserId,
currentSubject,
}: AddAuthKeyProps) {
const fetcher = useFetcher();
@@ -46,7 +59,9 @@ export default function AddAuthKey({
const [reusable, setReusable] = useState(false);
const [ephemeral, setEphemeral] = useState(false);
const [tagOnly, setTagOnly] = useState(false);
const currentUser = selfServiceOnly ? findCurrentUser(users, currentSubject) : null;
const currentUser = selfServiceOnly
? findCurrentUser(users, currentHeadscaleUserId, currentSubject)
: null;
const availableUsers = selfServiceOnly && currentUser ? [currentUser] : users;
const [userId, setUserId] = useState<string | null>(availableUsers[0]?.id);
const [tags, setTags] = useState("");
+14 -2
View File
@@ -7,6 +7,7 @@ import Notice from "~/components/notice";
import Select from "~/components/select";
import TableList from "~/components/table-list";
import { usersResource } from "~/server/headscale/live-store";
import { isUserPrincipal } from "~/server/web/auth";
import { Capabilities } from "~/server/web/roles";
import type { PreAuthKey } from "~/types";
import type { User } from "~/types/User";
@@ -90,7 +91,8 @@ export async function loader({ request, context }: Route.LoaderArgs) {
return {
access: canGenerateAny || canGenerateOwn,
currentSubject: principal.kind === "oidc" ? principal.user.subject : undefined,
currentHeadscaleUserId: isUserPrincipal(principal) ? principal.user.headscaleUserId : undefined,
currentSubject: isUserPrincipal(principal) ? principal.user.subject : undefined,
keys,
missing,
selfServiceOnly: !canGenerateAny && canGenerateOwn,
@@ -103,7 +105,16 @@ export const action = authKeysAction;
type Status = "all" | "active" | "expired" | "reusable" | "ephemeral";
export default function Page({
loaderData: { keys, missing, users, url, access, selfServiceOnly, currentSubject },
loaderData: {
keys,
missing,
users,
url,
access,
selfServiceOnly,
currentHeadscaleUserId,
currentSubject,
},
}: Route.ComponentProps) {
const [selectedUser, setSelectedUser] = useState("__headplane_all");
const [status, setStatus] = useState<Status>("active");
@@ -199,6 +210,7 @@ export default function Page({
</Link>
</p>
<AddAuthKey
currentHeadscaleUserId={currentHeadscaleUserId}
currentSubject={currentSubject}
selfServiceOnly={selfServiceOnly}
url={url}