This commit is contained in:
core 2023-06-15 15:53:56 -04:00
parent f4c0a1718e
commit f52db518e7
Signed by: core
GPG Key ID: FDBF740DADDCEECF
16 changed files with 931 additions and 63 deletions

View File

@ -0,0 +1,12 @@
<script lang="ts">
import {t} from "svelte-i18n";
export let selected;
</script>
<div>
<button disabled="{selected === 'hosts'}" on:click={() => {window.location.href="/hosts"}}>{$t("common.page.hosts")}</button>
<button disabled="{selected === 'lighthouses'}" on:click={() => {window.location.href="/lighthouses"}}>{$t("common.page.lighthouses")}</button>
<button disabled="{selected === 'relays'}" on:click={() => {window.location.href="/relays"}}>{$t("common.page.relays")}</button>
<button disabled="{selected === 'roles'}" on:click={() => {window.location.href="/roles"}}>{$t("common.page.roles")}</button>
</div>

View File

@ -12,43 +12,43 @@
* Do not edit the class manually.
*/
import type {ActorAPIKey} from './ActorAPIKey';
import {
ActorAPIKey,
instanceOfActorAPIKey,
ActorAPIKeyFromJSON,
ActorAPIKeyFromJSONTyped,
ActorAPIKeyToJSON,
} from './ActorAPIKey';
import type {ActorHost} from './ActorHost';
import {
ActorHost,
instanceOfActorHost,
ActorHostFromJSON,
ActorHostFromJSONTyped,
ActorHostToJSON,
} from './ActorHost';
import type {ActorOIDCUser} from './ActorOIDCUser';
import {
ActorOIDCUser,
instanceOfActorOIDCUser,
ActorOIDCUserFromJSON,
ActorOIDCUserFromJSONTyped,
ActorOIDCUserToJSON,
} from './ActorOIDCUser';
import type {ActorSupport} from './ActorSupport';
import {
ActorSupport,
instanceOfActorSupport,
ActorSupportFromJSON,
ActorSupportFromJSONTyped,
ActorSupportToJSON,
} from './ActorSupport';
import type {ActorSystem} from './ActorSystem';
import {
ActorSystem,
instanceOfActorSystem,
ActorSystemFromJSON,
ActorSystemFromJSONTyped,
ActorSystemToJSON,
} from './ActorSystem';
import type {ActorUser} from './ActorUser';
import {
ActorUser,
instanceOfActorUser,
ActorUserFromJSON,
ActorUserFromJSONTyped,

View File

@ -1,7 +1,7 @@
import {Logger, logSetup} from "$lib/logger";
import {PUBLIC_BASE_URL} from "$env/static/public";
export enum AuthResult {
export enum APIResult {
Failed = 0,
Successful = 1
}
@ -22,12 +22,12 @@ export interface APIError {
logSetup();
const logger = new Logger("auth.ts");
export async function isAuthedSession(): Promise<[AuthResult, SessionInfo | APIError]> {
export async function isAuthedSession(): Promise<[APIResult, SessionInfo | APIError]> {
logger.info('Checking for session authentication');
if (window.localStorage.getItem("session") === null) {
logger.error('unable to check session: session token not set');
return [AuthResult.Failed, {code: "missing_token", message: "not logged in"}];
return [APIResult.Failed, {code: "missing_token", message: "not logged in"}];
}
try {
@ -41,27 +41,27 @@ export async function isAuthedSession(): Promise<[AuthResult, SessionInfo | APIE
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error fetching user information: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, JSON.parse(await resp.text()).data.actor]
return [APIResult.Successful, JSON.parse(await resp.text()).data.actor]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function isAuthedMFA(): Promise<[AuthResult, SessionInfo | APIError]> {
export async function isAuthedMFA(): Promise<[APIResult, SessionInfo | APIError]> {
logger.info('Checking for MFA authentication');
if (window.localStorage.getItem("mfa") === null) {
logger.error('unable to check mfa: mfa token not set');
return [AuthResult.Failed, {code: "missing_token", message: "not logged in"}];
return [APIResult.Failed, {code: "missing_token", message: "not logged in"}];
}
const sess = await isAuthedSession();
if (sess[0] !== AuthResult.Successful) {
if (sess[0] !== APIResult.Successful) {
logger.error('unable to check mfa: session token invalid');
return [AuthResult.Failed, sess[1]];
return [APIResult.Failed, sess[1]];
}
try {
@ -75,16 +75,16 @@ export async function isAuthedMFA(): Promise<[AuthResult, SessionInfo | APIError
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error fetching user information: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, JSON.parse(await resp.text()).data.actor]
return [APIResult.Successful, JSON.parse(await resp.text()).data.actor]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function authSession(email: string): Promise<[AuthResult, null | APIError]> {
export async function authSession(email: string): Promise<[APIResult, null | APIError]> {
logger.info('Sending new session authentication');
try {
@ -101,16 +101,16 @@ export async function authSession(email: string): Promise<[AuthResult, null | AP
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending authentication: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, null]
return [APIResult.Successful, null]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function signup(email: string): Promise<[AuthResult, null | APIError]> {
export async function signup(email: string): Promise<[APIResult, null | APIError]> {
logger.info('sending signup');
try {
@ -127,16 +127,16 @@ export async function signup(email: string): Promise<[AuthResult, null | APIErro
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending authentication: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, null]
return [APIResult.Successful, null]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function verifyLink(ml: string): Promise<[AuthResult, null | APIError]> {
export async function verifyLink(ml: string): Promise<[APIResult, null | APIError]> {
logger.info('checking magic link authentication');
try {
@ -153,15 +153,15 @@ export async function verifyLink(ml: string): Promise<[AuthResult, null | APIErr
if (!resp.ok) {
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending authentication: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
window.localStorage.setItem("session", JSON.parse(await resp.text()).data.sessionToken);
return [AuthResult.Successful, null]
return [APIResult.Successful, null]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
@ -171,7 +171,7 @@ export interface TOTPCreateInfo {
url: string
}
export async function createTotp(token: string): Promise<[AuthResult, TOTPCreateInfo | APIError]> {
export async function createTotp(token: string): Promise<[APIResult, TOTPCreateInfo | APIError]> {
logger.info('creating totp authenticator');
try {
@ -188,21 +188,21 @@ export async function createTotp(token: string): Promise<[AuthResult, TOTPCreate
logger.error('call returned error code');
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending totp create: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [AuthResult.Successful, JSON.parse(await resp.text()).data]
return [APIResult.Successful, JSON.parse(await resp.text()).data]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function verifyTotp(token: string, totpToken: string, totpCode: string): Promise<[AuthResult, undefined | APIError]> {
export async function verifyTotp(token: string, totpToken: string, totpCode: string): Promise<[APIResult, undefined | APIError]> {
logger.info('verifying totp authenticator');
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/totp-authenticators`, {
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/verify-totp-authenticators`, {
'method': 'POST',
'body': JSON.stringify({
totpToken: totpToken,
@ -217,14 +217,45 @@ export async function verifyTotp(token: string, totpToken: string, totpCode: str
logger.error('call returned error code');
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending totp create: ${rawerror.message}`);
return [AuthResult.Failed, {code: rawerror.code, message: rawerror.message}];
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
window.localStorage.setItem("mfa", JSON.parse(await resp.text()).data.authToken);
return [AuthResult.Successful, undefined]
return [APIResult.Successful, undefined]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [AuthResult.Failed, {code: "api_call_failed", message: `${e}`}]
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}
export async function authTotp(token: string, totpCode: string): Promise<[APIResult, undefined | APIError]> {
logger.info('authorizing totp authenticator');
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/auth/totp`, {
'method': 'POST',
'body': JSON.stringify({
code: `${totpCode}`
}),
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!resp.ok) {
logger.error('call returned error code');
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending totp create: ${rawerror.message}`);
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
window.localStorage.setItem("mfa", JSON.parse(await resp.text()).data.authToken);
return [APIResult.Successful, undefined]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}

View File

@ -63,6 +63,50 @@
"label": "TOTP Code",
"button": "Verify"
},
"networkcreate": {
"title": "Create your network",
"explain": "This defines what IP addresses will be assigned to your devices. The range you enter below must fall within either the RFC 1918 Private Address Space or the RFC 6598 Shared Address Space. Enter your network via CIDR notation.",
"label": "Network range",
"button": "Create network",
"valid": "Valid - {numIps} addresses from {start} to {end}",
"invalid": "Invalid",
"error": {
"generic": "Unable to create network: {err}",
"api": "Unable to contact the server. Try again later."
}
},
"roles": {
"create": "Add",
"explain": "Roles control how hosts, lighthouses, and relays communicate through firewall rules.",
"noroles": "You don't have any roles. You'll need to add at least one before you can add any hosts.",
"add": {
"any": "Any",
"name": "Role name",
"desc": "Role description",
"button": "Create role",
"cancel": "Cancel",
"rules": "Inbound firewall rules",
"rulesexplainer": "Inbound traffic is denied by default. Add rules to allow traffic from hosts belonging to specific roles.",
"rulescols": {
"description": "Description",
"protocol": "Protocol",
"portrange": "Port range",
"allowedrole": "Allowed role"
},
"ruleremove": "Remove rule",
"ruleedit": "Edit rule",
"rulesadd": "Add rule",
"editrule": {
"protocol": "Protocol",
"range": "Port or port range",
"role": "Allowed role",
"desc": "Description",
"add": "Add rule",
"edit": "Edit rule",
"cancel": "Cancel"
}
}
},
"common": {
"title": "{title} | Trifid Web UI",
"page": {
@ -71,7 +115,10 @@
"login": "Login",
"ml": "Verify Magic Link",
"2fasetup": "Configure TOTP",
"2fa": "2-Factor Authentication"
"2fa": "2-Factor Authentication",
"networkcreate": "Create Network",
"hosts": "Hosts",
"roles": "Roles"
}
}
}

View File

@ -0,0 +1,42 @@
import {APIResult} from "$lib/auth";
import type {APIError} from "$lib/auth";
import {Logger, logSetup} from "$lib/logger";
import {PUBLIC_BASE_URL} from "$env/static/public";
logSetup();
const logger = new Logger("netcreate.ts");
export interface OrgCreateInfo {
organization: string,
ca: string,
network: string
}
export async function createNetwork(token: string, cidr: string): Promise<[APIResult, OrgCreateInfo | APIError]> {
logger.info('creating network');
try {
logger.debug(`api call: baseurl ${PUBLIC_BASE_URL}`);
const resp = await fetch(`${PUBLIC_BASE_URL}/v1/organization`, {
'method': 'POST',
'body': JSON.stringify({
cidr: `${cidr}`
}),
'headers': {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!resp.ok) {
logger.error('call returned error code');
const rawerror = JSON.parse(await resp.text()).errors[0];
logger.error(`error sending org create: ${rawerror.message}`);
return [APIResult.Failed, {code: rawerror.code, message: rawerror.message}];
}
return [APIResult.Successful, await resp.json()]
} catch (e) {
logger.error(`error making API request: ${e}`);
return [APIResult.Failed, {code: "api_call_failed", message: `${e}`}]
}
}

View File

@ -2,7 +2,7 @@
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, authSession, isAuthedSession} from "$lib/auth.ts";
import {APIResult, authSession, authTotp, isAuthedSession, verifyTotp} from "$lib/auth.ts";
import type {SessionInfo} from "$lib/auth.ts";
import type {APIError} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
@ -17,7 +17,7 @@
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == AuthResult.Failed) {
if (session_load_info[0] == APIResult.Failed) {
logger.error(`session load fail, the user is not already logged in`);
window.location.href = '/login';
return;
@ -39,6 +39,29 @@
async function onSubmit() {
loading = true;
let create_res = await authTotp(window.localStorage.getItem("session"), mfacode);
if (create_res[0] == APIResult.Failed) {
logger.error(`totp auth fail`);
isError = true;
let err = create_res[1] as APIError;
let etext = err.code;
if (etext === "api_call_failed") {
etext = $t('2fasetup.error.api');
} else if (etext === "ERR_USER_NO_TOTP") {
window.location.href = '/2fasetup';
return;
}
error = $t('2fasetup.error.generic', {values:{err:etext}});
loading = false;
return;
}
window.location.href = '/admin';
loading = false;
}
</script>
@ -52,7 +75,7 @@
<h3>{$t('2fa.subtitle')}</h3>
<form on:submit|preventDefault={onSubmit}>
<label for="mfacode">{$t('2fa.label')}</label>
<input type="number" bind:value={mfacode} id="mfacode"/>
<input type="text" bind:value={mfacode} id="mfacode"/>
<button>{$t('2fa.button')}</button>
{#if hasErrForm}
<p>{errForm}</p>

View File

@ -2,7 +2,7 @@
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, authSession, createTotp, isAuthedSession, verifyTotp} from "$lib/auth.ts";
import {APIResult, authSession, createTotp, isAuthedSession, verifyTotp} from "$lib/auth.ts";
import type {TOTPCreateInfo} from "$lib/auth.ts";
import type {SessionInfo} from "$lib/auth.ts";
import type {APIError} from "$lib/auth.ts";
@ -26,7 +26,7 @@
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == AuthResult.Failed) {
if (session_load_info[0] == APIResult.Failed) {
logger.error(`session load fail, the user is not already logged in`);
window.location.href = '/login';
return;
@ -40,7 +40,7 @@
let create_res = await createTotp(window.localStorage.getItem("session"));
if (create_res[0] == AuthResult.Failed) {
if (create_res[0] == APIResult.Failed) {
logger.error(`totp create fail`);
isError = true;
let err = create_res[1] as APIError;
@ -74,7 +74,7 @@
async function onSubmit() {
let create_res = await verifyTotp(window.localStorage.getItem("session"), totp_create_token, code);
if (create_res[0] == AuthResult.Failed) {
if (create_res[0] == APIResult.Failed) {
logger.error(`totp auth fail`);
isError = true;
let err = create_res[1] as APIError;
@ -113,7 +113,7 @@
<div><span>{totp_secret}</span></div>
<form on:submit|preventDefault={onSubmit}>
<label for="code">{$t('2fasetup.verify')}</label>
<input bind:value={code} type="number" min="0" max="999999" id="code" />
<input bind:value={code} type="text" id="code" />
<button>{$t('2fasetup.button')}</button>
</form>
</LoadingWrapper>

View File

@ -2,9 +2,11 @@
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {APIResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
import type {APIError} from "$lib/auth.ts";
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi} from "$lib/api";
let loading = true;
let isError = false;
@ -16,7 +18,7 @@
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == AuthResult.Failed) {
if (session_load_info[0] == APIResult.Failed) {
let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login';
@ -24,13 +26,43 @@
}
let mfa_load_info = await isAuthedMFA();
if (mfa_load_info[0] == AuthResult.Failed) {
if (mfa_load_info[0] == APIResult.Failed) {
let err = mfa_load_info[1] as APIError;
logger.error(`mfa load failed: ${err.code} ${err.message}`);
window.location.href = '/2fa';
return;
}
// pull networks
const confgiuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const networksApi = new NetworksApi(confgiuration);
let networks;
try {
networks = await networksApi.networksList();
} catch (e) {
let resp_json = await e.response.json();
if (resp_json.errors[0].code == "ERR_NO_ORG") {
window.location.href = "/networkcreate";
return;
} else {
isError = true;
error = $t("networkcreate.error.generic", {values:{err:resp_json.errors[0].code}});
loading = false;
return;
}
}
console.log(networks);
if (networks.data?.length == 0) {
window.location.href = '/networkcreate';
return;
}
window.location.href = '/hosts';
loading = false;
})
</script>

View File

@ -0,0 +1,77 @@
<script lang="ts">
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {APIResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
import type {APIError} from "$lib/auth.ts";
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi} from "$lib/api";
import AdminBar from "$components/AdminBar.svelte";
let loading = true;
let isError = false;
let error = '';
$: currentlyLoading = $isLoading || loading;
logSetup();
let logger = new Logger("hosts/+page.svelte");
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == APIResult.Failed) {
let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login';
return;
}
let mfa_load_info = await isAuthedMFA();
if (mfa_load_info[0] == APIResult.Failed) {
let err = mfa_load_info[1] as APIError;
logger.error(`mfa load failed: ${err.code} ${err.message}`);
window.location.href = '/2fa';
return;
}
// pull networks
const configuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const networksApi = new NetworksApi(configuration);
let networks;
try {
networks = await networksApi.networksList();
} catch (e) {
let resp_json = await e.response.json();
if (resp_json.errors[0].code == "ERR_NO_ORG") {
window.location.href = "/networkcreate";
return;
} else {
isError = true;
error = $t("networkcreate.error.generic", {values:{err:resp_json.errors[0].code}});
loading = false;
return;
}
}
console.log(networks);
if (networks.data?.length == 0) {
window.location.href = '/networkcreate';
return;
}
loading = false;
})
</script>
<svelte:head>
<title>{$t("common.title", {values: {title: $t("common.page.hosts")}})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<AdminBar selected="hosts" />
<h1>its not loading anymore</h1>
</LoadingWrapper>

View File

@ -2,7 +2,7 @@
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, authSession, isAuthedSession} from "$lib/auth.ts";
import {APIResult, authSession, isAuthedSession} from "$lib/auth.ts";
import type {APIError} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
@ -16,7 +16,7 @@
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] != AuthResult.Failed) {
if (session_load_info[0] != APIResult.Failed) {
logger.error(`session load success, the user is already logged in`);
window.location.href = '/2fa';
return;
@ -45,7 +45,7 @@
let auth_result = await authSession(email);
if (auth_result[0] === AuthResult.Failed) {
if (auth_result[0] === APIResult.Failed) {
hasErrForm = true;

View File

@ -2,7 +2,7 @@
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, verifyLink} from "$lib/auth.ts";
import {APIResult, verifyLink} from "$lib/auth.ts";
import type {APIError} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
@ -24,7 +24,7 @@
let call_result = await verifyLink(url.get("magicLinkToken"));
if (call_result[0] !== AuthResult.Successful) {
if (call_result[0] !== APIResult.Successful) {
let err = (call_result[1] as APIError).code;
if (err === "ERR_UNAUTHORIZED" || err === "ERR_EXPIRED") {

View File

@ -0,0 +1,183 @@
<script lang="ts">
import {isLoading, number, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {APIResult, authTotp, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
import type {APIError} from "$lib/auth.ts";
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi, ResponseError} from "$lib/api";
import {createNetwork} from "$lib/netcreate";
let loading = true;
let isError = false;
let error = '';
$: currentlyLoading = $isLoading || loading;
logSetup();
let logger = new Logger("networkcreate/+page.svelte");
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == APIResult.Failed) {
let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login';
return;
}
let mfa_load_info = await isAuthedMFA();
if (mfa_load_info[0] == APIResult.Failed) {
let err = mfa_load_info[1] as APIError;
logger.error(`mfa load failed: ${err.code} ${err.message}`);
window.location.href = '/2fa';
return;
}
// pull networks
const confgiuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const networksApi = new NetworksApi(confgiuration);
let networks;
try {
networks = await networksApi.networksList();
} catch (e) {
let resp_json = await e.response.json();
if (resp_json.errors[0].code != "ERR_NO_ORG") {
isError = true;
error = $t("networkcreate.error.generic", {values:{err:resp_json.errors[0].code}});
loading = false;
return;
}
}
if (networks !== undefined && networks.data?.length != 0) {
window.location.href = '/admin';
return;
}
loading = false;
})
let cidr = "100.100.0.0/22";
let sub = $t("networkcreate.valid", {values:{numIps:$number(1024), start: "100.64.0.0", end: "100.127.255.255"}})
const regex = new RegExp(/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$/im);
function recalculateSub() {
// step 01: is it a valid CIDR?
logger.info(cidr);
if (!regex.test(cidr)) {
sub = $t("networkcreate.invalid");
logger.info("error: regex");
console.log(regex.exec(cidr));
return;
}
if (!cidr.includes("/")) {
sub = $t("networkcreate.invalid");
logger.info("error: noslash");
return;
}
// step 02: is it in the local networks range?
// 192.168.*
// 10.*
// 172.16.*-172.31.*
// 100.64.*-100.127.*
function isInPrivateRange(addr: number, mask: number): boolean {
const netaddr_10 = 0b00001010_00000000_00000000_00000000;
const mask_8 = 0b11111111_00000000_00000000_00000000;
if (addr == netaddr_10 && mask == 8) return true;
const netaddr_172_16 = 0b10101100_00010000_00000000_00000000;
const mask_12 = 0b11111111_11110000_00000000_00000000;
if (addr == netaddr_172_16 && mask == 12) return true;
const netaddr_192_168 = 0b11000000_10101000_00000000_00000000;
const mask_16 = 0b11111111_11111111_00000000_00000000;
if (addr == netaddr_192_168 && mask == 16) return true;
const netaddr_10_64 = 0b01100100_01000000_00000000_00000000;
const mask_10 = 0b11111111_11000000_00000000_00000000;
if (addr == netaddr_10_64 && mask == 10) return true;
return (((addr & mask_8) == (netaddr_10 & mask_8) && mask > 8) || ((addr & mask_12) == (netaddr_172_16 & mask_12) && mask > 12) || ((addr & mask_16) == (netaddr_192_168 & mask_16) && mask > 16) || ((addr & mask_10) == (netaddr_10_64 & mask_10) && mask > 10));
}
let [address, netmask] = cidr.split("/");
let address_octets = address.split(".");
let address_numerical = (parseInt(address_octets[0]) << 24 | parseInt(address_octets[1]) << 16 | parseInt(address_octets[2]) << 8 | parseInt(address_octets[3])) >>> 0;
if (!isInPrivateRange(address_numerical, parseInt(netmask))) {
sub = $t("networkcreate.invalid");
logger.info("not in private range");
return;
}
// step 03: calculate address count
let prefix = parseInt(netmask);
let addressCount = Math.pow(2, 32 - prefix) - 2; // minus 2 for first and last addresses not usable
// step 04: calc addresses
let first_num = address_numerical;
let last_num = address_numerical + addressCount;
let first = [first_num >>> 24 & 0xFF, first_num >>> 16 & 0xFF, first_num >>> 8 & 0xFF, first_num & 0xFF].join(".");
let last = [last_num >>> 24 & 0xFF, last_num >>> 16 & 0xFF, last_num >>> 8 & 0xFF, last_num & 0xFF].join(".");
sub = $t("networkcreate.valid", {values:{numIps:$number(addressCount), start: first, end: last}})
}
async function onSubmit() {
loading = true;
let create_res = await createNetwork(window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa"), cidr);
if (create_res[0] == APIResult.Failed) {
logger.error(`totp auth fail`);
isError = true;
let err = create_res[1] as APIError;
let etext = err.code;
if (etext === "api_call_failed") {
etext = $t('networkcreate.error.api');
} else if (etext === "ERR_USER_NO_TOTP") {
window.location.href = '/2fasetup';
return;
}
error = $t('networkcreate.error.generic', {values:{err:etext}});
loading = false;
return;
}
window.location.href = '/admin';
loading = false;
}
</script>
<svelte:head>
<title>{$t("common.title", {values: {title: $t("common.page.networkcreate")}})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<h1>{$t("networkcreate.title")}</h1>
<p>{$t("networkcreate.explain")}</p>
<form on:submit|preventDefault={onSubmit}>
<label for="cidr">{$t("networkcreate.label")}</label>
<input bind:value={cidr} on:input={recalculateSub} type="text" id="cidr" />
<button>{$t("networkcreate.button")}</button>
<span>{sub}</span>
</form>
</LoadingWrapper>

View File

@ -0,0 +1,97 @@
<script lang="ts">
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {APIResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
import type {APIError} from "$lib/auth.ts";
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi, RolesApi} from "$lib/api";
import AdminBar from "$components/AdminBar.svelte";
let loading = true;
let isError = false;
let error = '';
$: currentlyLoading = $isLoading || loading;
logSetup();
let logger = new Logger("roles/+page.svelte");
let roles;
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == APIResult.Failed) {
let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login';
return;
}
let mfa_load_info = await isAuthedMFA();
if (mfa_load_info[0] == APIResult.Failed) {
let err = mfa_load_info[1] as APIError;
logger.error(`mfa load failed: ${err.code} ${err.message}`);
window.location.href = '/2fa';
return;
}
// pull networks
const configuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const networksApi = new NetworksApi(configuration);
let networks;
try {
networks = await networksApi.networksList();
} catch (e) {
let resp_json = await e.response.json();
if (resp_json.errors[0].code == "ERR_NO_ORG") {
window.location.href = "/networkcreate";
return;
} else {
isError = true;
error = $t("networkcreate.error.generic", {values:{err:resp_json.errors[0].code}});
loading = false;
return;
}
}
console.log(networks);
if (networks.data?.length == 0) {
window.location.href = '/networkcreate';
return;
}
const rolesApi = new RolesApi(configuration);
roles = await rolesApi.rolesList();
console.log(roles);
loading = false;
})
async function roleAdd() {
window.location.href = "/roles/add";
}
</script>
<svelte:head>
<title>{$t("common.title", {values: {title: $t("common.page.roles")}})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<AdminBar selected="roles" />
<h1>{$t("common.page.roles")}</h1>
<button on:click={roleAdd}>{$t("roles.create")}</button>
<p>{$t("roles.explain")}</p>
{#if (roles.data.length === 0)}
<p>{$t("roles.noroles")}</p>
{/if}
</LoadingWrapper>

View File

@ -0,0 +1,296 @@
<script lang="ts">
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {APIResult, isAuthedMFA, isAuthedSession} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
import type {APIError} from "$lib/auth.ts";
import {PUBLIC_BASE_URL} from "$env/static/public";
import {Configuration, NetworksApi, RolesApi} from "$lib/api";
import AdminBar from "$components/AdminBar.svelte";
let loading = true;
let isError = false;
let error = '';
$: currentlyLoading = $isLoading || loading;
logSetup();
let logger = new Logger("roles/add/+page.svelte");
let roles;
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] == APIResult.Failed) {
let err = session_load_info[1] as APIError;
logger.error(`session load failed: ${err.code} ${err.message}`);
window.location.href = '/login';
return;
}
let mfa_load_info = await isAuthedMFA();
if (mfa_load_info[0] == APIResult.Failed) {
let err = mfa_load_info[1] as APIError;
logger.error(`mfa load failed: ${err.code} ${err.message}`);
window.location.href = '/2fa';
return;
}
// pull networks
const configuration = new Configuration({
basePath: PUBLIC_BASE_URL,
accessToken: window.localStorage.getItem("session") + " " + window.localStorage.getItem("mfa")
});
const networksApi = new NetworksApi(configuration);
let networks;
try {
networks = await networksApi.networksList();
} catch (e) {
let resp_json = await e.response.json();
if (resp_json.errors[0].code == "ERR_NO_ORG") {
window.location.href = "/networkcreate";
return;
} else {
isError = true;
error = $t("networkcreate.error.generic", {values:{err:resp_json.errors[0].code}});
loading = false;
return;
}
}
console.log(networks);
if (networks.data?.length == 0) {
window.location.href = '/networkcreate';
return;
}
const rolesApi = new RolesApi(configuration);
roles = await rolesApi.rolesList();
loading = false;
});
let roleName;
let roleDescription;
enum RuleProtocol {
ANY = 0,
TCP = 1,
UDP = 2,
ICMP = 3
}
function protoToStringName(proto: RuleProtocol): string {
if (proto == RuleProtocol.ANY) {
return $t("roles.add.any");
} else if (proto == RuleProtocol.TCP) {
return "TCP";
} else if (proto == RuleProtocol.UDP) {
return "UDP";
} else if (proto == RuleProtocol.ICMP) {
return "ICMP";
}
}
interface Rule {
description: string,
protocol: RuleProtocol,
portRange: null | number | [number, number]
allowedRole: string | null
}
function prettyPortRange(range: null | number | [number, number]): string {
if (range === null) {
return $t("roles.add.any");
} else if (typeof range === 'number') {
return String(range);
} else {
return String(range[0]) + "-" + String(range[1])
}
}
function findRole(role: string): string {
if (role === null) {
return $t("roles.add.any");
}
for (let i = 0; i < roles.data.length; i++) {
if (roles.data[i].id == role) {
return roles.data[i].name;
}
}
return role;
}
let rules: Rule[] = [
{
description: "Allow pings from other hosts",
protocol: RuleProtocol.ICMP,
portRange: null,
allowedRole: null
}
];
function removeRule(rule: Rule) {
let newRules: Rule[] = [];
for (let i = 0; i < rules.length; i++) {
if (rules[i] != rule) {
newRules.push(rules[i]);
}
}
rules = newRules;
}
let isEditingRule = false;
let editingRuleDesc = "";
let editingRuleProtocol = RuleProtocol.ANY;
let editingRulePortRange = "any";
let editingRuleAllowedRole = "any";
let editingExistingRule = false;
let editingExistingTheRule = null;
$: allowClickingOtherStuff = isEditingRule;
function editRule(rule: Rule) {
if (isEditingRule) return;
isEditingRule = true;
editingRuleDesc = rule.description;
editingRuleProtocol = rule.protocol;
editingRulePortRange = prettyPortRange(rule.portRange);
editingRuleAllowedRole = (rule.allowedRole === null) ? "any" : rule.allowedRole;
editingExistingRule = true;
editingExistingTheRule = rule;
}
function addRule() {
if (isEditingRule) return;
isEditingRule = true;
editingRuleDesc = "";
editingRuleProtocol = RuleProtocol.ANY;
editingRulePortRange = "any";
editingRuleAllowedRole = "any";
editingExistingRule = false;
editingExistingTheRule = null;
}
function finishEdit() {
if (!isEditingRule) return;
// parse port range
let portRange;
if (editingRulePortRange == "any" || editingRulePortRange == "Any") {
portRange = null;
} else {
let split = editingRulePortRange.split("-");
if (split.length == 1) {
portRange = parseInt(split[0]);
} else {
portRange = [parseInt(split[0]), parseInt(split[1])];
}
}
rules.push({
allowedRole: editingRuleAllowedRole,
description: editingRuleDesc,
portRange: portRange,
protocol: editingRuleProtocol
});
if (editingExistingRule) {
removeRule(editingExistingTheRule);
}
isEditingRule = false;
editingRuleDesc = "";
editingRuleProtocol = RuleProtocol.ANY;
editingRulePortRange = "any";
editingRuleAllowedRole = "any";
editingExistingRule = false;
editingExistingTheRule = null;
}
function cancelEdit() {
if (!isEditingRule) return;
isEditingRule = false;
editingRuleDesc = "";
editingRuleProtocol = RuleProtocol.ANY;
editingRulePortRange = "any";
editingRuleAllowedRole = "any";
editingExistingRule = false;
editingExistingTheRule = null;
}
async function roleAdd() {
}
</script>
<svelte:head>
<title>{$t("common.title", {values: {title: $t("common.page.roles")}})}</title>
</svelte:head>
<LoadingWrapper isLoading={currentlyLoading} isError={isError} error={error}>
<AdminBar selected="roles" />
<form on:submit|preventDefault={roleAdd}>
<label for="roleName">{$t("roles.add.name")}</label>
<input bind:value={roleName} type="text" id="roleName" />
<label for="roleDesc">{$t("roles.add.desc")}</label>
<input bind:value={roleDescription} type="text" id="roleDesc" />
<h3>{$t("roles.add.rules")}</h3>
<p>{$t("roles.add.rulesexplainer")}</p>
<!-- firewall rules -->
<table>
<tr>
<th>{$t("roles.add.rulescols.description")}</th>
<th>{$t("roles.add.rulescols.protocol")}</th>
<th>{$t("roles.add.rulescols.portrange")}</th>
<th>{$t("roles.add.rulescols.allowedrole")}</th>
</tr>
{#each rules as rule}
<tr>
<td>{rule.description}</td>
<td>{protoToStringName(rule.protocol)}</td>
<td>{prettyPortRange(rule.portRange)}</td>
<td>{findRole(rule.allowedRole)}</td>
<button disabled="{allowClickingOtherStuff}" on:click={() => {editRule(rule)}}>{$t("roles.add.ruleedit")}</button>
<button disabled="{allowClickingOtherStuff}" on:click={() => {removeRule(rule)}}>{$t("roles.add.ruleremove")}</button>
</tr>
{/each}
</table>
<button disabled="{allowClickingOtherStuff}" on:click={() => {addRule()}}>{$t("roles.add.rulesadd")}</button>
{#if isEditingRule}
<form on:submit|preventDefault>
<label for="ruleProtocol">{$t("roles.add.editrule.protocol")}</label>
<select id="ruleProtocol" bind:value="{editingRuleProtocol}">
<option value="{RuleProtocol.ANY}">{$t("roles.add.any")}</option>
<option value="{RuleProtocol.TCP}">TCP</option>
<option value="{RuleProtocol.UDP}">UDP</option>
<option value="{RuleProtocol.ICMP}">ICMP</option>
</select>
<label for="rulePortRange">{$t("roles.add.editrule.range")}</label>
<input type="text" bind:value="{editingRulePortRange}" id="rulePortRange"/>
<label for="ruleAllowedRole">{$t("roles.add.editrule.role")}</label>
<select id="ruleAllowedRole" bind:value="{editingRuleAllowedRole}">
<option value="any">{$t("roles.add.any")}</option>
{#each roles.data as role}
<option value="{role.id}">{role.name}</option>
{/each}
</select>
<label for="ruleDescription">{$t("roles.add.editrule.desc")}</label>
<input type="text" bind:value="{editingRuleDesc}" id="ruleDescription"/>
<button on:click|preventDefault={finishEdit}>{editingExistingRule ? $t("roles.add.editrule.edit") : $t("roles.add.editrule.add")}</button>
<button on:click|preventDefault={cancelEdit}>{$t("roles.add.editrule.cancel")}</button>
</form>
{/if}
<br/>
<button disabled="{allowClickingOtherStuff}">{$t("roles.add.button")}</button>
<button disabled="{allowClickingOtherStuff}" on:click|preventDefault={() => {window.location.href = "/roles";}}>{$t("roles.add.cancel")}</button>
</form>
</LoadingWrapper>

View File

@ -2,7 +2,7 @@
import {isLoading, t} from "svelte-i18n";
import LoadingWrapper from "$components/LoadingWrapper.svelte";
import {onMount} from "svelte";
import {AuthResult, authSession, isAuthedSession, signup} from "$lib/auth.ts";
import {APIResult, authSession, isAuthedSession, signup} from "$lib/auth.ts";
import type {APIError} from "$lib/auth.ts";
import {Logger, logSetup} from "$lib/logger";
@ -16,7 +16,7 @@
onMount(async () => {
let session_load_info = await isAuthedSession();
if (session_load_info[0] != AuthResult.Failed) {
if (session_load_info[0] != APIResult.Failed) {
logger.error(`session load success, the user is already logged in`);
window.location.href = '/2fa';
return;
@ -45,7 +45,7 @@
let auth_result = await signup(email);
if (auth_result[0] === AuthResult.Failed) {
if (auth_result[0] === APIResult.Failed) {
hasErrForm = true;

View File

@ -30,7 +30,6 @@ use trifid_pki::cert::{
pub struct CodegenRequiredInfo {
pub host: host::Model,
pub host_static_addresses: HashMap<String, Vec<SocketAddrV4>>,
pub host_config_overrides: Vec<host_config_override::Model>,
pub network: network::Model,
pub organization: organization::Model,
pub dh_pubkey: Vec<u8>,
@ -40,6 +39,7 @@ pub struct CodegenRequiredInfo {
pub lighthouse_ips: Vec<Ipv4Addr>,
pub blocked_hosts: Vec<String>,
pub firewall_rules: Vec<NebulaConfigFirewallRule>,
pub config_overrides: Vec<(String, String)>
}
pub async fn generate_config(
@ -196,6 +196,28 @@ pub async fn generate_config(
local_range: None,
};
// Merge with config overrides and re-parse
let config_str = serde_yaml::to_string(&nebula_config)?;
let mut value: serde_yaml::Value = serde_yaml::from_str(&config_str)?;
for (key, kv_value) in &info.config_overrides {
// split up the key
// a.b.c.d = ['a']['b']['c']['d'] = value
let key_split = key.split('.');
let mut current_val = &mut value;
for key_iter in key_split {
current_val = current_val.get_mut(key_iter).ok_or("Invalid key-value override")?;
}
*current_val = serde_yaml::from_str(kv_value)?;
}
let config_str_merged = serde_yaml::to_string(&value)?;
let nebula_config = serde_yaml::from_str(&config_str_merged)?;
Ok((nebula_config, cert))
}
@ -315,9 +337,15 @@ pub async fn collect_info<'a>(
let best_ca = best_ca.unwrap();
// pull our host's config overrides
let config_overrides = host_config_overrides.iter().map(|u| {
(u.key.clone(), u.value.clone())
}).collect();
// pull our role's firewall rules
let firewall_rules = trifid_api_entities::entity::firewall_rule::Entity::find()
.filter(firewall_rule::Column::Role.eq(&host.id))
.filter(firewall_rule::Column::Role.eq(&host.role))
.all(&db.conn)
.await?;
let firewall_rules = firewall_rules
@ -349,7 +377,6 @@ pub async fn collect_info<'a>(
Ok(CodegenRequiredInfo {
host,
host_static_addresses: host_x_static_addresses,
host_config_overrides,
network,
organization: org,
dh_pubkey: dh_pubkey.to_vec(),
@ -359,5 +386,6 @@ pub async fn collect_info<'a>(
lighthouse_ips: lighthouses,
blocked_hosts,
firewall_rules,
config_overrides
})
}