From 597ab081f6ff5bce2af3d576f53c27318a0c12f5 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Wed, 1 Mar 2023 13:43:43 -0500 Subject: [PATCH 1/5] basic org creation --- tfweb/src/lib/i18n/en.json | 6 ++++- tfweb/src/lib/util.ts | 2 +- tfweb/src/routes/org/new/+page.svelte | 34 ++++++++++++++++++++++++--- tfweb/tsconfig.json | 10 +++++++- 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/tfweb/src/lib/i18n/en.json b/tfweb/src/lib/i18n/en.json index 74cda5c..7b3d1fd 100644 --- a/tfweb/src/lib/i18n/en.json +++ b/tfweb/src/lib/i18n/en.json @@ -92,6 +92,10 @@ "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)", "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", + "TypeError: NetworkError when attempting to fetch resource": "Unable to contact the backend. Please try again later." + } } } \ No newline at end of file diff --git a/tfweb/src/lib/util.ts b/tfweb/src/lib/util.ts index 2a17aba..b78c055 100644 --- a/tfweb/src/lib/util.ts +++ b/tfweb/src/lib/util.ts @@ -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 // @ts-ignore const { timeout = 8000 } = options; diff --git a/tfweb/src/routes/org/new/+page.svelte b/tfweb/src/routes/org/new/+page.svelte index a3bf266..137bca5 100644 --- a/tfweb/src/routes/org/new/+page.svelte +++ b/tfweb/src/routes/org/new/+page.svelte @@ -8,6 +8,8 @@ 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"; let logger = new Logger("admin/+page.svelte"); logSetup(); @@ -73,8 +75,34 @@ let showAdditionalConstraints = false; - function newOrg() { - + async function doCreateFlow() { + // STEP ONE: Create the org + logger.info("Creating base organization"); + let created_org_id; + try { + let resp = await fetch_timeout(`${API_ROOT}/v1/org`, { + 'method': 'POST', + 'headers': { + 'Authorization': 'Bearer ' + api_token + } + }); + if (resp.code !== 200) { + let err = await resp.json().errors[0].message; + logger.error(`${await resp.json().errors[0]}`); + fullPageError = true; + fullPageErrorTitle = t('neworg.apierror.orgcreate'); + fullPageErrorSubtitle = t('neworg.apierror.' + err); + return; + } + created_org_id = resp.json().org_id; + logger.info("Able to create base organization with id " + created_org_id); + } catch (e) { + logger.error(`${e}`); + fullPageError = true; + fullPageErrorTitle = t('neworg.apierror.orgcreate'); + fullPageErrorSubtitle = t('neworg.apierror.' + `${e}`.replaceAll('.', '')); + return; + } } @@ -90,7 +118,7 @@

{t('neworg.cahelp')}

