merge milestone:feat-org-keys (workable)

This commit is contained in:
c0repwn3r 2023-03-03 11:50:01 -05:00
commit 8d45d2c6a8
Signed by: core
GPG Key ID: FDBF740DADDCEECF
27 changed files with 1278 additions and 22 deletions

4
Cargo.lock generated
View File

@ -1022,6 +1022,9 @@ name = "ipnet"
version = "2.7.1" version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
@ -2403,6 +2406,7 @@ dependencies = [
"chrono", "chrono",
"dotenvy", "dotenvy",
"hex", "hex",
"ipnet",
"log", "log",
"paste", "paste",
"rand", "rand",

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

@ -92,6 +92,62 @@
"subnetprompt": "What subnets would you like to allow?", "subnetprompt": "What subnets would you like to allow?",
"subnethelp": "Comma-separated list of subnets in CIDR notation. This will constrain which subnets can be applied to client certs. Default: empty (any)", "subnethelp": "Comma-separated list of subnets in CIDR notation. This will constrain which subnets can be applied to client certs. Default: empty (any)",
"groupprompt": "What groups would you like to allow?", "groupprompt": "What groups would you like to allow?",
"grouphelp": "Comma-separated list of groups. This will constrain which groups can be applied to client certs. Default: empty (any)" "grouphelp": "Comma-separated list of groups. This will constrain which groups can be applied to client certs. Default: empty (any)",
"apierror": {
"orgcreate": "Unable to create organization",
"cacreate": "Unable to create signing CA",
"TypeError: NetworkError when attempting to fetch resource": "Unable to contact the backend. Please try again later.",
"invaliddata": "The server was unable to parse your request. Reload the page and try again. Ensure all fields are correct."
}
},
"admin": {
"apierror": {
"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

@ -1,4 +1,4 @@
export async function fetch_timeout(resource: RequestInfo | URL, options = {}) { export async function fetch_timeout(resource: RequestInfo | URL, options: RequestInit | undefined = {}) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
const { timeout = 8000 } = options; const { timeout = 8000 } = options;

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

@ -8,8 +8,10 @@
import {get_user_info} from "$lib/auth"; import {get_user_info} from "$lib/auth";
import {org} from "$lib/orgs"; import {org} from "$lib/orgs";
import type {Organization} from "$lib/orgs"; import type {Organization} from "$lib/orgs";
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(); logSetup();
let api_token = ""; let api_token = "";
@ -73,8 +75,65 @@
let showAdditionalConstraints = false; let showAdditionalConstraints = false;
function newOrg() { function handle(listfield: string): string[] {
if (listfield == "") {
return []
} else {
return listfield.split(',')
}
}
async function doCreateFlow() {
// STEP ONE: Create the org
logger.info("Creating organization");
let created_org_id;
try {
let resp = await fetch_timeout(`${API_ROOT}/v1/org`, {
'method': 'POST',
'headers': {
'Authorization': 'Bearer ' + api_token
},
'body': JSON.stringify({
'ip_ranges': handle(org_ip_range),
'subnet_ranges': handle(org_subnets),
'groups': handle(org_groups)
})
});
if (!resp.ok) {
if (resp.status === 422) {
// we had an invalid input
logger.error(`Invalid input`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.invaliddata');
return;
}
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 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");
window.location.href = "/org/" + created_org_id;
} catch (e) {
logger.error(`${e}`);
fullPageError = true;
fullPageErrorTitle = t('neworg.apierror.orgcreate');
fullPageErrorSubtitle = t('neworg.apierror.' + `${e}`.replaceAll('.', ''));
return;
}
} }
</script> </script>
@ -90,7 +149,7 @@
<h3 class="text-sm">{t('neworg.cahelp')}</h3> <h3 class="text-sm">{t('neworg.cahelp')}</h3>
</div> </div>
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={newOrg}> <form class="mt-5" action="#" method="POST" on:submit|preventDefault={doCreateFlow}>
<div class=" rounded-md shadow-sm"> <div class=" rounded-md shadow-sm">
<label for="iprange" class="mb-5">{t('neworg.iprangeprompt')}</label> <label for="iprange" class="mb-5">{t('neworg.iprangeprompt')}</label>
<input bind:value={org_ip_range} id="iprange" <input bind:value={org_ip_range} id="iprange"

View File

@ -8,7 +8,21 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true,
"paths": {
"$lib": [
"./src/lib"
],
"$lib/*": [
"./src/lib/*"
],
"$components": [
"./src/components"
],
"$components/*": [
"./src/components/*"
]
}
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //

View File

@ -25,3 +25,4 @@ hex = "0.4.3"
rand = "0.8.5" rand = "0.8.5"
trifid-pki = { version = "0.1.3", path = "../trifid-pki" } trifid-pki = { version = "0.1.3", path = "../trifid-pki" }
sha2 = "0.10.6" sha2 = "0.10.6"
ipnet = { version = "2.7.1", features = ["serde"] }

View File

@ -72,3 +72,10 @@ totp_verification_valid_for = 3600
# Do not change this value in a production instance. It will make existing data inaccessible until changed back. # Do not change this value in a production instance. It will make existing data inaccessible until changed back.
# ------- WARNING ------- # ------- WARNING -------
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2" data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
# How long should CA certs be valid for before they need to be replaced (in seconds)?
# This controls the maximum amount of time a network on this instance can go
# without a rekey.
# You probably don't need to change, this, 31536000 (1 year) is a sane default.
# This value only affects new certs signed by this instance.
ca_certs_valid_for = 31536000

View File

@ -6,3 +6,4 @@ magic_links_valid_for = 86400
session_tokens_valid_for = 86400 session_tokens_valid_for = 86400
totp_verification_valid_for = 3600 totp_verification_valid_for = 3600
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2" data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
ca_certs_valid_for = 31536000

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

@ -23,8 +23,9 @@ pub struct TFConfig {
pub db_url: String, pub db_url: String,
pub base: Url, pub base: Url,
pub web_root: Url, pub web_root: Url,
pub magic_links_valid_for: i64, pub magic_links_valid_for: u64,
pub session_tokens_valid_for: i64, pub session_tokens_valid_for: u64,
pub totp_verification_valid_for: i64, pub totp_verification_valid_for: u64,
pub data_key: String pub data_key: String,
pub ca_certs_valid_for: u64
} }

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!();
@ -180,7 +181,14 @@ async fn main() -> Result<(), Box<dyn Error>> {
crate::routes::v1::organization::orglist_req, crate::routes::v1::organization::orglist_req,
crate::routes::v1::organization::create_org, crate::routes::v1::organization::create_org,
crate::routes::v1::user::get_user, crate::routes::v1::user::get_user,
crate::routes::v1::user::options 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::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,

View File

@ -15,11 +15,13 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error; use std::error::Error;
use rocket::form::validate::Contains; use rocket::form::validate::Contains;
use sqlx::PgPool; use sqlx::PgPool;
use trifid_pki::ca::NebulaCAPool; use trifid_pki::ca::NebulaCAPool;
pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result<Option<i32>, Box<dyn Error>> { pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result<Option<i32>, Box<dyn Error>> {
Ok(sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await?.map(|r| r.id)) Ok(sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await?.map(|r| r.id))
} }
@ -60,7 +62,9 @@ pub async fn get_associated_orgs(user: i32, db: &PgPool) -> Result<Vec<i32>, Box
} }
pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> { pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
Ok(get_associated_orgs(user, db).await?.contains(org)) let associated_orgs = get_associated_orgs(user, db).await?;
Ok(associated_orgs.contains(org))
} }
pub async fn get_org_ca_pool(org: i32, db: &PgPool) -> Result<NebulaCAPool, Box<dyn Error>> { pub async fn get_org_ca_pool(org: i32, db: &PgPool) -> Result<NebulaCAPool, Box<dyn Error>> {

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

@ -0,0 +1,85 @@
// 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 rocket::{options, get, State};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use sqlx::PgPool;
use serde::{Serialize, Deserialize};
use crate::auth::TOTPAuthenticatedUserInfo;
use crate::org::{get_associated_orgs, get_org_ca_pool};
#[options("/v1/org/<_id>/ca")]
pub fn options(_id: i32) -> &'static str {
""
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CaList {
pub trusted_cas: Vec<CA>,
pub blocklisted_certs: Vec<String>
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CA {
pub fingerprint: String,
pub cert: String
}
#[get("/v1/org/<id>/ca")]
pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<CaList>), (Status, String)> {
let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {
Ok(r) => r,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query", e)))
};
if !associated_orgs.contains(&id) {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_NOT_YOUR_ORG", "you are not authorized to view details of this org")))
}
let ca_pool = match get_org_ca_pool(id, db.inner()).await {
Ok(pool) => pool,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to load certificates from database - {}\"}}]}}", e)));
}
};
let mut trusted_cas = vec![];
for (fingerprint, cert) in ca_pool.cas {
trusted_cas.push(CA {
fingerprint,
cert: match cert.serialize_to_pem() {
Ok(pem) => match String::from_utf8(pem) {
Ok(str) => str,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to encode one of the serialized certificates - {}\"}}]}}", e)))
},
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to serialize one of the certificates - {}\"}}]}}", e)));
}
}
})
}
Ok((ContentType::JSON, Json(CaList {
trusted_cas,
blocklisted_certs: ca_pool.cert_blocklist,
})))
}

