From 444b2325fbc07fda25f427d6d0c5aca28a2d426a Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Tue, 4 Nov 2025 23:16:49 -0500 Subject: [PATCH] feat: fix acl api logic to work correctly --- app/routes/acls/acl-action.ts | 29 +- app/routes/acls/acl-loader.ts | 21 +- app/routes/acls/components/cm.client.tsx | 16 +- app/routes/acls/components/fallback.tsx | 2 +- app/routes/acls/overview.tsx | 37 +- app/routes/auth/login/action.ts | 16 +- app/routes/auth/login/page.tsx | 4 +- app/routes/dns/dns-actions.ts | 430 +++++++++--------- app/routes/settings/auth-keys/actions.ts | 155 +++---- .../auth-keys/dialogs/add-auth-key.tsx | 37 +- .../auth-keys/dialogs/expire-auth-key.tsx | 6 +- app/routes/settings/auth-keys/overview.tsx | 39 +- app/routes/settings/restrictions/actions.ts | 307 ++++++------- .../restrictions/dialogs/add-domain.tsx | 10 +- .../restrictions/dialogs/add-group.tsx | 8 +- .../restrictions/dialogs/add-user.tsx | 8 +- app/routes/settings/restrictions/overview.tsx | 36 +- app/routes/settings/restrictions/table.tsx | 8 +- app/routes/users/components/manage-banner.tsx | 13 +- app/routes/users/components/menu.tsx | 8 +- app/routes/users/components/user-row.tsx | 12 +- app/routes/users/dialogs/create-user.tsx | 24 +- app/routes/users/dialogs/delete-user.tsx | 4 +- app/routes/users/dialogs/reassign-user.tsx | 18 +- app/routes/users/dialogs/rename-user.tsx | 18 +- app/routes/users/onboarding-skip.tsx | 9 +- app/routes/users/onboarding.tsx | 21 +- app/routes/users/overview.tsx | 44 +- app/routes/users/user-actions.ts | 223 ++++----- app/server/config/integration/abstract.ts | 4 +- app/server/config/integration/docker.ts | 10 +- app/server/config/integration/kubernetes.ts | 12 +- app/server/config/integration/proc.ts | 63 +-- app/server/headscale/api-client.ts | 296 ------------ app/server/headscale/api/endpoints/policy.ts | 34 +- app/server/headscale/config-dns.ts | 2 +- app/server/index.ts | 6 - 37 files changed, 772 insertions(+), 1218 deletions(-) delete mode 100644 app/server/headscale/api-client.ts diff --git a/app/routes/acls/acl-action.ts b/app/routes/acls/acl-action.ts index 3cf680b..bd946ae 100644 --- a/app/routes/acls/acl-action.ts +++ b/app/routes/acls/acl-action.ts @@ -1,40 +1,35 @@ -import { ActionFunctionArgs, data } from 'react-router'; -import { LoadContext } from '~/server'; +import { data } from 'react-router'; import ResponseError from '~/server/headscale/api/response-error'; import { Capabilities } from '~/server/web/roles'; -import { data400, data403 } from '~/utils/res'; +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, -}: ActionFunctionArgs) { +export async function aclAction({ request, context }: Route.ActionArgs) { const session = await context.sessions.auth(request); const check = await context.sessions.check( request, Capabilities.write_policy, ); if (!check) { - throw data403('You do not have permission to write to the ACL policy'); + 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 data400('Missing `policy` in the form data.'); + throw data('Missing `policy` in the form data.', { + status: 400, + }); } + const api = context.hsApi.getRuntimeClient(session.api_key); try { - const { policy, updatedAt } = await context.client.put<{ - policy: string; - updatedAt: string; - }>('v1/policy', session.api_key, { - policy: policyData, - }); - + const { policy, updatedAt } = await api.setPolicy(policyData); return data({ success: true, error: undefined, @@ -50,7 +45,7 @@ export async function aclAction({ // https://github.com/juanfont/headscale/blob/main/hscontrol/types/policy.go if (message.includes('update is disabled')) { // This means the policy is not writable - throw data403('Policy is not writable'); + throw data('Policy is not writable', { status: 403 }); } // https://github.com/juanfont/headscale/blob/main/hscontrol/policy/v1/acls.go#L81 diff --git a/app/routes/acls/acl-loader.ts b/app/routes/acls/acl-loader.ts index 3bbe91f..e8c7bea 100644 --- a/app/routes/acls/acl-loader.ts +++ b/app/routes/acls/acl-loader.ts @@ -1,8 +1,7 @@ -import { LoaderFunctionArgs } from 'react-router'; -import { LoadContext } from '~/server'; +import { data } from 'react-router'; import ResponseError from '~/server/headscale/api/response-error'; import { Capabilities } from '~/server/web/roles'; -import { data403 } from '~/utils/res'; +import type { Route } from './+types/overview'; // The logic for deciding policy factors is very complicated because // there are so many factors that need to be accounted for: @@ -11,15 +10,13 @@ import { data403 } from '~/utils/res'; // 3. Is the Headscale policy in file or database mode? // If database, we can read/write easily via the API. // If in file mode, we can only write if context.config is available. -// TODO: Consider adding back file editing mode instead of database -export async function aclLoader({ - request, - context, -}: LoaderFunctionArgs) { +export async function aclLoader({ request, context }: Route.LoaderArgs) { const session = await context.sessions.auth(request); const check = await context.sessions.check(request, Capabilities.read_policy); if (!check) { - throw data403('You do not have permission to read the ACL policy.'); + throw data('You do not have permission to read the ACL policy.', { + status: 403, + }); } const flags = { @@ -30,11 +27,9 @@ export async function aclLoader({ }; // Try to load the ACL policy from the API. + const api = context.hsApi.getRuntimeClient(session.api_key); try { - const { policy, updatedAt } = await context.client.get<{ - policy: string; - updatedAt: string | null; - }>('v1/policy', session.api_key); + const { policy, updatedAt } = await api.getPolicy(); // Successfully loaded the policy, mark it as readable // If `updatedAt` is null, it means the policy is in file mode. diff --git a/app/routes/acls/components/cm.client.tsx b/app/routes/acls/components/cm.client.tsx index d35b945..cf54c8c 100644 --- a/app/routes/acls/components/cm.client.tsx +++ b/app/routes/acls/components/cm.client.tsx @@ -38,14 +38,14 @@ export function Editor(props: EditorProps) { }> {() => ( props.onChange(value)} + readOnly={props.isDisabled} style={{ height: '100%' }} theme={light ? xcodeLight : xcodeDark} - onChange={(value) => props.onChange(value)} + value={props.value} /> )} @@ -92,14 +92,14 @@ export function Differ(props: DifferProps) { {() => ( )} diff --git a/app/routes/acls/components/fallback.tsx b/app/routes/acls/components/fallback.tsx index de6b24c..aa79ed1 100644 --- a/app/routes/acls/components/fallback.tsx +++ b/app/routes/acls/components/fallback.tsx @@ -23,12 +23,12 @@ export default function Fallback({ acl }: Props) { />