/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { BeakerIcon, EyeIcon, IssueDraftIcon, PencilIcon } from '@primer/octicons-react' import { type ActionFunctionArgs, json, LoaderFunctionArgs } from '@remix-run/node' import { useFetcher, useLoaderData } from '@remix-run/react' import { useEffect, useState } from 'react' import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components' import Button from '~/components/Button' import Code from '~/components/Code' import Link from '~/components/Link' import Notice from '~/components/Notice' import Spinner from '~/components/Spinner' import { toast } from '~/components/Toaster' import { cn } from '~/utils/cn' import { loadAcl, loadContext, patchAcl } from '~/utils/config/headplane' import { HeadscaleError, pull, put } from '~/utils/headscale' import { getSession } from '~/utils/sessions' import Monaco from './editor' export async function loader({ request }: LoaderFunctionArgs) { const session = await getSession(request.headers.get('Cookie')) try { const { policy } = await pull<{ policy: string }>( 'v1/policy', session.get('hsApiKey')!, ) console.log(policy) try { // We have read access, now do we have write access? // Attempt to set the policy to what we just got await put('v1/policy', session.get('hsApiKey')!, { policy, }) return { hasAclWrite: true, isPolicyApi: true, currentAcl: policy, aclType: 'json', } as const } catch (error) { if (!(error instanceof HeadscaleError)) { throw error } if (error.status === 500) { return { hasAclWrite: false, isPolicyApi: true, currentAcl: policy, aclType: 'json', } as const } } } catch (error) { // Propagate our errors through normal error handling if (!(error instanceof HeadscaleError)) { throw error } // Not on 0.23-beta1 or later if (error.status === 404) { const { data, type, read, write } = await loadAcl() return { hasAclWrite: write, isPolicyApi: false, currentAcl: read ? data : '', aclType: type, } } throw error } return { hasAclWrite: true, isPolicyApi: true, currentAcl: '', aclType: 'json', } as const } export async function action({ request }: ActionFunctionArgs) { const session = await getSession(request.headers.get('Cookie')) if (!session.has('hsApiKey')) { return json({ success: false }, { status: 401, }) } const data = await request.json() as { acl: string, api: boolean } if (data.api) { try { await put('v1/policy', session.get('hsApiKey')!, { policy: data.acl, }) return json({ success: true }) } catch (error) { return json({ success: false }, { status: error instanceof HeadscaleError ? error.status : 500, }) } } const context = await loadContext() if (!context.acl.write) { return json({ success: false }, { status: 403, }) } await patchAcl(data.acl) if (context.integration?.onAclChange) { await context.integration.onAclChange(context.integration.context) } return json({ success: true }) } export function ErrorBoundary() { return (
An ACL policy is not available or an error occurred while trying to fetch it.

Access Control List (ACL)

The ACL file is used to define the access control rules for your network. You can find more information about the ACL file in the {' '} Tailscale ACL guide {' '} and the {' '} Headscale docs .

If you are running Headscale 0.23-beta1 or later, the ACL configuration is most likely set to {' '} file {' '} mode but the ACL file is not available. In order to resolve this you will either need to correctly set {' '} policy.path {' '} in your Headscale configuration or set the {' '} policy.mode {' '} to {' '} database .

If you are running an older version of Headscale, the {' '} ACL_FILE {' '} environment variable is not set. Refer to the {' '} Headplane Configuration {' '} documentation for more information on how to set the ACL file and integrate it with Headscale.

) } export default function Page() { const data = useLoaderData() const fetcher = useFetcher() const [acl, setAcl] = useState(data.currentAcl) const [toasted, setToasted] = useState(false) useEffect(() => { if (!fetcher.data || toasted) { return } if (fetcher.data.success) { toast('Updated tailnet ACL policy') } else { toast('Failed to update tailnet ACL policy') } setToasted(true) setAcl(data.currentAcl) }, [fetcher.data, toasted, data.currentAcl]) return (
{data.hasAclWrite ? undefined : (
{data.isPolicyApi ? ( The ACL policy is read-only. You can view the current policy but you cannot make changes to it.
To resolve this, you need to set the ACL policy mode to database in your Headscale configuration.
) : ( The ACL policy is read-only. You can view the current policy but you cannot make changes to it.
To resolve this, you need to configure a Headplane integration or make the ACL_FILE environment variable available.
)}
)}

Access Control List (ACL)

The ACL file is used to define the access control rules for your network. You can find more information about the ACL file in the {' '} Tailscale ACL guide {' '} and the {' '} Headscale docs .

cn( 'px-4 py-2 rounded-tl-lg', 'focus:outline-none flex items-center gap-2', 'border-x border-gray-200 dark:border-gray-700', isSelected ? 'text-gray-900 dark:text-gray-100' : '', )} >

Edit file

cn( 'px-4 py-2', 'focus:outline-none flex items-center gap-2', 'border-x border-gray-200 dark:border-gray-700', isSelected ? 'text-gray-900 dark:text-gray-100' : '', )} >

Preview changes

cn( 'px-4 py-2 rounded-tr-lg', 'focus:outline-none flex items-center gap-2', 'border-x border-gray-200 dark:border-gray-700', isSelected ? 'text-gray-900 dark:text-gray-100' : '', )} >

Preview rules

The Preview rules is very much still a work in progress. It is a bit complicated to implement right now but hopefully it will be available soon.

) }