"some" wokr on roles (this is last major roadblock to milestone:feat-org-keys)

This commit is contained in:
c0repwn3r 2023-03-02 21:33:24 -05:00
parent cde5b73907
commit 39978c3579
Signed by: core
GPG Key ID: FDBF740DADDCEECF
18 changed files with 1026 additions and 8 deletions

View File

@ -0,0 +1,44 @@
<script lang="ts">
export let selected;
export let orgid;
let hostsstyle = (selected == "hosts" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
let lighthousestyle = (selected == "lighthouses" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
let relaysstyle = (selected == "relays" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
let rolesstyle = (selected == "roles" ? "dark:bg-slate-600 bg-slate-200" : "dark:hover:bg-slate-600 hover:bg-slate-200");
</script>
<div class="min-h-screen flex flex-row overflow-y-hidden">
<div class="max-h flex flex-col w-56 overflow-hidden border-r-2 border-slate-500">
<ul class="flex flex-col py-4">
<li>
<a href="/org/{orgid}/hosts" class="{hostsstyle} mt-2 flex flex-row items-center h-12 transition">
<span class="ml-10 text-sm font-medium">Hosts</span>
</a>
</li>
<li>
<a href="/org/{orgid}/lighthouses" class="{lighthousestyle} mt-2 flex flex-row items-center h-12 dark:hover:bg-slate-600 hover:bg-slate-200 transition">
<span class="ml-10 text-sm font-medium">Lighthouses</span>
</a>
</li>
<li>
<a href="/org/{orgid}/relays" class="{relaysstyle} mt-2 flex flex-row items-center h-12 dark:hover:bg-slate-600 hover:bg-slate-200 transition">
<span class="ml-10 text-sm font-medium">Relays</span>
</a>
</li>
<li>
<a href="/org/{orgid}/roles" class="{rolesstyle} mt-2 flex flex-row items-center h-12 dark:hover:bg-slate-600 hover:bg-slate-200 transition">
<span class="ml-10 text-sm font-medium">Roles</span>
</a>
</li>
</ul>
</div>
<div class="overflow-hidden w-full min-w-screen min-h-screen ml-5 mr-5 m-2">
<slot></slot>
</div>
</div>

View File

@ -106,5 +106,48 @@
"loadownedorg": "Unable to load organizations",
"this user does not have permission to access this org": "You do not have permission to access one of the orgs associated with your account. Your access may have been revoked. Please reload the page."
}
},
"org": {
"apierror": {
"notyourorg": "Access denied",
"notyourorgexplainer": "You do not have permission to manage this org. Ask the owner of this org to add you, or go back to the homepage.",
"loadroles": "Unable to load roles",
"TypeError: NetworkError when attempting to fetch resource": "Unable to contact the server. Please try again later",
"Unable to create default role firewall rules": "Unable to create the default role. Please try again later."
},
"nohosts": "You don't have any hosts set up",
"nohostsdesc": "Create and enroll a host to begin using it in your network.",
"nohostsdesc2": "Be sure to also create and enroll a lighthouse, or your hosts wont be able to communicate.",
"nohosts_cta": "Add host",
"nolighthouses": "You don't have any lighthouses set up",
"nolighthousesdesc": "Lighthouses keep track of potential routes to overlay network hosts, and they assist in establishing connections between hosts.",
"nolighthouses_list_item1": "Each network needs at least one lighthouse.",
"nolighthouses_list_item2": "If you plan to access hosts over the Internet, at least one lighthouse will need a static, public IPv4 address with its firewall configured to allow inbound udp traffic on a specific port.",
"nolighthouses_list_item3": "A modestly-sized cloud instance should be sufficient for most users.",
"nolighthouses_list_item4": "Lighthouses are also hosts - they will have an IP and you will be able to access them over the network.",
"nolighthouses_cta": "Add lighthouse",
"norelays": "Adding a relay is recommended",
"norelays_list_item1": "Relays ensure connectivity for scenarios when direct connections fail.",
"norelays_list_item2": "Without a relay, some devices may be unable to communicate.",
"norelays_list_item3": "Your lighthouses also act as relays.",
"norelays_lighthouse": "You currently have {} lighthouses acting as relays",
"norelays_cta": "Add relay",
"roles_title": "Roles",
"roles_bar": "Roles control how hosts, lighthouses, and relays communicate through firewall rules.",
"roles_cta": "Add",
"guide": "Read the guide →",
"noroles": "Wow, such empty",
"noroles_cta": "You don't have any roles. Consider creating one with the button above.",
"roleaddprompt": "What's the name of your new role?",
"roleaddpromptdesc": "What's the description of your new role?",
"roleadd_cta": "Create role"
}
}

