restart trifid-api to do it better

This commit is contained in:
core 2023-04-02 12:08:36 -04:00
parent 9a5ffcd622
commit 7d2e370060
Signed by: core
GPG Key ID: FDBF740DADDCEECF
46 changed files with 877 additions and 4657 deletions

1479
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
[build]
rustc-wrapper = "sccache"

View File

@ -1 +1 @@
DATABASE_URL=postgres://postgres@localhost/trifidapi
DATABASE_URL=postgres://postgres@localhost/hotel

1541
trifid-api/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,23 +6,17 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rocket = { version = "0.5.0-rc.2", features = ["json"] }
base64 = "0.21.0"
log = "0.4.17"
sqlx = { version = "0.6", features = [ "runtime-tokio-native-tls" , "postgres" ] }
tokio = { version = "1", features = ["full"] }
toml = "0.7.1"
serde = "1.0.152"
dotenvy = "0.15.6"
paste = "1.0.11"
totp-rs = { version = "4.2.0", features = ["qr", "otpauth", "gen_secret"]}
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"]}
url = { version = "2.3.1", features = ["serde"] }
urlencoding = "2.1.2"
chrono = "0.4.23"
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"
ipnet = { version = "2.7.1", features = ["serde"] }
actix-web = "4" # Web framework
serde = { version = "1", features = ["derive"] } # Serialization and deserialization
once_cell = "1" # Config
toml = "0.7" # Config
log = "0.4" # Logging
simple_logger = "4" # Logging
sea-orm = { version = "^0", features = [ "sqlx-postgres", "runtime-actix-rustls", "macros" ]} # Database
rand = "0.8" # Misc.
hex = "0.4" # Misc.

View File

@ -1,20 +1,3 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// generated by `sqlx migrate build-script`
fn main() {
// trigger recompilation when a new migration is added
println!("cargo:rerun-if-changed=migrations");
println!("cargo:rerun-if-changed=migrations/");
}

View File

@ -1,81 +0,0 @@
##################################
# trifid-api example config file #
##################################
# trifid-api, an open source reimplementation of the Defined Networking nebula management server.
# Copyright (C) 2023 c0repwn3r
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https:#www.gnu.org/licenses/>.
# Please read this file in it's entirety to learn what options you do or don't need to change
# to get a functional trifid-api instance.
# What port should the API server listen on?
# e.g. 8000 would mean the server is reachable at localhost:8000.
# You probably don't need to change this.
listen_port = 8000
# What is the postgres connection url to connect to the database?
# Example: postgres://username:password@database_host/database_name
# You absolutely need to change this.
db_url = "postgres://postgres@localhost/trifidapi"
# What is the externally accessible URL of this instance?
# If you are running behind a reverse proxy, or a domain name, or similar,
# you need to set this to the URL that the web UI can make requests to.
# e.g. http://localhost:8000
# Reminder: this ip needs to be internet-accessible.
# You absolutely need to change this.
base = "http://localhost:8000"
# What is the externally accessible URL of the **web ui** for this instance?
# This URL will be used to generate magic links, and needs to be correct.
# You absolutely need to change this.
web_root = "http://localhost:5173"
# How long should magic links be valid for (in seconds)?
# You probably don't need to change this, 86400 (24 hours) is a sane default.
magic_links_valid_for = 86400
# How long should session tokens be valid for (in seconds)?
# This controls how long a user can go without requesting a new "magic link" to re-log-in.
# This is a completley independent timer than `totp_verification_valid_for` - the auth token can (and often will) expire
# while the session token remains completley valid.
# You probably don't need to change this, 86400 (24 hours) is a sane default.
session_tokens_valid_for = 86400
# How long should 2FA authentication be valid for (in seconds)?
# This controls how long a user can remain logged in without having to re-do the 2FA authentication process.
# This is a completley independent timer than `session_tokens_valid_for` - the session token can expire while the 2FA token
# remains completley valid.
# You probably don't need to change this, 3600 (1 hour) is a sane default.
totp_verification_valid_for = 3600
# The per-instance data encryption key to protect sensitive data in the instance.
# YOU ABSOLUTELY NEED TO CHANGE THIS. If you don't change anything else in this file, this should be the one thing you change.
# This should be a 32-byte hex value. Generate it with `openssl rand -hex 32`, or any other tool of your choice.
# If you get "InvalidLength" errors while trying to do anything involving organizations, that indicates that this
# value was improperly generated.
#
# ------- WARNING -------
# 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

View File

@ -1,9 +0,0 @@
listen_port = 8000
db_url = "postgres://postgres@localhost/trifidapi"
base = "http://localhost:8000"
web_root = "http://localhost:5173"
magic_links_valid_for = 86400
session_tokens_valid_for = 86400
totp_verification_valid_for = 3600
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
ca_certs_valid_for = 31536000

5
trifid-api/diesel.toml Normal file
View File

@ -0,0 +1,5 @@
[print_schema]
file = "src/schema.rs"
[migrations_directory]
dir = "migrations"

View File

@ -1,29 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE users (
id SERIAL NOT NULL PRIMARY KEY,
email VARCHAR(320) NOT NULL UNIQUE,
created_on INTEGER NOT NULL, -- Unix (seconds) timestamp of user creation
banned INTEGER NOT NULL, -- Is the user banned? 1=Yes 0=No
ban_reason VARCHAR(1024) NOT NULL, -- What is the reason for this user's ban?
totp_secret VARCHAR(128) NOT NULL,
totp_verified INTEGER NOT NULL,
totp_otpurl VARCHAR(3000) NOT NULL
);
CREATE INDEX idx_users_email ON users(email);

View File

@ -1,21 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE magic_links (
id VARCHAR(39) NOT NULL PRIMARY KEY UNIQUE,
user_id SERIAL NOT NULL REFERENCES users(id),
expires_on INTEGER NOT NULL -- Unix (seconds) timestamp of when this link expires
);

View File

@ -1,21 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE session_tokens (
id VARCHAR(39) NOT NULL PRIMARY KEY,
user_id SERIAL NOT NULL REFERENCES users(id),
expires_on INTEGER NOT NULL -- Unix (seconds) timestamp of when this session expires
);

View File

@ -1,21 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE auth_tokens (
id VARCHAR(39) NOT NULL PRIMARY KEY,
user_id SERIAL NOT NULL REFERENCES users(id),
session_token VARCHAR(39) NOT NULL REFERENCES session_tokens(id)
);

View File

@ -1,22 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE totp_create_tokens (
id VARCHAR(41) NOT NULL PRIMARY KEY,
expires_on INTEGER NOT NULL, -- The unix (seconds) timestamp of when this TOTP create token expires
totp_otpurl VARCHAR(3000) NOT NULL,
totp_secret VARCHAR(128) NOT NULL
);

View File

@ -1,24 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE organizations (
id SERIAL NOT NULL PRIMARY KEY,
owner SERIAL NOT NULL REFERENCES users(id),
ca_key VARCHAR(3072) NOT NULL, -- The hex-encoded ENCRYPTED (see below) concatenation of all CA keys on this org
ca_crt VARCHAR(3072) NOT NULL, -- The concatenation of all CA certificates on this org. This is passed directly to NebulaCAPool
iv VARCHAR(128) NOT NULL -- The 12-byte hex-encoded IV, used to encrypt ca_key with the instance AES key
);
CREATE INDEX idx_organizations_owner ON organizations(owner);

