diff --git a/tfweb/src/components/org/OrgWrapper.svelte b/tfweb/src/components/org/OrgWrapper.svelte new file mode 100644 index 0000000..36ff390 --- /dev/null +++ b/tfweb/src/components/org/OrgWrapper.svelte @@ -0,0 +1,44 @@ + + +
+
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/tfweb/src/lib/i18n/en.json b/tfweb/src/lib/i18n/en.json index 143e191..4dd8574 100644 --- a/tfweb/src/lib/i18n/en.json +++ b/tfweb/src/lib/i18n/en.json @@ -106,5 +106,48 @@ "loadownedorg": "Unable to load organizations", "this user does not have permission to access this org": "You do not have permission to access one of the orgs associated with your account. Your access may have been revoked. Please reload the page." } + }, + + "org": { + "apierror": { + "notyourorg": "Access denied", + "notyourorgexplainer": "You do not have permission to manage this org. Ask the owner of this org to add you, or go back to the homepage.", + "loadroles": "Unable to load roles", + "TypeError: NetworkError when attempting to fetch resource": "Unable to contact the server. Please try again later", + "Unable to create default role firewall rules": "Unable to create the default role. Please try again later." + }, + + "nohosts": "You don't have any hosts set up", + "nohostsdesc": "Create and enroll a host to begin using it in your network.", + "nohostsdesc2": "Be sure to also create and enroll a lighthouse, or your hosts wont be able to communicate.", + "nohosts_cta": "Add host", + + "nolighthouses": "You don't have any lighthouses set up", + "nolighthousesdesc": "Lighthouses keep track of potential routes to overlay network hosts, and they assist in establishing connections between hosts.", + "nolighthouses_list_item1": "Each network needs at least one lighthouse.", + "nolighthouses_list_item2": "If you plan to access hosts over the Internet, at least one lighthouse will need a static, public IPv4 address with its firewall configured to allow inbound udp traffic on a specific port.", + "nolighthouses_list_item3": "A modestly-sized cloud instance should be sufficient for most users.", + "nolighthouses_list_item4": "Lighthouses are also hosts - they will have an IP and you will be able to access them over the network.", + "nolighthouses_cta": "Add lighthouse", + + "norelays": "Adding a relay is recommended", + "norelays_list_item1": "Relays ensure connectivity for scenarios when direct connections fail.", + "norelays_list_item2": "Without a relay, some devices may be unable to communicate.", + "norelays_list_item3": "Your lighthouses also act as relays.", + "norelays_lighthouse": "You currently have {} lighthouses acting as relays", + "norelays_cta": "Add relay", + + "roles_title": "Roles", + "roles_bar": "Roles control how hosts, lighthouses, and relays communicate through firewall rules.", + "roles_cta": "Add", + + "guide": "Read the guide →", + + "noroles": "Wow, such empty", + "noroles_cta": "You don't have any roles. Consider creating one with the button above.", + + "roleaddprompt": "What's the name of your new role?", + "roleaddpromptdesc": "What's the description of your new role?", + "roleadd_cta": "Create role" } } \ No newline at end of file diff --git a/tfweb/src/routes/auth/mfasetup/+page.svelte b/tfweb/src/routes/auth/mfasetup/+page.svelte index 070f83a..90f183a 100644 --- a/tfweb/src/routes/auth/mfasetup/+page.svelte +++ b/tfweb/src/routes/auth/mfasetup/+page.svelte @@ -11,7 +11,7 @@ import {finishTOTPSetup} from "$lib/totp"; import {setCookie} from "$lib/cookie"; - let logger = new Logger("auth/mfa/+page.svelte"); + let logger = new Logger("auth/mfasetup/+page.svelte"); logSetup(); let api_token = ""; diff --git a/tfweb/src/routes/org/[orgid]/+page.svelte b/tfweb/src/routes/org/[orgid]/+page.svelte new file mode 100644 index 0000000..a975396 --- /dev/null +++ b/tfweb/src/routes/org/[orgid]/+page.svelte @@ -0,0 +1,86 @@ + + +{#if fullPageError} + +{:else} + + + + + +{/if} \ No newline at end of file diff --git a/tfweb/src/routes/org/[orgid]/hosts/+page.svelte b/tfweb/src/routes/org/[orgid]/hosts/+page.svelte new file mode 100644 index 0000000..7604ce0 --- /dev/null +++ b/tfweb/src/routes/org/[orgid]/hosts/+page.svelte @@ -0,0 +1,86 @@ + + +{#if fullPageError} + +{:else} + + + + + + +{/if} \ No newline at end of file diff --git a/tfweb/src/routes/org/[orgid]/lighthouses/+page.svelte b/tfweb/src/routes/org/[orgid]/lighthouses/+page.svelte new file mode 100644 index 0000000..238ac4c --- /dev/null +++ b/tfweb/src/routes/org/[orgid]/lighthouses/+page.svelte @@ -0,0 +1,86 @@ + + +{#if fullPageError} + +{:else} + + + + + + +{/if} \ No newline at end of file diff --git a/tfweb/src/routes/org/[orgid]/relays/+page.svelte b/tfweb/src/routes/org/[orgid]/relays/+page.svelte new file mode 100644 index 0000000..abcc5b6 --- /dev/null +++ b/tfweb/src/routes/org/[orgid]/relays/+page.svelte @@ -0,0 +1,86 @@ + + +{#if fullPageError} + +{:else} + + + + + + +{/if} \ No newline at end of file diff --git a/tfweb/src/routes/org/[orgid]/roles/+page.svelte b/tfweb/src/routes/org/[orgid]/roles/+page.svelte new file mode 100644 index 0000000..2c12083 --- /dev/null +++ b/tfweb/src/routes/org/[orgid]/roles/+page.svelte @@ -0,0 +1,169 @@ + + +{#if fullPageError} + +{:else} + + + + +
+ +

{t('org.roles_title')}

+ + + +
+ {t('org.roles_bar')} + {t('org.guide')} +
+ + + + {#if roles.data.length === 0} +
+

{t('org.noroles')}

+

{t('org.noroles_cta')}

+
+ {:else} +
+ + + + + + + + + + {#each roles.data as role} + + + + + + + + {/each} +
+ Name + ID + + Rules + + Description +
{role.name}{role.id}{role.firewall_rules.length}{role.description} + +
+
+ {/if} +
+
+{/if} \ No newline at end of file diff --git a/tfweb/src/routes/org/[orgid]/roles/add/+page.svelte b/tfweb/src/routes/org/[orgid]/roles/add/+page.svelte new file mode 100644 index 0000000..6495d08 --- /dev/null +++ b/tfweb/src/routes/org/[orgid]/roles/add/+page.svelte @@ -0,0 +1,144 @@ + + +{#if fullPageError} + +{:else} +
+
+ + +
+
+ + +
+ +
+ + +
+ + + +
+
+
+{/if} \ No newline at end of file diff --git a/tfweb/src/routes/org/new/+page.svelte b/tfweb/src/routes/org/new/+page.svelte index ac005b7..d9690d9 100644 --- a/tfweb/src/routes/org/new/+page.svelte +++ b/tfweb/src/routes/org/new/+page.svelte @@ -11,7 +11,7 @@ import {API_ROOT} from "$lib/config"; import {fetch_timeout} from "$lib/util"; - let logger = new Logger("admin/+page.svelte"); + let logger = new Logger("org/new/+page.svelte"); logSetup(); let api_token = ""; @@ -111,15 +111,18 @@ fullPageErrorSubtitle = t('neworg.apierror.invaliddata'); return; } - let err = JSON.parse(await resp.text()).errors[0].message; - logger.error(`${await resp.json().errors[0]}`); + let text = await resp.text(); + console.log(text); + let err = JSON.parse(text).errors[0].message; + logger.error(`${err}`); fullPageError = true; fullPageErrorTitle = t('neworg.apierror.orgcreate'); fullPageErrorSubtitle = t('neworg.apierror.' + err); return; } - let resp_objectified = JSON.parse(await resp.text()); - console.log(JSON.stringify(resp_objectified)); + let text = await resp.text(); + console.log(text); + let resp_objectified = JSON.parse(text); created_org_id = resp_objectified.org_id; logger.info("Able to create organization with id " + created_org_id); logger.info("Org create success! Redirecting to manage page"); diff --git a/tfweb/tsconfig.json b/tfweb/tsconfig.json index f5d63d4..b050f46 100644 --- a/tfweb/tsconfig.json +++ b/tfweb/tsconfig.json @@ -15,6 +15,12 @@ ], "$lib/*": [ "./src/lib/*" + ], + "$components": [ + "./src/components" + ], + "$components/*": [ + "./src/components/*" ] } } diff --git a/trifid-api/migrations/20230302220748_add_roles.sql b/trifid-api/migrations/20230302220748_add_roles.sql new file mode 100644 index 0000000..54c1b55 --- /dev/null +++ b/trifid-api/migrations/20230302220748_add_roles.sql @@ -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 . + +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 +); \ No newline at end of file diff --git a/trifid-api/migrations/20230302220808_add_firewall_rules.sql b/trifid-api/migrations/20230302220808_add_firewall_rules.sql new file mode 100644 index 0000000..69961af --- /dev/null +++ b/trifid-api/migrations/20230302220808_add_firewall_rules.sql @@ -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 . + +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 +); \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 519617e..847c3b1 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -41,6 +41,7 @@ pub mod auth; pub mod crypto; pub mod org; pub mod kv; +pub mod role; static MIGRATOR: Migrator = sqlx::migrate!(); @@ -183,7 +184,11 @@ async fn main() -> Result<(), Box> { crate::routes::v1::user::options, crate::routes::v1::organization::createorgoptions, crate::routes::v1::ca::get_cas_for_org, - crate::routes::v1::ca::options + crate::routes::v1::ca::options, + crate::routes::v1::roles::get_roles, + crate::routes::v1::roles::options, + crate::routes::v1::roles::options_roleadd, + crate::routes::v1::roles::role_add ]) .register("/", catchers![ crate::routes::handler_400, diff --git a/trifid-api/src/role.rs b/trifid-api/src/role.rs new file mode 100644 index 0000000..5660964 --- /dev/null +++ b/trifid-api/src/role.rs @@ -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 . + +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 +} + +pub async fn get_role(role_id: i32, db: &PgPool) -> Result, Box> { + 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, Box> { + Ok(sqlx::query!("SELECT id FROM roles WHERE org = $1", org_id).fetch_all(db).await?.iter().map(|r| r.id).collect()) +} \ No newline at end of file diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index b23a49e..7c0037d 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -21,4 +21,5 @@ pub mod totp_authenticators; pub mod verify_totp_authenticator; pub mod organization; pub mod user; -pub mod ca; \ No newline at end of file +pub mod ca; +pub mod roles; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/organization.rs b/trifid-api/src/routes/v1/organization.rs index ade370e..2e2519b 100644 --- a/trifid-api/src/routes/v1/organization.rs +++ b/trifid-api/src/routes/v1/organization.rs @@ -162,6 +162,23 @@ pub async fn create_org(req: Json, user: TOTPAuthenticatedUserI let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?; + // last step: create a default role to allow pings from all hosts + let role_id = match sqlx::query!("INSERT INTO roles (org, name, description) VALUES ($1, 'Default', 'Allow pings from other hosts. Default role for new hosts.') RETURNING id", result.id).fetch_one(db.inner()).await { + Ok(r) => r.id, + Err(e) => { + error!("[tfapi] dberror: {}", e); + return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role"))); + } + }; + + match sqlx::query!("INSERT INTO roles_firewall_rules (role, protocol, port_range_start, port_range_end, allow_from, description) VALUES ($1, 3, 1, 1, -1, 'Allow pings from anyone on the network')", role_id).execute(db.inner()).await { + Ok(_) => {}, + Err(e) => { + error!("[tfapi] dberror: {} inserting on roleid {}", e, role_id); + return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role firewall rules"))); + } + } + Ok((ContentType::JSON, Json( OrginfoStruct { org_id: result.id, diff --git a/trifid-api/src/routes/v1/roles.rs b/trifid-api/src/routes/v1/roles.rs new file mode 100644 index 0000000..538f65a --- /dev/null +++ b/trifid-api/src/routes/v1/roles.rs @@ -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 . + +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 +} + +#[get("/v1/org//roles")] +pub async fn get_roles(org: i32, user: TOTPAuthenticatedUserInfo, db: &State) -> Result<(ContentType, Json), (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//role", data = "")] +pub async fn role_add(req: Json, org: i32, user: TOTPAuthenticatedUserInfo, db: &State) -> 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())) +} \ No newline at end of file