/* 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 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')!, ) 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, } } } 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 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.

) }