View File

@ -1,23 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE organization_authorized_users (
id SERIAL NOT NULL PRIMARY KEY,
user_id SERIAL NOT NULL REFERENCES users(id),
org_id SERIAL NOT NULL REFERENCES organizations(id)
);
CREATE INDEX idx_organization_authorized_users_user ON organization_authorized_users(user_id);
CREATE INDEX idx_organization_authorized_users_org ON organization_authorized_users(org_id);

View File

@ -1,20 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE cacheddata (
datakey VARCHAR(256) NOT NULL PRIMARY KEY,
datavalue VARCHAR(2048) NOT NULL
);

View File

@ -1,22 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE roles (
id SERIAL NOT NULL PRIMARY KEY,
org SERIAL NOT NULL REFERENCES organizations(id),
name VARCHAR(128) NOT NULL,
description VARCHAR(4096) NOT NULL
);

View File

@ -1,27 +0,0 @@
-- trifid-api, an open source reimplementation of the Defined Networking nebula management server.
-- Copyright (C) 2023 c0repwn3r
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program. If not, see <https:--www.gnu.org/licenses/>.
CREATE TABLE roles_firewall_rules (
id SERIAL NOT NULL PRIMARY KEY,
role SERIAL NOT NULL REFERENCES roles(id),
protocol INTEGER NOT NULL, -- 0: any 1: tcp 2: udp 3: icmp
port_range_start INTEGER NOT NULL, -- min: 1 max: 65535. Ignored if protocol==3
port_range_end INTEGER NOT NULL, -- min: 1 max: 65535, must be greater than or equal to port_range_start. Ignored if protocol==3
allow_from INTEGER NOT NULL, -- Allow traffic goverened by above rules from who?
-- -1: anybody
-- (a role, anything else): only that role
description VARCHAR(4096) NOT NULL
);

View File

@ -1,144 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::http::Status;
use rocket::{Request};
use rocket::request::{FromRequest, Outcome};
use crate::tokens::{validate_auth_token, validate_session_token};
pub struct PartialUserInfo {
pub user_id: i32,
pub created_at: i64,
pub email: String,
pub has_totp_auth: bool,
pub session_id: String,
pub auth_id: Option<String>
}
#[derive(Debug)]
pub enum AuthenticationError {
MissingToken,
InvalidToken(usize),
DatabaseError,
RequiresTOTP
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for PartialUserInfo {
type Error = AuthenticationError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = req.headers();
// make sure the bearer token exists
if let Some(authorization) = headers.get_one("Authorization") {
// parse bearer token
let components = authorization.split(' ').collect::<Vec<&str>>();
if components.len() != 2 && components.len() != 3 {
return Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken));
}
if components[0] != "Bearer" {
return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(0)));
}
if components.len() == 2 && !components[1].starts_with("st-") {
return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(1)));
}
let st: String;
let user_id: i64;
let at: Option<String>;
match &components[1][..3] {
"st-" => {
// validate session token
st = components[1].to_string();
match validate_session_token(st.clone(), req.rocket().state().unwrap()).await {
Ok(uid) => user_id = uid,
Err(_) => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(2)))
}
},
_ => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(3)))
}
if components.len() == 3 {
match &components[2][..3] {
"at-" => {
// validate auth token
at = Some(components[2].to_string());
match validate_auth_token(at.clone().unwrap().clone(), st.clone(), req.rocket().state().unwrap()).await {
Ok(_) => (),
Err(_) => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(4)))
}
},
_ => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken(5)))
}
} else {
at = None;
}
// this user is 100% valid and authenticated, fetch their info
let user = match sqlx::query!("SELECT * FROM users WHERE id = $1", user_id.clone() as i32).fetch_one(req.rocket().state().unwrap()).await {
Ok(u) => u,
Err(_) => return Outcome::Failure((Status::InternalServerError, AuthenticationError::DatabaseError))
};
Outcome::Success(PartialUserInfo {
user_id: user_id as i32,
created_at: user.created_on as i64,
email: user.email,
has_totp_auth: at.is_some(),
session_id: st,
auth_id: at,
})
} else {
Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken))
}
}
}
pub struct TOTPAuthenticatedUserInfo {
pub user_id: i32,
pub created_at: i64,
pub email: String,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for TOTPAuthenticatedUserInfo {
type Error = AuthenticationError;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let userinfo = PartialUserInfo::from_request(request).await;
match userinfo {
Outcome::Failure(e) => Outcome::Failure(e),
Outcome::Forward(f) => Outcome::Forward(f),
Outcome::Success(s) => {
if s.has_totp_auth {
Outcome::Success(Self {
user_id: s.user_id,
created_at: s.created_at,
email: s.email,
})
} else {
Outcome::Failure((Status::Unauthorized, AuthenticationError::RequiresTOTP))
}
}
}
}
}

View File

