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

View File

@ -15,6 +15,12 @@
], ],
"$lib/*": [ "$lib/*": [
"./src/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 crypto;
pub mod org; pub mod org;
pub mod kv; pub mod kv;
pub mod role;
static MIGRATOR: Migrator = sqlx::migrate!(); static MIGRATOR: Migrator = sqlx::migrate!();
@ -183,7 +184,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
crate::routes::v1::user::options, crate::routes::v1::user::options,
crate::routes::v1::organization::createorgoptions, crate::routes::v1::organization::createorgoptions,
crate::routes::v1::ca::get_cas_for_org, 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![ .register("/", catchers![
crate::routes::handler_400, 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

@ -22,3 +22,4 @@ pub mod verify_totp_authenticator;
pub mod organization; pub mod organization;
pub mod user; 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)))?; 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( Ok((ContentType::JSON, Json(
OrginfoStruct { OrginfoStruct {
org_id: result.id, 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()))
}