feat: add permissions check on dns page
This commit is contained in:
parent
9d046a0cf6
commit
5d3fada266
@ -15,9 +15,16 @@ import cn from '~/utils/cn';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
configAvailable: boolean;
|
configAvailable: boolean;
|
||||||
uiAccess: boolean;
|
|
||||||
onboarding: boolean;
|
onboarding: boolean;
|
||||||
user?: AuthSession['user'];
|
user?: AuthSession['user'];
|
||||||
|
access: {
|
||||||
|
ui: boolean;
|
||||||
|
machines: boolean;
|
||||||
|
dns: boolean;
|
||||||
|
users: boolean;
|
||||||
|
policy: boolean;
|
||||||
|
settings: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LinkProps {
|
interface LinkProps {
|
||||||
@ -137,27 +144,45 @@ export default function Header(data: Props) {
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{data.uiAccess && !data.onboarding ? (
|
{data.access.ui && !data.onboarding ? (
|
||||||
<nav className="container flex items-center gap-x-4 overflow-x-auto font-semibold">
|
<nav className="container flex items-center gap-x-4 overflow-x-auto font-semibold">
|
||||||
<TabLink
|
{data.access.machines ? (
|
||||||
to="/machines"
|
<TabLink
|
||||||
name="Machines"
|
to="/machines"
|
||||||
icon={<Server className="w-5" />}
|
name="Machines"
|
||||||
/>
|
icon={<Server className="w-5" />}
|
||||||
<TabLink to="/users" name="Users" icon={<Users className="w-5" />} />
|
/>
|
||||||
<TabLink
|
) : undefined}
|
||||||
to="/acls"
|
{data.access.users ? (
|
||||||
name="Access Control"
|
<TabLink
|
||||||
icon={<Lock className="w-5" />}
|
to="/users"
|
||||||
/>
|
name="Users"
|
||||||
|
icon={<Users className="w-5" />}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{data.access.policy ? (
|
||||||
|
<TabLink
|
||||||
|
to="/acls"
|
||||||
|
name="Access Control"
|
||||||
|
icon={<Lock className="w-5" />}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
{data.configAvailable ? (
|
{data.configAvailable ? (
|
||||||
<>
|
<>
|
||||||
<TabLink to="/dns" name="DNS" icon={<Globe2 className="w-5" />} />
|
{data.access.dns ? (
|
||||||
<TabLink
|
<TabLink
|
||||||
to="/settings"
|
to="/dns"
|
||||||
name="Settings"
|
name="DNS"
|
||||||
icon={<Settings className="w-5" />}
|
icon={<Globe2 className="w-5" />}
|
||||||
/>
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{data.access.settings ? (
|
||||||
|
<TabLink
|
||||||
|
to="/settings"
|
||||||
|
name="Settings"
|
||||||
|
icon={<Settings className="w-5" />}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
</>
|
</>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { XCircleFillIcon } from '@primer/octicons-react';
|
import { XCircleFillIcon } from '@primer/octicons-react';
|
||||||
import { type LoaderFunctionArgs, redirect } from 'react-router';
|
import { type LoaderFunctionArgs, redirect } from 'react-router';
|
||||||
import { Outlet, useLoaderData } from 'react-router';
|
import { Outlet, useLoaderData } from 'react-router';
|
||||||
|
import { ErrorPopup } from '~/components/Error';
|
||||||
import type { LoadContext } from '~/server';
|
import type { LoadContext } from '~/server';
|
||||||
import { ResponseError } from '~/server/headscale/api-client';
|
import { ResponseError } from '~/server/headscale/api-client';
|
||||||
import cn from '~/utils/cn';
|
import cn from '~/utils/cn';
|
||||||
@ -65,3 +66,7 @@ export default function Layout() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary() {
|
||||||
|
return <ErrorPopup type="embedded" />;
|
||||||
|
}
|
||||||
|
|||||||
@ -93,6 +93,20 @@ export async function loader({
|
|||||||
debug: context.config.debug,
|
debug: context.config.debug,
|
||||||
user: session.get('user'),
|
user: session.get('user'),
|
||||||
uiAccess: check,
|
uiAccess: check,
|
||||||
|
access: {
|
||||||
|
ui: await context.sessions.check(request, Capabilities.ui_access),
|
||||||
|
dns: await context.sessions.check(request, Capabilities.read_network),
|
||||||
|
users: await context.sessions.check(request, Capabilities.read_users),
|
||||||
|
policy: await context.sessions.check(request, Capabilities.read_policy),
|
||||||
|
machines: await context.sessions.check(
|
||||||
|
request,
|
||||||
|
Capabilities.read_machines,
|
||||||
|
),
|
||||||
|
settings: await context.sessions.check(
|
||||||
|
request,
|
||||||
|
Capabilities.read_feature,
|
||||||
|
),
|
||||||
|
},
|
||||||
onboarding: request.url.endsWith('/onboarding'),
|
onboarding: request.url.endsWith('/onboarding'),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -1,10 +1,20 @@
|
|||||||
import { ActionFunctionArgs, data } from 'react-router';
|
import { ActionFunctionArgs, data } from 'react-router';
|
||||||
import { LoadContext } from '~/server';
|
import { LoadContext } from '~/server';
|
||||||
|
import { Capabilities } from '~/server/web/roles';
|
||||||
|
|
||||||
export async function dnsAction({
|
export async function dnsAction({
|
||||||
request,
|
request,
|
||||||
context,
|
context,
|
||||||
}: ActionFunctionArgs<LoadContext>) {
|
}: ActionFunctionArgs<LoadContext>) {
|
||||||
|
const check = await context.sessions.check(
|
||||||
|
request,
|
||||||
|
Capabilities.write_network,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!check) {
|
||||||
|
return data({ success: false }, 403);
|
||||||
|
}
|
||||||
|
|
||||||
if (!context.hs.writable()) {
|
if (!context.hs.writable()) {
|
||||||
return data({ success: false }, 403);
|
return data({ success: false }, 403);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useLoaderData } from 'react-router';
|
|||||||
import Code from '~/components/Code';
|
import Code from '~/components/Code';
|
||||||
import Notice from '~/components/Notice';
|
import Notice from '~/components/Notice';
|
||||||
import type { LoadContext } from '~/server';
|
import type { LoadContext } from '~/server';
|
||||||
|
import { Capabilities } from '~/server/web/roles';
|
||||||
import ManageDomains from './components/manage-domains';
|
import ManageDomains from './components/manage-domains';
|
||||||
import ManageNS from './components/manage-ns';
|
import ManageNS from './components/manage-ns';
|
||||||
import ManageRecords from './components/manage-records';
|
import ManageRecords from './components/manage-records';
|
||||||
@ -11,11 +12,30 @@ import ToggleMagic from './components/toggle-magic';
|
|||||||
import { dnsAction } from './dns-actions';
|
import { dnsAction } from './dns-actions';
|
||||||
|
|
||||||
// We do not want to expose every config value
|
// We do not want to expose every config value
|
||||||
export async function loader({ context }: LoaderFunctionArgs<LoadContext>) {
|
export async function loader({
|
||||||
|
request,
|
||||||
|
context,
|
||||||
|
}: LoaderFunctionArgs<LoadContext>) {
|
||||||
if (!context.hs.readable()) {
|
if (!context.hs.readable()) {
|
||||||
throw new Error('No configuration is available');
|
throw new Error('No configuration is available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const check = await context.sessions.check(
|
||||||
|
request,
|
||||||
|
Capabilities.read_network,
|
||||||
|
);
|
||||||
|
if (!check) {
|
||||||
|
// Not authorized to view this page
|
||||||
|
throw new Error(
|
||||||
|
'You do not have permission to view this page. Please contact your administrator.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const writablePermission = await context.sessions.check(
|
||||||
|
request,
|
||||||
|
Capabilities.write_network,
|
||||||
|
);
|
||||||
|
|
||||||
const config = context.hs.c!;
|
const config = context.hs.c!;
|
||||||
const dns = {
|
const dns = {
|
||||||
prefixes: config.prefixes,
|
prefixes: config.prefixes,
|
||||||
@ -29,6 +49,7 @@ export async function loader({ context }: LoaderFunctionArgs<LoadContext>) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...dns,
|
...dns,
|
||||||
|
access: writablePermission,
|
||||||
writable: context.hs.writable(),
|
writable: context.hs.writable(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -46,7 +67,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allNs.global = data.nameservers;
|
allNs.global = data.nameservers;
|
||||||
const isDisabled = data.writable === false;
|
const isDisabled = data.access === false || data.writable === false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-16 max-w-screen-lg">
|
<div className="flex flex-col gap-16 max-w-screen-lg">
|
||||||
@ -56,6 +77,12 @@ export default function Page() {
|
|||||||
the configuration
|
the configuration
|
||||||
</Notice>
|
</Notice>
|
||||||
)}
|
)}
|
||||||
|
{data.access ? undefined : (
|
||||||
|
<Notice>
|
||||||
|
Your permissions do not allow you to modify the DNS settings for this
|
||||||
|
tailnet.
|
||||||
|
</Notice>
|
||||||
|
)}
|
||||||
<RenameTailnet name={data.baseDomain} isDisabled={isDisabled} />
|
<RenameTailnet name={data.baseDomain} isDisabled={isDisabled} />
|
||||||
<ManageNS nameservers={allNs} isDisabled={isDisabled} />
|
<ManageNS nameservers={allNs} isDisabled={isDisabled} />
|
||||||
<ManageRecords records={data.extraRecords} isDisabled={isDisabled} />
|
<ManageRecords records={data.extraRecords} isDisabled={isDisabled} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user