@ -1,31 +1,51 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use log::error;
use once_cell::sync::Lazy;
use serde::{Serialize, Deserialize};
use serde::Deserialize;
use url::Url;
pub static CONFIG: Lazy<TrifidConfig> = Lazy::new(|| {
let config_str = match fs::read_to_string("/etc/trifid/config.toml") {
Ok(str) => str,
Err(e) => {
error!("Unable to read config file: {}", e);
std::process::exit(1);
}
};
#[derive(Deserialize)]
pub struct TFConfig {
pub listen_port: u16,
pub db_url: String,
pub base: Url,
pub web_root: Url,
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
match toml::from_str(&config_str) {
Ok(cfg) => cfg,
Err(e) => {
error!("Unable to parse config file: {}", e);
std::process::exit(1);
}
}
});
#[derive(Serialize, Debug, Deserialize)]
pub struct TrifidConfig {
pub database: TrifidConfigDatabase
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrifidConfigDatabase {
pub url: String,
#[serde(default = "max_connections_default")]
pub max_connections: u32,
#[serde(default = "min_connections_default")]
pub min_connections: u32,
#[serde(default = "time_defaults")]
pub connect_timeout: u64,
#[serde(default = "time_defaults")]
pub acquire_timeout: u64,
#[serde(default = "time_defaults")]
pub idle_timeout: u64,
#[serde(default = "time_defaults")]
pub max_lifetime: u64,
#[serde(default = "sqlx_logging_default")]
pub sqlx_logging: bool
}
fn max_connections_default() -> u32 { 100 }
fn min_connections_default() -> u32 { 5 }
fn time_defaults() -> u64 { 8 }
fn sqlx_logging_default() -> bool { true }

View File

@ -1,43 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::{Aead, Payload};
use rand::Rng;
use rand::rngs::OsRng;
use crate::config::TFConfig;
pub fn get_cipher_from_config(config: &TFConfig) -> Result<Aes256Gcm, Box<dyn Error>> {
let key_slice = hex::decode(&config.data_key)?;
Ok(Aes256Gcm::new_from_slice(&key_slice)?)
}
pub fn encrypt_with_nonce(plaintext: &[u8], nonce: [u8; 12], cipher: &Aes256Gcm) -> Result<Vec<u8>, aes_gcm::Error> {
let nonce = Nonce::from_slice(&nonce);
let ciphertext = cipher.encrypt(nonce, plaintext)?;
Ok(ciphertext)
}
pub fn decrypt_with_nonce(ciphertext: &[u8], nonce: [u8; 12], cipher: &Aes256Gcm) -> Result<Vec<u8>, aes_gcm::Error> {
let nonce = Nonce::from_slice(&nonce);
let plaintext = cipher.decrypt(nonce, Payload::from(ciphertext))?;
Ok(plaintext)
}
pub fn generate_random_iv() -> [u8; 12] {
OsRng.gen()
}

View File

@ -1,15 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -1,98 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fmt::{Display, Formatter};
use crate::format::PEMValidationError::{IncorrectSegmentLength, InvalidBase64Data, MissingStartSentinel};
use crate::util::base64decode;
pub const ED_PUBKEY_START_STR: &str = "-----BEGIN NEBULA ED25519 PUBLIC KEY-----";
pub const ED_PUBKEY_END_STR: &str = "-----END NEBULA ED25519 PUBLIC KEY-----";
pub const DH_PUBKEY_START_STR: &str = "-----BEGIN NEBULA X25519 PUBLIC KEY-----";
pub const DH_PUBKEY_END_STR: &str = "-----END NEBULA X25519 PUBLIC KEY-----";
pub enum PEMValidationError {
MissingStartSentinel,
InvalidBase64Data,
MissingEndSentinel,
IncorrectSegmentLength(usize, usize)
}
impl Display for PEMValidationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingEndSentinel => write!(f, "Missing ending sentinel"),
Self::MissingStartSentinel => write!(f, "Missing starting sentinel"),
Self::InvalidBase64Data => write!(f, "invalid base64 data"),
Self::IncorrectSegmentLength(expected, got) => write!(f, "incorrect number of segments, expected {} got {}", expected, got)
}
}
}
pub fn validate_ed_pubkey_pem(pubkey: &str) -> Result<(), PEMValidationError> {
let segments = pubkey.split('\n');
let segs = segments.collect::<Vec<&str>>();
if segs.len() < 3 {
return Err(IncorrectSegmentLength(3, segs.len()))
}
if segs[0] != ED_PUBKEY_START_STR {
return Err(MissingStartSentinel)
}
if base64decode(segs[1]).is_err() {
return Err(InvalidBase64Data)
}
if segs[2] != ED_PUBKEY_END_STR {
return Err(MissingStartSentinel)
}
Ok(())
}
pub fn validate_dh_pubkey_pem(pubkey: &str) -> Result<(), PEMValidationError> {
let segments = pubkey.split('\n');
let segs = segments.collect::<Vec<&str>>();
if segs.len() < 3 {
return Err(IncorrectSegmentLength(3, segs.len()))
}
if segs[0] != DH_PUBKEY_START_STR {
return Err(MissingStartSentinel)
}
if base64decode(segs[1]).is_err() {
return Err(InvalidBase64Data)
}
if segs[2] != DH_PUBKEY_END_STR {
return Err(MissingStartSentinel)
}
Ok(())
}
pub fn validate_ed_pubkey_base64(pubkey: &str) -> Result<(), PEMValidationError> {
match base64decode(pubkey) {
Ok(k) => validate_ed_pubkey_pem(match std::str::from_utf8(k.as_ref()) {
Ok(k) => k,
Err(_) => return Err(InvalidBase64Data)
}),
Err(_) => Err(InvalidBase64Data)
}
}
pub fn validate_dh_pubkey_base64(pubkey: &str) -> Result<(), PEMValidationError> {
match base64decode(pubkey) {
Ok(k) => validate_dh_pubkey_pem(match std::str::from_utf8(k.as_ref()) {
Ok(k) => k,
Err(_) => return Err(InvalidBase64Data)
}),
Err(_) => Err(InvalidBase64Data)
}
}

View File

@ -1,28 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error;
use sqlx::PgPool;
pub async fn kv_get<'a>(key: &'a str, db: &PgPool) -> Result<Option<String>, Box<dyn Error>> {
let res = sqlx::query!("SELECT datavalue FROM cacheddata WHERE datakey = $1", key).fetch_optional(db).await?;
Ok(res.map(|i| i.datavalue))
}
pub async fn kv_set(key: &str, value: &str, db: &PgPool) -> Result<(), Box<dyn Error>> {
sqlx::query!("INSERT INTO cacheddata (datakey, datavalue) VALUES ($2, $1) ON CONFLICT (datakey) DO UPDATE SET datavalue = $1", value, key).execute(db).await?;
Ok(())
}

View File