View File

@ -11,7 +11,7 @@
import {finishTOTPSetup} from "$lib/totp";
import {setCookie} from "$lib/cookie";
let logger = new Logger("auth/mfa/+page.svelte");
let logger = new Logger("auth/mfasetup/+page.svelte");
logSetup();
let api_token = "";

View File

@ -0,0 +1,86 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
let logger = new Logger("org/[orgid]/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
window.location.href = `/org/${orgid}/lighthouses`;
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
{/if}

View File

@ -0,0 +1,86 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "$components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/hosts/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="hosts" orgid="{orgid}">
</OrgWrapper>
{/if}

View File

@ -0,0 +1,86 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "../../../../components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/lighthouses/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="lighthouses" orgid="{orgid}">
</OrgWrapper>
{/if}

View File

@ -0,0 +1,86 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "$components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/relays/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="relays" orgid="{orgid}">
</OrgWrapper>
{/if}

View File

@ -0,0 +1,169 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "../../../../components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/roles/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
let roles = {
'data': []
};
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org/${orgid}/roles`, {
'method': 'GET',
'headers': {
'Authorization': `Bearer ${api_token}`
}
});
if (!resp.ok) {
let err = JSON.parse(await resp.text()).errors[0].message;
logger.error(`${err}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadroles');
fullPageErrorSubtitle = t('org.apierror.' + err);
return;
}
roles = JSON.parse(await resp.text());
} catch (e) {
logger.error(`${e}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadroles');
fullPageErrorSubtitle = t('org.apierror.' + `${e}`.replaceAll('.', ''));
return;
}
})
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<!-- BREAKING THE LAYOUT! -->
<!-- Sidenav :O -->
<OrgWrapper selected="roles" orgid="{orgid}">
<div class="w-full">
<!-- Top bar -->
<h1 class="align-middle inline mt-10 font-semibold text-2xl">{t('org.roles_title')}</h1>
<button on:click={() => {window.location.href = `/org/${orgid}/roles/add`}} class="align-middle inline w-20 h-31 float-right dark:bg-purple-700 rounded font-bold text-sm p-2 w-25 border-2 dark:border-purple-600 border-purple-700 bg-purple-200">{t('org.roles_cta')}</button>
<!-- Infobox -->
<div class="mt-5 p-4 bg-purple-800/20 text-sm rounded">
<span>{t('org.roles_bar')}</span>
<a class="inline text-purple-600 hover:text-purple-700 transition font-semibold"
href="/docs/roles">{t('org.guide')}</a>
</div>
<!-- Table -->
<!-- roles.data.length === 0 -->
{#if roles.data.length === 0}
<div class="mt-5">
<h2 class="text-center text-xl font-semibold">{t('org.noroles')}</h2>
<h3 class="text-center text-md">{t('org.noroles_cta')}</h3>
</div>
{:else}
<div class="mt-5">
<table class="rounded w-full bg-white dark:bg-slate-800 text-sm shadow-sm">
<tr>
<th class="w-1/4 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">
Name
</th>
<th class="w-1/8 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">ID
</th>
<th class="w-1/8 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">
Rules
</th>
<th class="w-3/4 border-b font-semibold p-4 text-slate-900 dark:text-slate-200 text-left">
Description
</th>
<th class="border-b p-4"></th>
</tr>
{#each roles.data as role}
<tr>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.name}</td>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.id}</td>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.firewall_rules.length}</td>
<td class="p-4 text-slate-600 dark:text-slate-300 text-left">{role.description}</td>
<td>
<button class="w-16 border p-2 rounded m-2 hover:bg-slate-100 transition dark:hover:bg-slate-700" on:click={() => {
window.location.href = `/org/${orgid}/roles/${role.id}`
}}>
Edit
</button>
</td>
</tr>
{/each}
</table>
</div>
{/if}
</div>
</OrgWrapper>
{/if}

View File

@ -0,0 +1,144 @@
<script lang="ts">
import {onMount} from "svelte";
import {enforce_auth, enforce_session} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {list_org} from "$lib/orgs";
import {t} from "$lib/i18n";
import FullPageError from "../../../../../components/FullPageError.svelte";
import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs";
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
import {page} from "$app/stores";
import OrgWrapper from "../../../../../components/org/OrgWrapper.svelte";
let logger = new Logger("org/[orgid]/roles/+page.svelte");
logSetup();
let orgid = $page.params.orgid;
let api_token = "";
let fullPageError = false;
let fullPageErrorTitle = "";
let fullPageErrorSubtitle = "";
let user_info;
let roles = {
'data': []
};
// this page requires session and mfa auth.
onMount(async () => {
let st_result = await enforce_session();
if (!st_result[0]) {
// Session token is invalid. redirect to login
window.location = "/auth/login";
return;
}
let at_result = await enforce_auth();
if (!at_result[0]) {
// Auth token is invalid. Redirect to mfa page.
window.location = "/auth/mfa";
return;
}
// user is fully authenticated and permitted to proceed
api_token = at_result[1];
logger.info("User authenticated successfully");
// GET USER INFO
user_info = await get_user_info(api_token);
if (typeof user_info === "string") {
logger.error(user_info);
fullPageError = true;
fullPageErrorTitle = t('admin.apierror.loaduser');
fullPageErrorSubtitle = t('admin.apierror.' + user_info);
return;
}
let org_list: string | number[] = await list_org(api_token);
if (typeof org_list === "string") {
// Error
logger.error(org_list);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadorg');
fullPageErrorSubtitle = t('org.apierror.' + org_list);
return;
}
let list: number[] = org_list;
if (!list.includes(Number(orgid))) {
fullPageError = true;
fullPageErrorTitle = t('org.apierror.notyourorg');
fullPageErrorSubtitle = t('org.apierror.notyourorgexplainer');
return;
}
})
let name = "";
let description = "";
async function createRole() {
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org/${orgid}/role`, {
'method': 'POST',
'headers': {
'Authorization': `Bearer ${api_token}`
},
'body': JSON.stringify({
'name': name,
'description': description
})
});
if (!resp.ok) {
let err = JSON.parse(await resp.text()).errors[0].message;
logger.error(`${err}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.addrole');
fullPageErrorSubtitle = t('org.apierror.' + err);
return;
}
roles = JSON.parse(await resp.text());
window.location.href = `/org/${orgid}/roles`
} catch (e) {
logger.error(`${e}`);
fullPageError = true;
fullPageErrorTitle = t('org.apierror.loadroles');
fullPageErrorSubtitle = t('org.apierror.' + `${e}`.replaceAll('.', ''));
return;
}
}
</script>
{#if fullPageError}
<FullPageError title="{fullPageErrorTitle}" subtitle="{fullPageErrorSubtitle}"/>
{:else}
<div class="flex in-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div class="w-full max-w-md">
<!-- The actual form -->
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={createRole}>
<div class="rounded-md shadow-sm">
<label for="name">{t('org.roleaddprompt')}</label>
<input bind:value={name} id="name"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
</div>
<div class="rounded-md shadow-sm mt-5">
<label for="desc">{t('org.roleaddpromptdesc')}</label>
<input bind:value={description} id="desc"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
</div>
<button class="bg-purple-400 dark:bg-purple-600 mt-4 w-full py-2 -space-y-px rounded-md shadow-sm place-content-center">
{t('org.roleadd_cta')}
</button>
</form>
</div>
</div>
{/if}

View File

@ -11,7 +11,7 @@
import {API_ROOT} from "$lib/config";
import {fetch_timeout} from "$lib/util";
let logger = new Logger("admin/+page.svelte");
let logger = new Logger("org/new/+page.svelte");
logSetup();
let api_token = "";
@ -111,15 +111,18 @@
fullPageErrorSubtitle = t('neworg.apierror.invaliddata');
return;
}
let err = JSON.parse(await resp.text()).errors[0].message;
logger.error(`${await resp.json().errors[0]}`);
let text = await resp.text();
console.log(text);
let err = JSON.parse(text).errors[0].message;
logger.error(`${err}`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.' + err);
return;
}
let resp_objectified = JSON.parse(await resp.text());
console.log(JSON.stringify(resp_objectified));
let text = await resp.text();
console.log(text);
let resp_objectified = JSON.parse(text);
created_org_id = resp_objectified.org_id;
logger.info("Able to create organization with id " + created_org_id);
logger.info("Org create success! Redirecting to manage page");

View File

@ -15,6 +15,12 @@
],
"$lib/*": [
"./src/lib/*"
],
"$components": [
"./src/components"
],
"$components/*": [
"./src/components/*"
]
}
}

View File

@ -0,0 +1,22 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE roles (
id SERIAL NOT NULL PRIMARY KEY,
org SERIAL NOT NULL REFERENCES organizations(id),
name VARCHAR(128) NOT NULL,
description VARCHAR(4096) NOT NULL
);

View File

@ -0,0 +1,27 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE roles_firewall_rules (
id SERIAL NOT NULL PRIMARY KEY,
role SERIAL NOT NULL REFERENCES roles(id),
protocol INTEGER NOT NULL, -- 0: any 1: tcp 2: udp 3: icmp
port_range_start INTEGER NOT NULL, -- min: 1 max: 65535. Ignored if protocol==3
port_range_end INTEGER NOT NULL, -- min: 1 max: 65535, must be greater than or equal to port_range_start. Ignored if protocol==3
allow_from INTEGER NOT NULL, -- Allow traffic goverened by above rules from who?
-- -1: anybody
-- (a role, anything else): only that role
description VARCHAR(4096) NOT NULL
);

View File

@ -41,6 +41,7 @@ pub mod auth;
pub mod crypto;
pub mod org;
pub mod kv;
pub mod role;
static MIGRATOR: Migrator = sqlx::migrate!();
@ -183,7 +184,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
crate::routes::v1::user::options,
crate::routes::v1::organization::createorgoptions,
crate::routes::v1::ca::get_cas_for_org,
crate::routes::v1::ca::options
crate::routes::v1::ca::options,
crate::routes::v1::roles::get_roles,
crate::routes::v1::roles::options,
crate::routes::v1::roles::options_roleadd,
crate::routes::v1::roles::role_add
])
.register("/", catchers![
crate::routes::handler_400,

98
trifid-api/src/role.rs Normal file
View File

@ -0,0 +1,98 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error;
use sqlx::PgPool;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[repr(i32)]
pub enum Protocol {
Any = 0,
TCP = 1,
UDP = 2,
ICMP = 3
}
#[derive(Serialize, Deserialize)]
pub enum AllowFrom {
Anyone,
SpecificRole(i32)
}
#[derive(Serialize, Deserialize)]
pub struct FirewallRule {
pub id: i32,
pub protocol: Protocol,
pub port_start: u16,
pub port_end: u16,
pub allow_from: AllowFrom,
pub description: String
}
#[derive(Serialize, Deserialize)]
pub struct Role {
pub id: i32,
pub org_id: i32,
pub name: String,
pub description: String,
pub firewall_rules: Vec<FirewallRule>
}
pub async fn get_role(role_id: i32, db: &PgPool) -> Result<Option<Role>, Box<dyn Error>> {
let query_result: Option<_> = sqlx::query!("SELECT * FROM roles WHERE id = $1", role_id).fetch_optional(db).await?;
if let Some(res) = query_result {
// get all firewall rules
let rules_res = sqlx::query!("SELECT * FROM roles_firewall_rules WHERE role = $1", Some(role_id)).fetch_all(db).await?;
let mut rules = vec![];
for rule in rules_res {
rules.push(FirewallRule {
id: rule.id,
protocol: match rule.protocol {
0 => Protocol::Any,
1 => Protocol::TCP,
2 => Protocol::UDP,
3 => Protocol::ICMP,
_ => return Err(format!("invalid protocol on a firewall rule {}", rule.id).into())
},
port_start: rule.port_range_start as u16,
port_end: rule.port_range_end as u16,
allow_from: match rule.allow_from {
-1 => AllowFrom::Anyone,
_ => AllowFrom::SpecificRole(rule.allow_from)
},
description: rule.description,
})
}
Ok(Some(Role {
id: role_id,
org_id: res.org,
name: res.name,
description: res.description,
firewall_rules: rules,
}))
} else {
Ok(None)
}
}
pub async fn get_role_ids_for_ca(org_id: i32, db: &PgPool) -> Result<Vec<i32>, Box<dyn Error>> {
Ok(sqlx::query!("SELECT id FROM roles WHERE org = $1", org_id).fetch_all(db).await?.iter().map(|r| r.id).collect())
}

View File

@ -21,4 +21,5 @@ pub mod totp_authenticators;
pub mod verify_totp_authenticator;
pub mod organization;
pub mod user;
pub mod ca;
pub mod ca;
pub mod roles;

View File

@ -162,6 +162,23 @@ pub async fn create_org(req: Json<CreateCARequest>, user: TOTPAuthenticatedUserI
let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;
// last step: create a default role to allow pings from all hosts
let role_id = match sqlx::query!("INSERT INTO roles (org, name, description) VALUES ($1, 'Default', 'Allow pings from other hosts. Default role for new hosts.') RETURNING id", result.id).fetch_one(db.inner()).await {
Ok(r) => r.id,
Err(e) => {
error!("[tfapi] dberror: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role")));
}
};
match sqlx::query!("INSERT INTO roles_firewall_rules (role, protocol, port_range_start, port_range_end, allow_from, description) VALUES ($1, 3, 1, 1, -1, 'Allow pings from anyone on the network')", role_id).execute(db.inner()).await {
Ok(_) => {},
Err(e) => {
error!("[tfapi] dberror: {} inserting on roleid {}", e, role_id);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role firewall rules")));
}
}
Ok((ContentType::JSON, Json(
OrginfoStruct {
org_id: result.id,

View File

@ -0,0 +1,95 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use log::error;
use rocket::{options, get, State, post};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use sqlx::PgPool;
use crate::auth::TOTPAuthenticatedUserInfo;
use crate::org::user_has_org_assoc;
use crate::role::{get_role, get_role_ids_for_ca, Role};
#[options("/v1/org/<_org>/roles")]
pub fn options(_org: i32) -> &'static str { "" }
#[options("/v1/org/<_org>/role")]
pub fn options_roleadd(_org: i32) -> &'static str { "" }
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct RolesResponse {
pub data: Vec<Role>
}
#[get("/v1/org/<org>/roles")]
pub async fn get_roles(org: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<RolesResponse>), (Status, String)> {
if !user_has_org_assoc(user.user_id, org, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
}
let roles = match get_role_ids_for_ca(org, db.inner()).await {
Ok(r) => r,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", e)))
}
};
let mut resp = RolesResponse {
data: vec![]
};
for role in roles {
let role_info = match get_role(role, db.inner()).await {
Ok(r) => r,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", e)))
}
};
if let Some(info) = role_info {
resp.data.push(info);
} else {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", "missing role as returned by server - possibly deleted inbetween?")))
}
}
Ok((ContentType::JSON, Json(resp)))
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct RoleaddReq {
pub name: String,
pub description: String
}
#[post("/v1/org/<org>/role", data = "<req>")]
pub async fn role_add(req: Json<RoleaddReq>, org: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, String), (Status, String)> {
if !user_has_org_assoc(user.user_id, org, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
}
match sqlx::query!("INSERT INTO roles (org, name, description) VALUES ($1, $2, $3)", org, req.name, req.description).execute(db.inner()).await {
Ok(_) => (),
Err(e) => {
error!("[tfapi] dberror: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "database returned error while trying to create role", "unable to insert role")))
}
}
Ok((ContentType::JSON, "{}".to_string()))
}