Skip to content

Commit

Permalink
edit acl rule
Browse files Browse the repository at this point in the history
  • Loading branch information
LucaRickli committed Nov 23, 2024
1 parent 735204c commit 13fbfdb
Show file tree
Hide file tree
Showing 12 changed files with 406 additions and 176 deletions.
24 changes: 16 additions & 8 deletions src/lib/api/headscale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ export interface AclData {
}[];
acls: {
id: string;
action: 'accept';
action: string;
src: string[];
dst: { host: string; port: string }[];
proto?: string;
Expand All @@ -679,13 +679,21 @@ export class Acl {
public static async load(headscale = new Headscale()) {
const response = await headscale.client.GET('/api/v1/policy');

return {
...response,
data: new Acl(response.data),
error: response.error
? new ApiError(response.error, { method: 'GET', path: '/api/v1/policy' })
: undefined
};
try {
return {
...response,
data: new Acl(response.data),
error: response.error
? new ApiError(response.error, { method: 'GET', path: '/api/v1/policy' })
: undefined
};
} catch (err) {
return {
...response,
data: undefined,
error: err
};
}
}

protected static parsePolicy(policy: string) {
Expand Down
55 changes: 48 additions & 7 deletions src/lib/components/data/acl/EditRule.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@
import { defaults, superForm } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { createEventDispatcher } from 'svelte';
import { get, writable } from 'svelte/store';
import { z } from 'zod';
import * as Select from '$lib/components/ui/select/index.js';
import * as Dialog from '$lib/components/ui/dialog';
import { Textarea } from '$lib/components/ui/textarea';
import * as Form from '$lib/components/form';
import type { Acl, AclData, User } from '$lib/api';
import SelectRuleSource from './SelectRuleSource.svelte';
import Code from '$lib/components/utils/Code.svelte';
import SelectRuleTarget from './SelectRuleTarget.svelte';
export let acl: Acl;
export let users: User[];
export let rule: AclData['acls'][0];
const dispatch = createEventDispatcher<{ submit: undefined }>();
const schema = z.object({
const schema: z.ZodType<Omit<AclData['acls'][0], 'id'>> = z.object({
action: z.literal('accept'),
src: z.array(z.string()),
dst: z.array(z.string())
dst: z.array(z.object({ host: z.string(), port: z.string() })),
comments: z.array(z.string()).default([])
});
const form = superForm(defaults(zod(schema)), {
Expand All @@ -38,10 +44,18 @@
const { form: formData, constraints } = form;
const description = writable<string>(rule.comments?.join('\n') || '');
description.subscribe((desc) => {
formData.update((data) => ({ ...data, comments: desc.split(/\n{1,}/gm) }));
});
export function reset() {
formData.set({
action: 'accept',
src: rule.src,
dst: rule.dst.map((dst) => `${dst.host}:${dst.port}`)
dst: rule.dst,
comments: get(description).split(/\n{2,}/gm)
});
}
Expand All @@ -59,20 +73,47 @@
</Dialog.Header>

<Form.Root {form} {reset} submitText="Save">
<Form.Field {form} name="action">
<Form.Control let:attrs>
<Form.Label>Action</Form.Label>

<Select.Root selected={{ label: 'accept', value: 'accept' }}>
<Select.Trigger>
<Select.Value />
</Select.Trigger>

<Select.Content>
<Select.Group>
<Select.Item value="accept">accept</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</Form.Control>
</Form.Field>

<Form.Field {form} name="src">
<Form.Control let:attrs>
<Form.Label for={attrs.id}>Source</Form.Label>
<SelectRuleSource {acl} {users} bind:selected={$formData.src} />

<SelectRuleSource {...attrs} {acl} {users} bind:selected={$formData.src} />
</Form.Control>
</Form.Field>

<Form.Field {form} name="dst">
<Form.Control let:attrs>
<Form.Label for={attrs.id}>Destination</Form.Label>

<SelectRuleTarget {...attrs} {acl} {users} bind:selected={$formData.dst} />
</Form.Control>
</Form.Field>
</Form.Root>

<Code yaml={$formData} />
<Form.Field {form} name="comments">
<Form.Control let:attrs>
<Form.Label for={attrs.id}>Description</Form.Label>

<Textarea {...attrs} {...$constraints.comments} bind:value={$description} />
</Form.Control>
</Form.Field>
</Form.Root>
</Dialog.Content>
</Dialog.Root>
11 changes: 7 additions & 4 deletions src/lib/components/data/acl/RuleInfo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,13 @@
</div>

{#if rule.comments?.length}
<div class="space-y-1.5">
{#each rule.comments as comment}
<p>{comment}</p>
{/each}
<div>
<Label>Description</Label>
<div class="space-y-1.5">
{#each rule.comments as comment}
<p>{comment}</p>
{/each}
</div>
</div>
{/if}
</div>
43 changes: 43 additions & 0 deletions src/lib/components/data/acl/SelectItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script lang="ts">
import type { Selected } from 'bits-ui';
import { writable } from 'svelte/store';
import * as Select from '$lib/components/ui/select/index.js';
export let items: { [group: string]: string[] };
export let selected: string;
export let placeholder: string = 'Select';
const sel = writable<Selected<string>>({ label: selected, value: selected });
sel.subscribe(({ value }) => {
selected = value;
});
</script>

<Select.Root bind:selected={$sel}>
<Select.Trigger>
<Select.Value {placeholder} />
</Select.Trigger>

<Select.Content>
{#each Object.keys(items) as group}
<Select.Group>
<Select.Label>{group}</Select.Label>

{#each items[group] || [] as item}
<Select.Item value={item}>{item}</Select.Item>
{/each}
</Select.Group>
{/each}

{#if selected.length && !Object.values(items).find((i) => i.includes(selected))?.length}
<Select.Group>
<Select.Label>Unknown</Select.Label>
<Select.Item value={selected}>{selected}</Select.Item>
</Select.Group>
{/if}

<slot />
</Select.Content>
</Select.Root>
163 changes: 57 additions & 106 deletions src/lib/components/data/acl/SelectRuleSource.svelte
Original file line number Diff line number Diff line change
@@ -1,126 +1,77 @@
<script lang="ts">
import type { Selected } from 'bits-ui';
import { get, writable } from 'svelte/store';
import * as Select from '$lib/components/ui/select/index.js';
import { Input } from '$lib/components/ui/input';
import Plus from 'lucide-svelte/icons/plus';
import Trash_2 from 'lucide-svelte/icons/trash-2';
import { Button } from '$lib/components/ui/button';
import { Acl, groupRegex, tagRegex, User } from '$lib/api';
import { Acl, User } from '$lib/api';
import SelectItem from './SelectItem.svelte';
export let acl: Acl;
export let selected: string[];
export let users: User[] | undefined;
export let required = true;
export let id: string = window.crypto.randomUUID();
const sel = writable<Selected<string>[]>([]);
sel.subscribe((state) => (selected = state?.map((i) => i.value)));
const items: { [group: string]: string[] } = {
Users: getNames(users),
Groups: getNames(acl.groups),
Hosts: getNames(acl.hosts),
Tags: getNames(acl.tagOwners),
General: ['*']
};
const customItemInput = writable<string>();
const customItems = writable<string[]>(filterCustomItems());
const sel = writable<string[]>(selected);
const newItem = writable<string>('');
function commentFilter(data: Array<string>, replaceRegex?: RegExp): Array<string> {
return data
.filter((i) => i !== '$$comments')
.map((i) => (replaceRegex ? i.replace(replaceRegex, '') : i));
}
function handleAdd() {
if (!get(newItem).length) return;
function filterCustomItems(): string[] {
const tags = getNames(acl.tagOwners);
const groups = getNames(acl.groups);
const hosts = getNames(acl.hosts);
return selected.filter(
(item) =>
item !== '*' &&
!groups.find((group) => group === item) &&
!tags.find((tag) => tag === item) &&
!hosts.find((host) => host === item) &&
!users?.find((usr) => usr.name === item)
);
sel.update((s) => [...s, get(newItem)]);
selected = [...selected, get(newItem)];
newItem.set('');
}
function handleAdd() {
const input = get(customItemInput);
customItems.update((items) => [...items, input]);
customItemInput.set('');
function handleDelete(item: string) {
if (!item.length) return;
sel.update((sel) => sel.filter((i) => i !== item));
selected = selected.filter((i) => i !== item);
}
function getNames(items: { name: string }[]): string[] {
return items.map((i) => i.name);
function getNames(items: Partial<{ name: string }>[] | undefined): string[] {
return items?.map((i) => i.name).filter((i) => typeof i !== 'undefined') || [];
}
</script>

<Select.Root portal={null} multiple bind:selected={$sel} {required}>
<Select.Trigger>
<Select.Value asChild>
<span>
{($sel.length || 0) + ' selected'}
</span>
</Select.Value>
</Select.Trigger>

<Select.Content class="h-96 overflow-y-scroll">
<Select.Group>
<Select.Label>Users</Select.Label>
{#each users || [] as user}
<Select.Item value={user.name}>
{user.name}
</Select.Item>
{/each}
</Select.Group>

<Select.Group>
<Select.Label>Groups</Select.Label>
{#each commentFilter(getNames(acl.groups)) as group}
<Select.Item value={group}>
{group.replace(groupRegex, '')}
</Select.Item>
{/each}
</Select.Group>

<Select.Group>
<Select.Label>Hosts</Select.Label>
{#each commentFilter(getNames(acl.hosts)) as host}
<Select.Item value={host}>
{host.replace(tagRegex, '')}
</Select.Item>
{/each}
</Select.Group>

<Select.Group>
<Select.Label>Known tags</Select.Label>
{#each commentFilter(getNames(acl.tagOwners)) as tag}
<Select.Item value={tag}>
{tag.replace(tagRegex, '')}
</Select.Item>
{/each}
</Select.Group>

<Select.Group>
<Select.Label>General</Select.Label>
<Select.Item value="*">Any</Select.Item>
</Select.Group>

<Select.Group>
<Select.Label>Custom</Select.Label>
{#each $customItems as tag}
<Select.Item value={tag}>
{tag}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>

<Select.Input name={id} {id} />
</Select.Root>

<form
class="mb-3 mt-2 grid gap-2.5"
style="grid-template-columns: 1fr 50px;"
on:submit|preventDefault={handleAdd}
>
<Input placeholder="Custom" bind:value={$customItemInput} />
<Button type="submit">Add</Button>
</form>
<div class="space-y-2">
{#each $sel || [] as item}
<div class="grid gap-1.5" style="grid-template-columns: 1fr auto;">
<SelectItem {items} bind:selected={item}></SelectItem>

<Button
class="hover:bg-destructive hover:text-destructive-foreground"
variant="outline"
on:click={() => handleDelete(item)}
>
<Trash_2 class="h-4 w-4" />
</Button>
</div>
{/each}

<form
class="!mt-5 grid gap-1.5"
style="grid-template-columns: 1fr auto;"
on:submit|preventDefault|stopPropagation={handleAdd}
>
{#key $newItem}
<SelectItem {items} bind:selected={$newItem} />
{/key}

<Button type="submit" variant="outline" disabled={!$newItem.length}>
<Plus class="h-4 w-4" />
</Button>
</form>
</div>
Loading

0 comments on commit 13fbfdb

Please sign in to comment.