@ -1,213 +1,35 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
extern crate core;
use std::error::Error;
use std::fs;
use std::path::Path;
use dotenvy::dotenv;
use log::{error, info};
use rocket::{catchers, Request, Response, routes};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::Header;
use sha2::Sha256;
use sqlx::migrate::Migrator;
use sqlx::postgres::PgPoolOptions;
use crate::config::TFConfig;
use crate::kv::{kv_get, kv_set};
use sha2::Digest;
use std::time::Duration;
use actix_web::{App, HttpResponse, HttpServer, post, web::{Data, Json, JsonConfig}};
use log::{error, info, Level};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use serde::{Serialize, Deserialize};
use crate::config::CONFIG;
pub mod format;
pub mod util;
pub mod db;
pub mod config;
pub mod tokens;
pub mod routes;
pub mod auth;
pub mod crypto;
pub mod org;
pub mod kv;
pub mod role;
static MIGRATOR: Migrator = sqlx::migrate!();
pub struct CORS;
#[rocket::async_trait]
impl Fairing for CORS {
fn info(&self) -> Info {
Info {
name: "Add CORS headers to responses",
kind: Kind::Response,
}
}
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, PATCH, OPTIONS"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
}
}
#[rocket::main]
#[actix_web::main]
async fn main() -> Result<(), Box<dyn Error>> {
let _ = rocket::build();
simple_logger::init_with_level(Level::Debug).unwrap();
info!("[tfapi] loading config");
info!("Connecting to database at {}...", CONFIG.database.url);
let _ = dotenv();
let mut opt = ConnectOptions::new(CONFIG.database.url.clone());
opt.max_connections(CONFIG.database.max_connections)
.min_connections(CONFIG.database.min_connections)
.connect_timeout(Duration::from_secs(CONFIG.database.connect_timeout))
.acquire_timeout(Duration::from_secs(CONFIG.database.acquire_timeout))
.idle_timeout(Duration::from_secs(CONFIG.database.idle_timeout))
.max_lifetime(Duration::from_secs(CONFIG.database.max_lifetime))
.sqlx_logging(CONFIG.database.sqlx_logging)
.sqlx_logging_level(log::LevelFilter::Info);
if std::env::var("CONFIG_FILE").is_err() && !Path::new("config.toml").exists() {
error!("[tfapi] fatal: the environment variable CONFIG_FILE is not set");
error!("[tfapi] help: try creating a .env file that sets it");
error!("[tfapi] help: or, create a file config.toml with your config, as it is loaded automatically");
std::process::exit(1);
}
let db = Database::connect(opt).await?;
let config_file = if Path::new("config.toml").exists() {
"config.toml".to_string()
} else {
std::env::var("CONFIG_FILE").unwrap()
};
let config_data = match fs::read_to_string(&config_file) {
Ok(d) => d,
Err(e) => {
error!("[tfapi] fatal: unable to read config from {}", config_file);
error!("[tfapi] fatal: {}", e);
std::process::exit(1);
}
};
let config: TFConfig = match toml::from_str(&config_data) {
Ok(c) => c,
Err(e) => {
error!("[tfapi] fatal: unable to parse config from {}", config_file);
error!("[tfapi] fatal: {}", e);
std::process::exit(1);
}
};
info!("[tfapi] connecting to database pool");
let pool = match PgPoolOptions::new().max_connections(5).connect(&config.db_url).await {
Ok(p) => p,
Err(e) => {
error!("[tfapi] fatal: unable to connect to database pool");
error!("[tfapi] fatal: {}", e);
std::process::exit(1);
}
};
info!("[tfapi] running database migrations");
MIGRATOR.run(&pool).await?;
info!("[tfapi] verifying encryption key");
let kv_hash = kv_get("pmk_hash", &pool).await.expect("Unable to get pmk hash from kv store");
let mut hasher = Sha256::new();
hasher.update(config.data_key.as_bytes());
let config_hash = hex::encode(hasher.finalize());
if let Some(k_hash) = kv_hash {
if config_hash != k_hash {
error!("[tfapi] fatal: instance master key does not match key used to encrypt data");
error!("[tfapi] fatal: datastore was encrypted with keyid {k_hash}");
error!("[tfapi] fatal: the key in your config has the keyid {config_hash}");
error!("[tfapi] fatal: you probably changed data_key. please return it to it's original value");
std::process::exit(1);
} else {
info!("[tfapi] data keyid is {config_hash}");
}
} else {
info!("[tfapi] detected first run");
info!("[tfapi] welcome to trifid!");
info!("[tfapi] data keyid is {config_hash}");
if let Err(e) = kv_set("pmk_hash", config_hash.as_str(), &pool).await {
error!("[tfapi] fatal: unable to set pmk_hash in kv store");
error!("[tfapi] fatal: the database returned the following error:");
error!("[tfapi] fatal: {e}");
std::process::exit(1);
} else {
info!("[tfapi] configured instance information in kv store");
}
}
info!("[tfapi] building rocket config");
let figment = rocket::Config::figment().merge(("port", config.listen_port));
let _ = rocket::custom(figment)
.mount("/", routes![
crate::routes::v1::auth::magic_link::magiclink_request,
crate::routes::v1::auth::magic_link::options,
crate::routes::v1::signup::signup_request,
crate::routes::v1::signup::options,
crate::routes::v1::auth::verify_magic_link::verify_magic_link,
crate::routes::v1::auth::verify_magic_link::options,
crate::routes::v1::totp_authenticators::totp_authenticators_request,
crate::routes::v1::totp_authenticators::options,
crate::routes::v1::verify_totp_authenticator::verify_totp_authenticator_request,
crate::routes::v1::verify_totp_authenticator::options,
crate::routes::v1::auth::totp::totp_request,
crate::routes::v1::auth::totp::options,
crate::routes::v1::auth::check_session::check_session,
crate::routes::v1::auth::check_session::check_session_auth,
crate::routes::v1::auth::check_session::options,
crate::routes::v1::auth::check_session::options_auth,
crate::routes::v2::whoami::whoami_request,
crate::routes::v2::whoami::options,
crate::routes::v1::organization::options,
crate::routes::v1::organization::orgidoptions,
crate::routes::v1::organization::orginfo_req,
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::organization::createorgoptions,
crate::routes::v1::ca::get_cas_for_org,
crate::routes::v1::ca::options,
crate::routes::v1::roles::get_roles,
crate::routes::v1::roles::options,
crate::routes::v1::roles::options_roleadd,
crate::routes::v1::roles::role_add
])
.register("/", catchers![
crate::routes::handler_400,
crate::routes::handler_401,
crate::routes::handler_403,
crate::routes::handler_404,
crate::routes::handler_422,
crate::routes::handler_500,
crate::routes::handler_501,
crate::routes::handler_502,
crate::routes::handler_503,
crate::routes::handler_504,
crate::routes::handler_505,
])
.attach(CORS)
.manage(pool)
.manage(config)
.launch().await?;
Ok(())
}

View File

@ -1,77 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error;
use 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<Option<i32>, Box<dyn Error>> {
Ok(sqlx::query!("SELECT id FROM organizations WHERE owner = $1", user).fetch_optional(db).await?.map(|r| r.id))
}
pub async fn get_orgs_by_assoc_id(user: i32, db: &PgPool) -> Result<Vec<i32>, Box<dyn Error>> {
let res: Vec<_> = sqlx::query!("SELECT org_id FROM organization_authorized_users WHERE user_id = $1", user).fetch_all(db).await?;
let mut ret = vec![];
for i in res {
ret.push(i.org_id);
}
Ok(ret)
}
pub async fn get_users_by_assoc_org(org: i32, db: &PgPool) -> Result<Vec<i32>, Box<dyn Error>> {
let res: Vec<_> = sqlx::query!("SELECT user_id FROM organization_authorized_users WHERE org_id = $1", org).fetch_all(db).await?;
let mut ret = vec![];
for i in res {
ret.push(i.user_id);
}
Ok(ret)
}
pub async fn get_associated_orgs(user: i32, db: &PgPool) -> Result<Vec<i32>, Box<dyn Error>> {
let mut assoc_orgs = vec![];
if let Some(owned_org) = get_org_by_owner_id(user, db).await? {
assoc_orgs.push(owned_org);
}
assoc_orgs.append(&mut get_orgs_by_assoc_id(user, db).await?);
Ok(assoc_orgs)
}
pub async fn user_has_org_assoc(user: i32, org: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
let associated_orgs = get_associated_orgs(user, db).await?;
Ok(associated_orgs.contains(org))
}
pub async fn get_org_ca_pool(org: i32, db: &PgPool) -> Result<NebulaCAPool, Box<dyn Error>> {
// get CAPool PEM from db
let pem = sqlx::query!("SELECT ca_crt FROM organizations WHERE id = $1", org).fetch_one(db).await?.ca_crt;
let ca_pool = NebulaCAPool::new_from_pem(&hex::decode(pem)?)?;
Ok(ca_pool)
}

View File