View File

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

View File

@ -14,11 +14,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::{get, post, options, State}; use std::time::{Duration, SystemTime};
use ipnet::Ipv4Net;
use rocket::{get, post, options, State, error};
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::serde::json::Json; use rocket::serde::json::Json;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use sqlx::PgPool; use sqlx::PgPool;
use trifid_pki::cert::{NebulaCertificate, NebulaCertificateDetails, serialize_ed25519_private};
use trifid_pki::ed25519_dalek::{SigningKey};
use trifid_pki::rand_core::OsRng;
use crate::auth::TOTPAuthenticatedUserInfo; use crate::auth::TOTPAuthenticatedUserInfo;
use crate::config::TFConfig; use crate::config::TFConfig;
use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config}; use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config};
@ -32,6 +37,8 @@ pub fn options() -> &'static str {
pub fn orgidoptions(_id: i32) -> &'static str { pub fn orgidoptions(_id: i32) -> &'static str {
"" ""
} }
#[options("/v1/org")]
pub fn createorgoptions() -> &'static str {""}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
@ -64,7 +71,7 @@ pub struct OrginfoStruct {
#[get("/v1/org/<orgid>")] #[get("/v1/org/<orgid>")]
pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> { pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
if !user_has_org_assoc(orgid, user.user_id, 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)))? { if !user_has_org_assoc(user.user_id, orgid, 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"))); return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
} }
@ -81,12 +88,59 @@ pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State
))) )))
} }
#[post("/v1/org")] #[derive(Serialize, Deserialize)]
pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> { #[serde(crate = "rocket::serde")]
pub struct CreateCARequest {
pub ip_ranges: Vec<Ipv4Net>,
pub subnet_ranges: Vec<Ipv4Net>,
pub groups: Vec<String>
}
#[post("/v1/org", data = "<req>")]
pub async fn create_org(req: Json<CreateCARequest>, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
if get_org_by_owner_id(user.user_id, 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)))?.is_some() { if get_org_by_owner_id(user.user_id, 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)))?.is_some() {
return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time"))) return Err((Status::Conflict, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_USER_OWNS_ORG", "a user can only own one organization at a time")))
} }
// Generate the CA keypair
let private_key = SigningKey::generate(&mut OsRng);
let public_key = private_key.verifying_key();
// Create the CA certificate
let mut ca_cert = NebulaCertificate {
details: NebulaCertificateDetails {
name: format!("{}'s Organization - Root Signing CA", user.email),
ips: req.ip_ranges.clone(),
subnets: req.subnet_ranges.clone(),
groups: req.groups.clone(),
not_before: SystemTime::now(),
not_after: SystemTime::now() + Duration::from_secs(config.ca_certs_valid_for),
public_key: public_key.to_bytes(),
is_ca: true,
issuer: "".to_string(), // This is a self-signed certificate! There is no issuer present
},
signature: vec![],
};
// Self-sign the CA certificate
match ca_cert.sign(&private_key) {
Ok(_) => (),
Err(e) => {
error!("[tfapi] security: certificate signature error: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_SIGN_ERROR", "there was an error generating the CA certificate, please try again later")))
}
}
// PEM-encode the CA key
let ca_key_pem = serialize_ed25519_private(&private_key.to_keypair_bytes());
// PEM-encode the CA cert
let ca_cert_pem = match ca_cert.serialize_to_pem() {
Ok(pem) => pem,
Err(e) => {
error!("[tfapi] security: certificate encoding error: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_CERT_ENCODE_ERROR", "there was an error encoding the CA certificate, please try again later")))
}
};
// generate an AES iv to use for key encryption // generate an AES iv to use for key encryption
let iv = generate_random_iv(); let iv = generate_random_iv();
let iv_hex = hex::encode(iv); let iv_hex = hex::encode(iv);
@ -98,16 +152,33 @@ pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, con
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e))); return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e)));
} }
}; };
let ca_key = match encrypt_with_nonce(b"", iv, &cipher) { let ca_key = match encrypt_with_nonce(&ca_key_pem, iv, &cipher) {
Ok(key) => hex::encode(key), Ok(key) => hex::encode(key),
Err(e) => { Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e))); return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e)));
} }
}; };
let ca_crt = hex::encode(b""); let ca_crt = hex::encode(ca_cert_pem);
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()))
}

View File

@ -74,7 +74,7 @@ pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) ->
let otpid = format!("totp-{}", Uuid::new_v4()); let otpid = format!("totp-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?; sqlx::query!("INSERT INTO totp_create_tokens (id, expires_on, totp_otpurl, totp_secret) VALUES ($1, $2, $3, $4);", otpid.clone(), (SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;
Ok((otpid, totpmachine)) Ok((otpid, totpmachine))
} }