-
+
Date: Wed, 1 Mar 2023 14:35:13 -0500 Subject: [PATCH 2/5] add more reqs - WIP WILL NOT BUILD --- Cargo.lock | 4 ++ trifid-api/Cargo.toml | 3 +- trifid-api/src/main.rs | 3 +- trifid-api/src/routes/v1/ca.rs | 89 ++++++++++++++++++++++++ trifid-api/src/routes/v1/mod.rs | 3 +- trifid-api/src/routes/v1/organization.rs | 2 + 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 trifid-api/src/routes/v1/ca.rs diff --git a/Cargo.lock b/Cargo.lock index 1ede611..63782ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1022,6 +1022,9 @@ name = "ipnet" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +dependencies = [ + "serde", +] [[package]] name = "itertools" @@ -2403,6 +2406,7 @@ dependencies = [ "chrono", "dotenvy", "hex", + "ipnet", "log", "paste", "rand", diff --git a/trifid-api/Cargo.toml b/trifid-api/Cargo.toml index cf1e56d..d63dc22 100644 --- a/trifid-api/Cargo.toml +++ b/trifid-api/Cargo.toml @@ -24,4 +24,5 @@ aes-gcm = "0.10.1" hex = "0.4.3" rand = "0.8.5" trifid-pki = { version = "0.1.3", path = "../trifid-pki" } -sha2 = "0.10.6" \ No newline at end of file +sha2 = "0.10.6" +ipnet = { version = "2.7.1", features = ["serde"] } diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 92a9ca5..125597a 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -180,7 +180,8 @@ async fn main() -> Result<(), Box> { crate::routes::v1::organization::orglist_req, crate::routes::v1::organization::create_org, crate::routes::v1::user::get_user, - crate::routes::v1::user::options + crate::routes::v1::user::options, + crate::routes::v1::organization::createorgoptions, ]) .register("/", catchers![ crate::routes::handler_400, diff --git a/trifid-api/src/routes/v1/ca.rs b/trifid-api/src/routes/v1/ca.rs new file mode 100644 index 0000000..6a67c4c --- /dev/null +++ b/trifid-api/src/routes/v1/ca.rs @@ -0,0 +1,89 @@ +// 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 ipnet::Ipv4Net; +use rocket::{post, options, get, State}; +use rocket::http::{ContentType, Status}; +use rocket::serde::json::Json; +use sqlx::PgPool; +use crate::config::TFConfig; +use serde::{Serialize, Deserialize}; +use crate::auth::TOTPAuthenticatedUserInfo; +use crate::org::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, + pub blocklisted_certs: Vec +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CA { + pub fingerprint: String, + pub cert: String +} + +#[get("/v1/org//ca")] +pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State) -> Result<(ContentType, Json), (Status, String)> { + 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, + }))) +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CreateCARequest { + pub ip_ranges: Vec, + pub subnet_ranges: Vec, + pub groups: Vec +} + +#[post("/v1/org//ca", data = "")] +pub async fn create_signing_ca_req(id: i32, data: Json, user: TOTPAuthenticatedUserInfo, config: &State, db: &State) { + +} \ 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 cae8fbd..b23a49e 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -20,4 +20,5 @@ pub mod signup; pub mod totp_authenticators; pub mod verify_totp_authenticator; pub mod organization; -pub mod user; \ No newline at end of file +pub mod user; +pub mod ca; \ 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 013c80a..2c94363 100644 --- a/trifid-api/src/routes/v1/organization.rs +++ b/trifid-api/src/routes/v1/organization.rs @@ -32,6 +32,8 @@ pub fn options() -> &'static str { pub fn orgidoptions(_id: i32) -> &'static str { "" } +#[options("/v1/org")] +pub fn createorgoptions() -> &'static str {""} #[derive(Serialize, Deserialize)] #[serde(crate = "rocket::serde")] From 677f60fc2b704d2d101a341ab451e9ea52f010b0 Mon Sep 17 00:00:00 2001 From: core Date: Wed, 1 Mar 2023 22:06:07 -0500 Subject: [PATCH 3/5] cert signing on org creation --- tfweb/src/lib/i18n/en.json | 11 +++- tfweb/src/routes/org/new/+page.svelte | 40 ++++++++++++--- trifid-api/config.example.toml | 7 +++ trifid-api/config.toml | 1 + trifid-api/src/config.rs | 9 ++-- trifid-api/src/main.rs | 2 + trifid-api/src/org.rs | 6 ++- trifid-api/src/routes/v1/ca.rs | 24 ++++----- trifid-api/src/routes/v1/organization.rs | 64 +++++++++++++++++++++--- trifid-api/src/tokens.rs | 2 +- 10 files changed, 133 insertions(+), 33 deletions(-) diff --git a/tfweb/src/lib/i18n/en.json b/tfweb/src/lib/i18n/en.json index 7b3d1fd..143e191 100644 --- a/tfweb/src/lib/i18n/en.json +++ b/tfweb/src/lib/i18n/en.json @@ -95,7 +95,16 @@ "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", - "TypeError: NetworkError when attempting to fetch resource": "Unable to contact the backend. Please try again later." + "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." } } } \ 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 137bca5..ac005b7 100644 --- a/tfweb/src/routes/org/new/+page.svelte +++ b/tfweb/src/routes/org/new/+page.svelte @@ -75,27 +75,55 @@ let showAdditionalConstraints = false; + function handle(listfield: string): string[] { + if (listfield == "") { + return [] + } else { + return listfield.split(',') + } + } + async function doCreateFlow() { // STEP ONE: Create the org - logger.info("Creating base organization"); + 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.code !== 200) { - let err = await resp.json().errors[0].message; + 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 err = JSON.parse(await resp.text()).errors[0].message; logger.error(`${await resp.json().errors[0]}`); fullPageError = true; fullPageErrorTitle = t('neworg.apierror.orgcreate'); fullPageErrorSubtitle = t('neworg.apierror.' + err); return; } - created_org_id = resp.json().org_id; - logger.info("Able to create base organization with id " + created_org_id); + let resp_objectified = JSON.parse(await resp.text()); + console.log(JSON.stringify(resp_objectified)); + 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; diff --git a/trifid-api/config.example.toml b/trifid-api/config.example.toml index b60e4c7..921f189 100644 --- a/trifid-api/config.example.toml +++ b/trifid-api/config.example.toml @@ -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. # ------- WARNING ------- 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 \ No newline at end of file diff --git a/trifid-api/config.toml b/trifid-api/config.toml index 02d1ef3..f206eff 100644 --- a/trifid-api/config.toml +++ b/trifid-api/config.toml @@ -6,3 +6,4 @@ magic_links_valid_for = 86400 session_tokens_valid_for = 86400 totp_verification_valid_for = 3600 data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2" +ca_certs_valid_for = 31536000 \ No newline at end of file diff --git a/trifid-api/src/config.rs b/trifid-api/src/config.rs index 0697317..954a4f7 100644 --- a/trifid-api/src/config.rs +++ b/trifid-api/src/config.rs @@ -23,8 +23,9 @@ pub struct TFConfig { pub db_url: String, pub base: Url, pub web_root: Url, - pub magic_links_valid_for: i64, - pub session_tokens_valid_for: i64, - pub totp_verification_valid_for: i64, - pub data_key: String + pub magic_links_valid_for: u64, + pub session_tokens_valid_for: u64, + pub totp_verification_valid_for: u64, + pub data_key: String, + pub ca_certs_valid_for: u64 } \ No newline at end of file diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 125597a..519617e 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -182,6 +182,8 @@ async fn main() -> Result<(), Box> { crate::routes::v1::user::get_user, crate::routes::v1::user::options, crate::routes::v1::organization::createorgoptions, + crate::routes::v1::ca::get_cas_for_org, + crate::routes::v1::ca::options ]) .register("/", catchers![ crate::routes::handler_400, diff --git a/trifid-api/src/org.rs b/trifid-api/src/org.rs index 83c754e..dc7d723 100644 --- a/trifid-api/src/org.rs +++ b/trifid-api/src/org.rs @@ -15,11 +15,13 @@ // along with this program. If not, see . use std::error::Error; +use log::info; use rocket::form::validate::Contains; use sqlx::PgPool; use trifid_pki::ca::NebulaCAPool; pub async fn get_org_by_owner_id(user: i32, db: &PgPool) -> Result, Box> { + 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, Box } pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result> { - 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> { diff --git a/trifid-api/src/routes/v1/ca.rs b/trifid-api/src/routes/v1/ca.rs index 6a67c4c..02e3c1c 100644 --- a/trifid-api/src/routes/v1/ca.rs +++ b/trifid-api/src/routes/v1/ca.rs @@ -22,7 +22,7 @@ use sqlx::PgPool; use crate::config::TFConfig; use serde::{Serialize, Deserialize}; use crate::auth::TOTPAuthenticatedUserInfo; -use crate::org::get_org_ca_pool; +use crate::org::{get_associated_orgs, get_org_ca_pool}; #[options("/v1/org/<_id>/ca")] pub fn options(_id: i32) -> &'static str { @@ -45,6 +45,15 @@ pub struct CA { #[get("/v1/org//ca")] pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State) -> Result<(ContentType, Json), (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) => { @@ -73,17 +82,4 @@ pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &Stat trusted_cas, blocklisted_certs: ca_pool.cert_blocklist, }))) -} - -#[derive(Serialize, Deserialize)] -#[serde(crate = "rocket::serde")] -pub struct CreateCARequest { - pub ip_ranges: Vec, - pub subnet_ranges: Vec, - pub groups: Vec -} - -#[post("/v1/org//ca", data = "")] -pub async fn create_signing_ca_req(id: i32, data: Json, user: TOTPAuthenticatedUserInfo, config: &State, db: &State) { - } \ 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 2c94363..ade370e 100644 --- a/trifid-api/src/routes/v1/organization.rs +++ b/trifid-api/src/routes/v1/organization.rs @@ -14,11 +14,16 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -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::serde::json::Json; use serde::{Serialize, Deserialize}; 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::config::TFConfig; use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config}; @@ -66,7 +71,7 @@ pub struct OrginfoStruct { #[get("/v1/org/")] pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State) -> Result<(ContentType, Json), (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"))); } @@ -83,12 +88,59 @@ pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State ))) } -#[post("/v1/org")] -pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State, config: &State) -> Result<(ContentType, Json), (Status, String)> { +#[derive(Serialize, Deserialize)] +#[serde(crate = "rocket::serde")] +pub struct CreateCARequest { + pub ip_ranges: Vec, + pub subnet_ranges: Vec, + pub groups: Vec +} + +#[post("/v1/org", data = "")] +pub async fn create_org(req: Json, user: TOTPAuthenticatedUserInfo, db: &State, config: &State) -> Result<(ContentType, Json), (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() { 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 let iv = generate_random_iv(); let iv_hex = hex::encode(iv); @@ -100,13 +152,13 @@ pub async fn create_org(user: TOTPAuthenticatedUserInfo, db: &State, con 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), Err(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)))?; diff --git a/trifid-api/src/tokens.rs b/trifid-api/src/tokens.rs index a9b0d8e..8590cdf 100644 --- a/trifid-api/src/tokens.rs +++ b/trifid-api/src/tokens.rs @@ -74,7 +74,7 @@ pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) -> 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)) } From cde5b73907c695256ad6de9c5909147863da509a Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Thu, 2 Mar 2023 09:40:24 -0500 Subject: [PATCH 4/5] cargo-fix --- trifid-api/src/org.rs | 2 +- trifid-api/src/routes/v1/ca.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/trifid-api/src/org.rs b/trifid-api/src/org.rs index dc7d723..501b14a 100644 --- a/trifid-api/src/org.rs +++ b/trifid-api/src/org.rs @@ -15,7 +15,7 @@ // along with this program. If not, see . use std::error::Error; -use log::info; + use rocket::form::validate::Contains; use sqlx::PgPool; use trifid_pki::ca::NebulaCAPool; diff --git a/trifid-api/src/routes/v1/ca.rs b/trifid-api/src/routes/v1/ca.rs index 02e3c1c..fe7b442 100644 --- a/trifid-api/src/routes/v1/ca.rs +++ b/trifid-api/src/routes/v1/ca.rs @@ -14,12 +14,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use ipnet::Ipv4Net; -use rocket::{post, options, get, State}; + +use rocket::{options, get, State}; use rocket::http::{ContentType, Status}; use rocket::serde::json::Json; use sqlx::PgPool; -use crate::config::TFConfig; + use serde::{Serialize, Deserialize}; use crate::auth::TOTPAuthenticatedUserInfo; use crate::org::{get_associated_orgs, get_org_ca_pool}; From 39978c3579d63d20e30ac663f19a8e92a2247379 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Thu, 2 Mar 2023 21:33:24 -0500 Subject: [PATCH 5/5] "some" wokr on roles (this is last major roadblock to milestone:feat-org-keys) --- tfweb/src/components/org/OrgWrapper.svelte | 44 +++++ tfweb/src/lib/i18n/en.json | 43 +++++ tfweb/src/routes/auth/mfasetup/+page.svelte | 2 +- tfweb/src/routes/org/[orgid]/+page.svelte | 86 +++++++++ .../src/routes/org/[orgid]/hosts/+page.svelte | 86 +++++++++ .../org/[orgid]/lighthouses/+page.svelte | 86 +++++++++ .../routes/org/[orgid]/relays/+page.svelte | 86 +++++++++ .../src/routes/org/[orgid]/roles/+page.svelte | 169 ++++++++++++++++++ .../routes/org/[orgid]/roles/add/+page.svelte | 144 +++++++++++++++ tfweb/src/routes/org/new/+page.svelte | 13 +- tfweb/tsconfig.json | 6 + .../migrations/20230302220748_add_roles.sql | 22 +++ .../20230302220808_add_firewall_rules.sql | 27 +++ trifid-api/src/main.rs | 7 +- trifid-api/src/role.rs | 98 ++++++++++ trifid-api/src/routes/v1/mod.rs | 3 +- trifid-api/src/routes/v1/organization.rs | 17 ++ trifid-api/src/routes/v1/roles.rs | 95 ++++++++++ 18 files changed, 1026 insertions(+), 8 deletions(-) create mode 100644 tfweb/src/components/org/OrgWrapper.svelte create mode 100644 tfweb/src/routes/org/[orgid]/+page.svelte create mode 100644 tfweb/src/routes/org/[orgid]/hosts/+page.svelte create mode 100644 tfweb/src/routes/org/[orgid]/lighthouses/+page.svelte create mode 100644 tfweb/src/routes/org/[orgid]/relays/+page.svelte create mode 100644 tfweb/src/routes/org/[orgid]/roles/+page.svelte create mode 100644 tfweb/src/routes/org/[orgid]/roles/add/+page.svelte create mode 100644 trifid-api/migrations/20230302220748_add_roles.sql create mode 100644 trifid-api/migrations/20230302220808_add_firewall_rules.sql create mode 100644 trifid-api/src/role.rs create mode 100644 trifid-api/src/routes/v1/roles.rs 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