@ -1,98 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error;
use sqlx::PgPool;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[repr(i32)]
pub enum Protocol {
Any = 0,
TCP = 1,
UDP = 2,
ICMP = 3
}
#[derive(Serialize, Deserialize)]
pub enum AllowFrom {
Anyone,
SpecificRole(i32)
}
#[derive(Serialize, Deserialize)]
pub struct FirewallRule {
pub id: i32,
pub protocol: Protocol,
pub port_start: u16,
pub port_end: u16,
pub allow_from: AllowFrom,
pub description: String
}
#[derive(Serialize, Deserialize)]
pub struct Role {
pub id: i32,
pub org_id: i32,
pub name: String,
pub description: String,
pub firewall_rules: Vec<FirewallRule>
}
pub async fn get_role(role_id: i32, db: &PgPool) -> Result<Option<Role>, Box<dyn Error>> {
let query_result: Option<_> = sqlx::query!("SELECT * FROM roles WHERE id = $1", role_id).fetch_optional(db).await?;
if let Some(res) = query_result {
// get all firewall rules
let rules_res = sqlx::query!("SELECT * FROM roles_firewall_rules WHERE role = $1", Some(role_id)).fetch_all(db).await?;
let mut rules = vec![];
for rule in rules_res {
rules.push(FirewallRule {
id: rule.id,
protocol: match rule.protocol {
0 => Protocol::Any,
1 => Protocol::TCP,
2 => Protocol::UDP,
3 => Protocol::ICMP,
_ => return Err(format!("invalid protocol on a firewall rule {}", rule.id).into())
},
port_start: rule.port_range_start as u16,
port_end: rule.port_range_end as u16,
allow_from: match rule.allow_from {
-1 => AllowFrom::Anyone,
_ => AllowFrom::SpecificRole(rule.allow_from)
},
description: rule.description,
})
}
Ok(Some(Role {
id: role_id,
org_id: res.org,
name: res.name,
description: res.description,
firewall_rules: rules,
}))
} else {
Ok(None)
}
}
pub async fn get_role_ids_for_ca(org_id: i32, db: &PgPool) -> Result<Vec<i32>, Box<dyn Error>> {
Ok(sqlx::query!("SELECT id FROM roles WHERE org = $1", org_id).fetch_all(db).await?.iter().map(|r| r.id).collect())
}

View File

@ -1,82 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod v1;
pub mod v2;
use rocket::catch;
use serde::{Serialize};
use rocket::http::Status;
pub const ERR_MSG_MALFORMED_REQUEST: &str = "unable to parse the request body - is it valid JSON, using correct types?";
pub const ERR_MSG_MALFORMED_REQUEST_CODE: &str = "ERR_MALFORMED_REQUEST";
/*
TODO:
/v1/auth/magic-link [done]
/v1/auth/totp [done]
/v1/auth/verify-magic-link [done]
/v1/hosts/host-{id}/enrollment-code
/v1/hosts/host-{id}/enrollment-code-check
/v1/hosts/host-{id}
/v1/roles/role-{id}
/v1/feature-flags
/v1/hosts
/v1/networks
/v1/roles
/v1/signup [done]
/v1/totp-authenticators [done]
/v1/verify-totp-authenticator [done]
/v1/dnclient
/v2/enroll
/v2/whoami [in-progress]
*/
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct APIError {
errors: Vec<APIErrorSingular>
}
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
pub struct APIErrorSingular {
code: String,
message: String
}
macro_rules! error_handler {
($code: expr, $err: expr, $msg: expr) => {
::paste::paste! {
#[catch($code)]
pub fn [<handler_ $code>]() -> (Status, String) {
(Status::from_code($code).unwrap(), format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", $err, $msg))
}
}
};
}
error_handler!(400, "ERR_MALFORMED_REQUEST", "unable to parse the request body, is it properly formatted?");
error_handler!(401, "ERR_AUTHENTICATION_REQUIRED", "this endpoint requires authentication but it was not provided");
error_handler!(403, "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid");
error_handler!(404, "ERR_NOT_FOUND", "resource not found");
error_handler!(405, "ERR_METHOD_NOT_ALLOWED", "method not allowed for this endpoint");
error_handler!(422, "ERR_MALFORMED_REQUEST", "unable to parse the request body, is it properly formatted?");
error_handler!(500, "ERR_QL_QUERY_FAILED", "graphql query timed out");
error_handler!(501, "ERR_NOT_IMPLEMENTED", "query not supported by this version of graphql");
error_handler!(502, "ERR_PROXY_ERR", "servers under load, please try again later");
error_handler!(503, "ERR_SERVER_OVERLOADED", "servers under load, please try again later");
error_handler!(504, "ERR_PROXY_TIMEOUT", "servers under load, please try again later");
error_handler!(505, "ERR_CLIENT_UNSUPPORTED", "your version of dnclient is out of date, please update");

View File

@ -1,47 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::{post, options};
use crate::auth::{PartialUserInfo, TOTPAuthenticatedUserInfo};
/*
These endpoints do not return any actual data, and are used purely to check auth tokens
by the client code.
Since PartialUserInfo implements FromRequest, and will error out the req even before
it gets to our handler if the auth is invalid, these reqs just have to have
it as a param. They therefore don't need to s
*/
#[options("/v1/auth/check_session")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/auth/check_session")]
pub async fn check_session(_user: PartialUserInfo) -> &'static str {
"ok"
}
#[options("/v1/auth/check_auth")]
pub async fn options_auth() -> &'static str {
""
}
#[post("/v1/auth/check_auth")]
pub async fn check_session_auth(_user: TOTPAuthenticatedUserInfo) -> &'static str {
"ok"
}

View File

@ -1,78 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::{post, State};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use rocket::http::{ContentType, Status};
use sqlx::PgPool;
use crate::config::TFConfig;
use crate::tokens::send_magic_link;
use rocket::options;
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkRequest {
pub email: String,
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkResponse {
pub data: Option<String>,
pub metadata: MagicLinkResponseMetadata,
}
#[options("/v1/auth/magic-link")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/auth/magic-link", data = "<req>")]
pub async fn magiclink_request(req: Json<MagicLinkRequest>, pool: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<MagicLinkResponse>), (Status, String)> {
// figure out if the user already exists
let mut id = -1;
match sqlx::query!("SELECT id FROM users WHERE email = $1", req.email.clone()).fetch_optional(pool.inner()).await {
Ok(res) => if let Some(r) = res { id = r.id as i64 },
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
}
if id == -1 {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid")))
}
// send magic link to email
match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {
Ok(_) => (),
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
};
// this endpoint doesn't actually ever return an error? it will send you the magic link no matter what
// this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)
Ok((ContentType::JSON, Json(MagicLinkResponse {
data: None,
metadata: MagicLinkResponseMetadata {},
})))
}

View File

@ -1,20 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod verify_magic_link;
pub mod magic_link;
pub mod totp;
pub mod check_session;

View File

@ -1,89 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use crate::auth::PartialUserInfo;
use serde::{Serialize, Deserialize};
use rocket::{post, State};
use sqlx::PgPool;
use rocket::options;
use crate::tokens::{generate_auth_token, get_totpmachine, user_has_totp};
pub const TOTP_GENERIC_UNAUTHORIZED_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_INVALID_TOTP_CODE\",\"message\":\"invalid TOTP code (maybe it expired?)\",\"path\":\"code\"}]}";
pub const TOTP_NO_TOTP_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_NO_TOTP\",\"message\":\"logged-in user does not have totp enabled\",\"path\":\"code\"}]}";
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpRequest {
pub code: String
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpResponseData {
#[serde(rename = "authToken")]
auth_token: String
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpResponseMetadata {
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpResponse {
data: TotpResponseData,
metadata: TotpResponseMetadata
}
#[options("/v1/auth/totp")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/auth/totp", data = "<req>")]
pub async fn totp_request(req: Json<TotpRequest>, user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<TotpResponse>), (Status, String)> {
if !match user_has_totp(user.user_id, db.inner()).await {
Ok(b) => b,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
} {
return Err((Status::UnprocessableEntity, TOTP_NO_TOTP_ERROR.to_string()))
}
if user.has_totp_auth {
return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_AUTHED", "user already has valid totp authentication")))
}
let totpmachine = match get_totpmachine(user.user_id, db.inner()).await {
Ok(t) => t,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
}
};
if !totpmachine.check_current(&req.0.code).unwrap_or(false) {
return Err((Status::Unauthorized, TOTP_GENERIC_UNAUTHORIZED_ERROR.to_string()))
}
Ok((ContentType::JSON, Json(TotpResponse {
data: TotpResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {
Ok(t) => t,
Err(e) => { return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e))) }
} },
metadata: TotpResponseMetadata {},
})))
}

View File

@ -1,91 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::time::{SystemTime, UNIX_EPOCH};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use rocket::{post, State};
use sqlx::PgPool;
use crate::config::TFConfig;
use crate::tokens::generate_session_token;
use rocket::options;
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct VerifyMagicLinkRequest {
#[serde(rename = "magicLinkToken")]
pub magic_link_token: String,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyMagicLinkResponseMetadata {}
#[derive(Serialize, Deserialize)]
pub struct VerifyMagicLinkResponseData {
#[serde(rename = "sessionToken")]
pub session_token: String,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyMagicLinkResponse {
pub data: VerifyMagicLinkResponseData,
pub metadata: VerifyMagicLinkResponseMetadata,
}
#[options("/v1/auth/verify-magic-link")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/auth/verify-magic-link", data = "<req>")]
pub async fn verify_magic_link(req: Json<VerifyMagicLinkRequest>, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<VerifyMagicLinkResponse>), (Status, String)> {
// get the current time to check if the token is expired
let (user_id, expired_at) = match sqlx::query!("SELECT user_id, expires_on FROM magic_links WHERE id = $1", req.0.magic_link_token).fetch_one(db.inner()).await {
Ok(row) => (row.user_id, row.expires_on),
Err(e) => {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNAUTHORIZED", "this token is invalid", e)))
}
};
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32;
println!("expired on {}, currently {}", expired_at, current_time);
if expired_at < current_time {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_UNAUTHORIZED", "valid authorization was provided but it is expired")))
}
// generate session token
let token = match generate_session_token(user_id as i64, db.inner(), config.inner()).await {
Ok(t) => t,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
}
};
// delete the token
match sqlx::query!("DELETE FROM magic_links WHERE id = $1", req.0.magic_link_token).execute(db.inner()).await {
Ok(_) => (),
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
}
}
Ok((ContentType::JSON, Json(VerifyMagicLinkResponse {
data: VerifyMagicLinkResponseData { session_token: token },
metadata: VerifyMagicLinkResponseMetadata {},
})))
}

View File

@ -1,85 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::{options, get, State};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use sqlx::PgPool;
use serde::{Serialize, Deserialize};
use crate::auth::TOTPAuthenticatedUserInfo;
use crate::org::{get_associated_orgs, get_org_ca_pool};
#[options("/v1/org/<_id>/ca")]
pub fn options(_id: i32) -> &'static str {
""
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CaList {
pub trusted_cas: Vec<CA>,
pub blocklisted_certs: Vec<String>
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CA {
pub fingerprint: String,
pub cert: String
}
#[get("/v1/org/<id>/ca")]
pub async fn get_cas_for_org(id: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<CaList>), (Status, String)> {
let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await {
Ok(r) => r,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query", e)))
};
if !associated_orgs.contains(&id) {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_NOT_YOUR_ORG", "you are not authorized to view details of this org")))
}
let ca_pool = match get_org_ca_pool(id, db.inner()).await {
Ok(pool) => pool,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to load certificates from database - {}\"}}]}}", e)));
}
};
let mut trusted_cas = vec![];
for (fingerprint, cert) in ca_pool.cas {
trusted_cas.push(CA {
fingerprint,
cert: match cert.serialize_to_pem() {
Ok(pem) => match String::from_utf8(pem) {
Ok(str) => str,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to encode one of the serialized certificates - {}\"}}]}}", e)))
},
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"ERR_QL_QUERY_FAILED\",\"message\":\"unable to serialize one of the certificates - {}\"}}]}}", e)));
}
}
})
}
Ok((ContentType::JSON, Json(CaList {
trusted_cas,
blocklisted_certs: ca_pool.cert_blocklist,
})))
}

View File

@ -1,25 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod auth;
pub mod signup;
pub mod totp_authenticators;
pub mod verify_totp_authenticator;
pub mod organization;
pub mod user;
pub mod ca;
pub mod roles;

View File

@ -1,190 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::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};
use crate::org::{get_associated_orgs, get_org_by_owner_id, get_users_by_assoc_org, user_has_org_assoc};
#[options("/v1/orgs")]
pub fn options() -> &'static str {
""
}
#[options("/v1/org/<_id>")]
pub fn orgidoptions(_id: i32) -> &'static str {
""
}
#[options("/v1/org")]
pub fn createorgoptions() -> &'static str {""}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct OrglistStruct {
pub org_ids: Vec<i32>
}
#[get("/v1/orgs")]
pub async fn orglist_req(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrglistStruct>), (Status, String)> {
// this endpoint lists the associated organizations this user has access to
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)))
};
Ok((ContentType::JSON, Json(OrglistStruct {
org_ids: associated_orgs
})))
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct OrginfoStruct {
pub org_id: i32,
pub owner_id: i32,
pub ca_crt: String,
pub authorized_users: Vec<i32>
}
#[get("/v1/org/<orgid>")]
pub async fn orginfo_req(orgid: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
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")));
}
let org = sqlx::query!("SELECT id, owner, ca_crt FROM organizations WHERE id = $1", orgid).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?;
let authorized_users = get_users_by_assoc_org(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)))?;
Ok((ContentType::JSON, Json(
OrginfoStruct {
org_id: orgid,
owner_id: org.owner,
ca_crt: org.ca_crt,
authorized_users,
}
)))
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct CreateCARequest {
pub ip_ranges: Vec<Ipv4Net>,
pub subnet_ranges: Vec<Ipv4Net>,
pub groups: Vec<String>
}
#[post("/v1/org", data = "<req>")]
pub async fn create_org(req: Json<CreateCARequest>, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<OrginfoStruct>), (Status, String)> {
if get_org_by_owner_id(user.user_id, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?.is_some() {
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);
let owner_id = user.user_id;
let cipher = match get_cipher_from_config(config) {
Ok(c) => c,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e)));
}
};
let ca_key = match encrypt_with_nonce(&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(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)))?;
// 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,
owner_id,
ca_crt,
authorized_users: vec![owner_id],
}
)))
}

View File

@ -1,95 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use log::error;
use rocket::{options, get, State, post};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use sqlx::PgPool;
use crate::auth::TOTPAuthenticatedUserInfo;
use crate::org::user_has_org_assoc;
use crate::role::{get_role, get_role_ids_for_ca, Role};
#[options("/v1/org/<_org>/roles")]
pub fn options(_org: i32) -> &'static str { "" }
#[options("/v1/org/<_org>/role")]
pub fn options_roleadd(_org: i32) -> &'static str { "" }
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct RolesResponse {
pub data: Vec<Role>
}
#[get("/v1/org/<org>/roles")]
pub async fn get_roles(org: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<RolesResponse>), (Status, String)> {
if !user_has_org_assoc(user.user_id, org, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
}
let roles = match get_role_ids_for_ca(org, db.inner()).await {
Ok(r) => r,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", e)))
}
};
let mut resp = RolesResponse {
data: vec![]
};
for role in roles {
let role_info = match get_role(role, db.inner()).await {
Ok(r) => r,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", e)))
}
};
if let Some(info) = role_info {
resp.data.push(info);
} else {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "there was an error querying the db, please try again later", "missing role as returned by server - possibly deleted inbetween?")))
}
}
Ok((ContentType::JSON, Json(resp)))
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct RoleaddReq {
pub name: String,
pub description: String
}
#[post("/v1/org/<org>/role", data = "<req>")]
pub async fn role_add(req: Json<RoleaddReq>, org: i32, user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, String), (Status, String)> {
if !user_has_org_assoc(user.user_id, org, db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))? {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_MISSING_ORG_AUTHORIZATION", "this user does not have permission to access this org")));
}
match sqlx::query!("INSERT INTO roles (org, name, description) VALUES ($1, $2, $3)", org, req.name, req.description).execute(db.inner()).await {
Ok(_) => (),
Err(e) => {
error!("[tfapi] dberror: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_INTERNAL_SERVER_ERR", "database returned error while trying to create role", "unable to insert role")))
}
}
Ok((ContentType::JSON, "{}".to_string()))
}

View File

@ -1,89 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::{post, State};
use rocket::serde::json::Json;
use serde::{Serialize, Deserialize};
use std::time::{SystemTime, UNIX_EPOCH};
use rocket::http::{ContentType, Status};
use sqlx::PgPool;
use crate::config::TFConfig;
use crate::tokens::send_magic_link;
use rocket::options;
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct SignupRequest {
pub email: String,
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct SignupResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct SignupResponse {
pub data: Option<String>,
pub metadata: SignupResponseMetadata,
}
/*
created_on TIMESTAMP NOT NULL,
banned INTEGER NOT NULL,
ban_reason VARCHAR(1024) NOT NULL
*/
#[options("/v1/signup")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/signup", data = "<req>")]
pub async fn signup_request(req: Json<SignupRequest>, pool: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<SignupResponse>), (Status, String)> {
// figure out if the user already exists
let mut id = -1;
match sqlx::query!("SELECT id FROM users WHERE email = $1", req.email.clone()).fetch_optional(pool.inner()).await {
Ok(res) => if let Some(r) = res { id = r.id as i64 },
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
}
if id == -1 {
let id_res = match sqlx::query!("INSERT INTO users (email, created_on, banned, ban_reason, totp_secret, totp_otpurl, totp_verified) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING RETURNING id;", req.email, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, 0, "", "", "", 0).fetch_one(pool.inner()).await {
Ok(row) => row.id,
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
};
id = id_res as i64;
}
// send magic link to email
match send_magic_link(id, req.email.clone(), pool.inner(), config.inner()).await {
Ok(_) => (),
Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
}
};
// this endpoint doesn't actually ever return an error? it will send you the magic link no matter what
// this appears to do the exact same thing as /v1/auth/magic-link, but it doesn't check if you have an account (magic-link does)
Ok((ContentType::JSON, Json(SignupResponse {
data: None,
metadata: SignupResponseMetadata {},
})))
}

View File

@ -1,76 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use rocket::{State, post};
use sqlx::PgPool;
use serde::{Serialize, Deserialize};
use crate::auth::PartialUserInfo;
use crate::config::TFConfig;
use crate::tokens::{create_totp_token, user_has_totp};
use rocket::options;
#[derive(Deserialize)]
pub struct TotpAuthenticatorsRequest {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpAuthenticatorsResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpAuthenticatorsResponseData {
#[serde(rename = "totpToken")]
pub totp_token: String,
pub secret: String,
pub url: String,
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct TotpAuthenticatorsResponse {
pub data: TotpAuthenticatorsResponseData,
pub metadata: TotpAuthenticatorsResponseMetadata,
}
#[options("/v1/totp-authenticators")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/totp-authenticators", data = "<_req>")]
pub async fn totp_authenticators_request(_req: Json<TotpAuthenticatorsRequest>, user: PartialUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<TotpAuthenticatorsResponse>), (Status, String)> {
if match user_has_totp(user.user_id, db.inner()).await {
Ok(b) => b,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
} {
return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_EXISTS", "this user already has a totp authenticator on their account")))
}
// generate a totp token
let (totptoken, totpmachine) = match create_totp_token(user.email, db.inner(), config.inner()).await {
Ok(t) => t,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured issuing a totp token, try again later", e)))
};
Ok((ContentType::JSON, Json(TotpAuthenticatorsResponse {
data: TotpAuthenticatorsResponseData {
totp_token: totptoken,
secret: totpmachine.get_secret_base32(),
url: totpmachine.get_url(),
},
metadata: TotpAuthenticatorsResponseMetadata {},
})))
}

View File

@ -1,47 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use serde::{Serialize, Deserialize};
use rocket::{get, options};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use rocket::State;
use sqlx::PgPool;
#[options("/v1/user/<_id>")]
pub fn options(_id: i32) -> &'static str {
""
}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct UserInfoResponse {
email: String
}
#[get("/v1/user/<id>")]
pub async fn get_user(id: i32, db: &State<PgPool>) -> Result<(ContentType, Json<UserInfoResponse>), (Status, String)> {
let user = match sqlx::query!("SELECT email FROM users WHERE id = $1", id.clone() as i32).fetch_one(db.inner()).await {
Ok(u) => u,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))
};
Ok((ContentType::JSON, Json(
UserInfoResponse {
email: user.email,
}
)))
}

View File

@ -1,75 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use rocket::http::{ContentType, Status};
use crate::auth::PartialUserInfo;
use rocket::post;
use rocket::serde::json::Json;
use rocket::State;
use serde::{Serialize, Deserialize};
use sqlx::PgPool;
use crate::tokens::{generate_auth_token, use_totp_token, verify_totp_token};
use rocket::options;
#[derive(Serialize, Deserialize)]
pub struct VerifyTotpAuthenticatorRequest {
#[serde(rename = "totpToken")]
pub totp_token: String,
pub code: String,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyTotpAuthenticatorResponseMetadata {}
#[derive(Serialize, Deserialize)]
pub struct VerifyTotpAuthenticatorResponseData {
#[serde(rename = "authToken")]
pub auth_token: String,
}
#[derive(Serialize, Deserialize)]
pub struct VerifyTotpAuthenticatorResponse {
pub data: VerifyTotpAuthenticatorResponseData,
pub metadata: VerifyTotpAuthenticatorResponseMetadata,
}
#[options("/v1/verify-totp-authenticator")]
pub async fn options() -> &'static str {
""
}
#[post("/v1/verify-totp-authenticator", data = "<req>")]
pub async fn verify_totp_authenticator_request(req: Json<VerifyTotpAuthenticatorRequest>, db: &State<PgPool>, user: PartialUserInfo) -> Result<(ContentType, Json<VerifyTotpAuthenticatorResponse>), (Status, String)> {
let totpmachine = match verify_totp_token(req.0.totp_token.clone(), user.email.clone(), db.inner()).await {
Ok(t) => t,
Err(e) => return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNAUTHORIZED", "this token is invalid", e)))
};
if !totpmachine.check_current(&req.0.code).unwrap() {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\",\"path\":\"totpToken\"}}]}}", "ERR_INVALID_TOTP_CODE", "Invalid TOTP code")))
}
match use_totp_token(req.0.totp_token, user.email, db.inner()).await {
Ok(_) => (),
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
}
Ok((ContentType::JSON, Json(VerifyTotpAuthenticatorResponse {
data: VerifyTotpAuthenticatorResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await {
Ok(at) => at,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNABLE_TO_ISSUE", "an error occured trying to issue a session token, please try again later", e)))
} },
metadata: VerifyTotpAuthenticatorResponseMetadata {},
})))
}

View File

@ -1,17 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod whoami;

View File

@ -1,85 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use chrono::{NaiveDateTime, Utc};
use serde::{Serialize, Deserialize};
use rocket::{options, get, State};
use rocket::http::{ContentType, Status};
use rocket::serde::json::Json;
use sqlx::PgPool;
use crate::auth::PartialUserInfo;
use crate::org::get_org_by_owner_id;
use crate::tokens::user_has_totp;
#[derive(Serialize, Deserialize)]
pub struct WhoamiMetadata {}
#[derive(Serialize, Deserialize)]
pub struct WhoamiActor {
pub id: String,
#[serde(rename = "organizationID")]
pub organization_id: String,
pub email: String,
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "hasTOTPAuthenticator")]
pub has_totpauthenticator: bool,
}
#[derive(Serialize, Deserialize)]
pub struct WhoamiData {
#[serde(rename = "actorType")]
pub actor_type: String,
pub actor: WhoamiActor,
}
#[derive(Serialize, Deserialize)]
pub struct WhoamiResponse {
pub data: WhoamiData,
pub metadata: WhoamiMetadata,
}
#[options("/v2/whoami")]
pub fn options() -> &'static str {
""
}
#[get("/v2/whoami")]
pub async fn whoami_request(user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<WhoamiResponse>), (Status, String)> {
let org = match get_org_by_owner_id(user.user_id, db.inner()).await {
Ok(b) => match b {
Some(r) => r.to_string(),
None => String::new()
},
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DBERROR", "an error occured trying to verify your user", e)))
};
Ok((ContentType::JSON, Json(WhoamiResponse {
data: WhoamiData {
actor_type: "user".to_string(),
actor: WhoamiActor {
id: user.user_id.to_string(),
organization_id: org,
email: user.email,
created_at: NaiveDateTime::from_timestamp_opt(user.created_at, 0).unwrap().and_local_timezone(Utc).unwrap().to_rfc3339(),
has_totpauthenticator: match user_has_totp(user.user_id, db.inner()).await {
Ok(b) => b,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DBERROR", "an error occured trying to verify your user", e)))
},
}
},
metadata: WhoamiMetadata {},
})))
}

View File

@ -1,109 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error;
use log::info;
use sqlx::PgPool;
use uuid::Uuid;
use crate::config::TFConfig;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use totp_rs::{Secret, TOTP};
use crate::util::{TOTP_ALGORITHM, TOTP_DIGITS, TOTP_ISSUER, TOTP_SKEW, TOTP_STEP};
// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com&token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg
pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box<dyn Error>> {
let otp = format!("ml-{}", Uuid::new_v4());
let otp_url = config.web_root.join(&format!("/auth/magic-link?email={}&token={}", urlencoding::encode(&email.clone()), otp.clone())).unwrap();
sqlx::query!("INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?;
// TODO: send email
info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for);
Ok(())
}
pub async fn generate_session_token(user_id: i64, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> {
let token = format!("st-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;
Ok(token)
}
pub async fn validate_session_token(token: String, db: &PgPool) -> Result<i64, Box<dyn Error>> {
Ok(sqlx::query!("SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on > $2", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64)
}
pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool) -> Result<String, Box<dyn Error>> {
let token = format!("at-{}", Uuid::new_v4());
sqlx::query!("INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, session_id, user_id as i32).execute(db).await?;
Ok(token)
}
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box<dyn Error>> {
validate_session_token(session_id.clone(), db).await?;
sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
Ok(())
}
/*
CREATE TABLE totp_create_tokens (
id VARCHAR(39) NOT NULL PRIMARY KEY,
expires_on INTEGER NOT NULL,
totp_otpurl VARCHAR(3000) NOT NULL,
totp_secret VARCHAR(128) NOT NULL
);
*/
pub async fn create_totp_token(email: String, db: &PgPool, config: &TFConfig) -> Result<(String, TOTP), Box<dyn Error>> {
// create the TOTP parameters
let secret = Secret::generate_secret();
let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();
let otpurl = totpmachine.get_url();
let otpsecret = totpmachine.get_secret_base32();
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() + config.totp_verification_valid_for) as i32, otpurl, otpsecret).execute(db).await?;
Ok((otpid, totpmachine))
}
pub async fn verify_totp_token(otpid: String, email: String, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
let totprow = sqlx::query!("SELECT * FROM totp_create_tokens WHERE id = $1 AND expires_on > $2", otpid, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?;
let secret = Secret::Encoded(totprow.totp_secret);
let totpmachine = TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), email).unwrap();
if totpmachine.get_url() != totprow.totp_otpurl {
return Err("OTPURLs do not match (email does not match?)".into())
}
Ok(totpmachine)
}
pub async fn use_totp_token(otpid: String, email: String, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
let totpmachine = verify_totp_token(otpid.clone(), email.clone(), db).await?;
sqlx::query!("DELETE FROM totp_create_tokens WHERE id = $1", otpid).execute(db).await?;
sqlx::query!("UPDATE users SET totp_otpurl = $1, totp_secret = $2, totp_verified = 1 WHERE email = $3", totpmachine.get_url(), totpmachine.get_secret_base32(), email).execute(db).await?;
Ok(totpmachine)
}
pub async fn get_totpmachine(user: i32, db: &PgPool) -> Result<TOTP, Box<dyn Error>> {
let user = sqlx::query!("SELECT totp_secret, totp_otpurl, email FROM users WHERE id = $1", user).fetch_one(db).await?;
let secret = Secret::Encoded(user.totp_secret);
Ok(TOTP::new(TOTP_ALGORITHM, TOTP_DIGITS, TOTP_SKEW, TOTP_STEP, secret.to_bytes().unwrap(), Some(TOTP_ISSUER.to_string()), user.email).unwrap())
}
pub async fn user_has_totp(user: i32, db: &PgPool) -> Result<bool, Box<dyn Error>> {
Ok(sqlx::query!("SELECT totp_verified FROM users WHERE id = $1", user).fetch_one(db).await?.totp_verified == 1)
}

View File

@ -1,31 +0,0 @@
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
// Copyright (C) 2023 c0repwn3r
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use base64::Engine;
use totp_rs::Algorithm;
pub const TOTP_ALGORITHM: Algorithm = Algorithm::SHA1;
pub const TOTP_DIGITS: usize = 6;
pub const TOTP_SKEW: u8 = 1;
pub const TOTP_STEP: u64 = 30;
pub const TOTP_ISSUER: &str = "trifidapi";
pub fn base64decode(val: &str) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(val)
}
pub fn base64encode(val: Vec<u8>) -> String {
base64::engine::general_purpose::STANDARD.encode(val)
}