Merge remote-tracking branch 'origin/feat-new-api'

# Conflicts:
#	.gitignore
#	Cargo.lock
#	trifid-api/Cargo.toml
This commit is contained in:
c0repwn3r 2023-05-11 17:19:03 -04:00
commit 224e3680e0
Signed by: core
GPG Key ID: FDBF740DADDCEECF
111 changed files with 9283 additions and 4047 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
target target
pg_data pg_data
.idea .idea
tfclient/tmpexec.bin

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true"> <component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="trifidapi@localhost" uuid="39c81b89-3fc4-493f-b203-7a00527cffe6"> <data-source source="LOCAL" name="trifid@localhost" uuid="39c81b89-3fc4-493f-b203-7a00527cffe6">
<driver-ref>postgresql</driver-ref> <driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize> <synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver> <jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/trifidapi</jdbc-url> <jdbc-url>jdbc:postgresql://localhost:5432/trifid</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir> <working-dir>$ProjectFileDir$</working-dir>
</data-source> </data-source>
</component> </component>

View File

@ -6,6 +6,8 @@
<sourceFolder url="file://$MODULE_DIR$/trifid-api/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/trifid-api/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/trifid-pki/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/trifid-pki/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/dnapi-rs/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/dnapi-rs/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/trifid-api/trifid_api_entities/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/trifid-api/trifid_api_migration/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />

1739
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
[workspace] [workspace]
members = [ members = [
"trifid-api", "trifid-api",
"trifid-api/trifid_api_migration",
"trifid-api/trifid_api_entities",
"tfclient", "tfclient",
"trifid-pki", "trifid-pki",
"dnapi-rs" "dnapi-rs"

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/trifid

1541
trifid-api/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,23 +6,27 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
rocket = { version = "0.5.0-rc.2", features = ["json"] } actix-web = "4" # Web framework
base64 = "0.21.0" actix-request-identifier = "4" # Web framework
log = "0.4.17"
sqlx = { version = "0.6", features = [ "runtime-tokio-native-tls" , "postgres" ] } serde = { version = "1", features = ["derive"] } # Serialization and deserialization
tokio = { version = "1", features = ["full"] } serde_json = "1.0.95" # Serialization and deserialization (cursors)
toml = "0.7.1"
serde = "1.0.152" once_cell = "1" # Config
dotenvy = "0.15.6" toml = "0.7" # Config / Serialization and deserialization
paste = "1.0.11"
totp-rs = { version = "4.2.0", features = ["qr", "otpauth", "gen_secret"]} log = "0.4" # Logging
uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics"]} simple_logger = "4" # Logging
url = { version = "2.3.1", features = ["serde"] }
urlencoding = "2.1.2" sea-orm = { version = "^0", features = [ "sqlx-postgres", "runtime-actix-rustls", "macros" ]} # Database
chrono = "0.4.23" trifid_api_migration = { version = "0.1.0", path = "trifid_api_migration" } # Database
aes-gcm = "0.10.1" trifid_api_entities = { version = "0.1.0", path = "trifid_api_entities" } # Database
hex = "0.4.3"
rand = "0.8.5" rand = "0.8" # Misc.
trifid-pki = { version = "0.1", path = "../trifid-pki" } hex = "0.4" # Misc.
sha2 = "0.10.6" totp-rs = { version = "5.0.1", features = ["gen_secret", "otpauth"] } # Misc.
ipnet = { version = "2.7.1", features = ["serde"] } base64 = "0.21.0" # Misc.
chrono = "0.4.24" # Misc.
trifid-pki = { version = "0.1.9" } # Cryptography
aes-gcm = "0.10.1" # Cryptography

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() { 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,6 +1,6 @@
################################## ##########################
# trifid-api example config file # # trifid-api config file #
################################## ##########################
# trifid-api, an open source reimplementation of the Defined Networking nebula management server. # trifid-api, an open source reimplementation of the Defined Networking nebula management server.
# Copyright (C) 2023 c0repwn3r # Copyright (C) 2023 c0repwn3r
# #
@ -20,50 +20,99 @@
# Please read this file in it's entirety to learn what options you do or don't need to change # 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. # to get a functional trifid-api instance.
# What port should the API server listen on? #### [database] ####
# e.g. 8000 would mean the server is reachable at localhost:8000. # Options related to the PostgreSQL database connection.
# You probably don't need to change this. [database]
listen_port = 8000 # The PostgreSQL connection URL to connect to the database.
# Example: postgres://username:password@ip:port/database-name.
# The database provided must exist. Database migrations will be run automatically upon database startup.
# Url. Required.
url = "your-database-url-here"
# What is the postgres connection url to connect to the database? # The maximum number of connections that will be established to the database.
# Example: postgres://username:password@database_host/database_name # This will effectively mean the amount of requests that trifid-api can process in parallel, as almost every
# You absolutely need to change this. # request handler acquires a connection from the pool.
db_url = "postgres://postgres@localhost/trifidapi" # Integer. Optional. Default: 100
# max_connections = 100
# What is the externally accessible URL of this instance? # The minimum number of connections that will be established to the database.
# If you are running behind a reverse proxy, or a domain name, or similar, # At least this number of connections will be created and kept idle until needed. If requests have a lot of latency
# you need to set this to the URL that the web UI can make requests to. # due to acquiring connections from the database, raise this number.
# e.g. http://localhost:8000 # Integer. Optional. Default = 5
# Reminder: this ip needs to be internet-accessible. # min_connections = 5
# You absolutely need to change this.
base = "http://localhost:8000"
# What is the externally accessible URL of the **web ui** for this instance? # The maximum amount of time (in seconds) that the database pool will wait in order to connect to the database.
# This URL will be used to generate magic links, and needs to be correct. # After this amount of time, the connection will return an error and trifid-api will exit. If you have a very high-latency
# You absolutely need to change this. # database connection, raise this number.
web_root = "http://localhost:5173" # Integer. Optional. Default = 8
# connect_timeout = 8
# How long should magic links be valid for (in seconds)? # The maximum amount of time (in seconds) that the database pool will wait in order to acquire a connection from the database pool.
# You probably don't need to change this, 86400 (24 hours) is a sane default. # After this amount of time, the connection will return an error and trifid-api will exit. If you have a very high-latency
magic_links_valid_for = 86400 # database connection, raise this number.
# Integer. Optional. Default = 8
# acquire_timeout = 8
# How long should session tokens be valid for (in seconds)? # The maximum amount of time (in seconds) that a database connection will remain idle before the connection is closed.
# This controls how long a user can go without requesting a new "magic link" to re-log-in. # This only applies if closing this connection would not bring the number of connections below min_connections.
# This is a completley independent timer than `totp_verification_valid_for` - the auth token can (and often will) expire # Unless you are handling thousands of requests per second, you probably don't need to change this value.
# while the session token remains completley valid. # Integer. Optional. Default = 8
# You probably don't need to change this, 86400 (24 hours) is a sane default. # idle_timeout = 8
session_tokens_valid_for = 86400
# How long should 2FA authentication be valid for (in seconds)? # The maximum amount of time (in seconds) that a database connection will remain active before it is closed and replaced with a new connection.
# This controls how long a user can remain logged in without having to re-do the 2FA authentication process. # It is unlikely you ever need to change this value, unless your database takes 5 or more seconds per query, in which case you
# This is a completley independent timer than `session_tokens_valid_for` - the session token can expire while the 2FA token # need a better database.
# remains completley valid. # Integer. Optional. Default = 8
# You probably don't need to change this, 3600 (1 hour) is a sane default. # max_lifetime = 8
totp_verification_valid_for = 3600
# Should sqlx query logging be enabled?
# Disable this if you are tired of the constant query spam in the logs. Enable for debugging.
# Boolean. Optional. Default = true
# sqlx_logging = true
#### [server] ####
# Configure options for the trifid-api HTTP server.
[server]
# What IPs and ports should the trifid-api server listen on?
# This may need to be changed if you want to bind on a different port or interface.
# SocketAddr. Optional. Default = 0.0.0.0:8080 (all IPs, port 8080)
# bind = "0.0.0.0:8080"
#### [tokens] ####
# Configure options related to the various tokens that may be issued by the trifid-api server.
[tokens]
# How long (in seconds) should magic link tokens be valid for?
# This controls how long links sent to user's email addresses will remain valid for login.
# The default of 3600 (1 hour) is a sane default and you likely do not need to change this.
# Integer. Optional. Default = 3600
# magic_link_expiry_time_seconds = 3600 # 1 hour
# How long (in seconds) should session tokens be valid for?
# This controls how long it will take before a user will need to re-log in with a magic link, if they do not explicitly
# log out first.
# The default of 15780000 (6 months) is a sane default and you likely do not need to change this.
# Integer. Optional. Default = 15780000
# session_token_expiry_time_seconds = 15780000 # 6 months
# How long (in seconds) should TOTP setup tokens be valid for?
# This controls how long a user will have to setup TOTP after starting the setup process before the token is invalidated
# and they need to try again.
# The default of 600 (10 minutes) is a sane default and you likely do not need to change this.
# Integer. Optional. Default = 600
# totp_setup_timeout_time_seconds = 600 # 10 minutes
# How long (in seconds) should MFA auth tokens be valid for?
# This controls how long a user will remain logged in before they need to re-input their 2FA code..
# The default of 600 (10 minutes) is a sane default and you likely do not need to change this.
# Integer. Optional. Default = 600
# mfa_tokens_expiry_time_seconds = 600 # 10 minutes
#### [crypto] ####
# Configure settings related to the cryptography used inside trifid-api
[crypto]
# The per-instance data encryption key to protect sensitive data in the instance. # 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. # 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. # 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 # If you get "InvalidLength" errors while trying to do anything involving organizations, that indicates that this
# value was improperly generated. # value was improperly generated.
@ -71,11 +120,4 @@ totp_verification_valid_for = 3600
# ------- WARNING ------- # ------- WARNING -------
# Do not change this value in a production instance. It will make existing data inaccessible until changed back. # Do not change this value in a production instance. It will make existing data inaccessible until changed back.
# ------- WARNING ------- # ------- WARNING -------
data_key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2" data-key = "edd600bcebea461381ea23791b6967c8667e12827ac8b94dc022f189a5dc59a2"
# How long should CA certs be valid for before they need to be replaced (in seconds)?
# This controls the maximum amount of time a network on this instance can go
# without a rekey.
# You probably don't need to change, this, 31536000 (1 year) is a sane default.
# This value only affects new certs signed by this instance.
ca_certs_valid_for = 31536000

View File

@ -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

@ -0,0 +1,206 @@
// 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 actix_web::HttpRequest;
use std::error::Error;
use crate::timers::expired;
use crate::tokens::get_token_type;
use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter};
use trifid_api_entities::entity::api_key;
use trifid_api_entities::entity::api_key_scope;
use trifid_api_entities::entity::user;
use trifid_api_entities::entity::{auth_token, session_token};
pub enum TokenInfo {
SessionToken(SessionTokenInfo),
AuthToken(AuthTokenInfo),
ApiToken(ApiTokenInfo),
NotPresent,
}
pub struct SessionTokenInfo {
pub token: String,
pub user: SessionTokenUser,
pub expires_at: i64,
}
pub struct SessionTokenUser {
pub id: String,
pub email: String,
}
pub struct ApiTokenInfo {
pub scopes: Vec<String>,
pub organization: String,
}
pub struct AuthTokenInfo {
pub token: String,
pub session_info: SessionTokenInfo,
}
pub async fn enforce_session(
req: &HttpRequest,
db: &DatabaseConnection,
) -> Result<TokenInfo, Box<dyn Error>> {
let header = req
.headers()
.get("Authorization")
.ok_or("Missing authorization header")?;
let authorization = header.to_str()?;
let authorization_split: Vec<&str> = authorization.split(' ').collect();
if authorization_split[0] != "Bearer" {
return Err("Not a bearer token".into());
}
let tokens = &authorization_split[1..];
let sess_token = tokens
.iter()
.find(|i| get_token_type(i).unwrap_or("n-sess") == "sess")
.copied()
.ok_or("Missing session token")?;
let token: session_token::Model = session_token::Entity::find()
.filter(session_token::Column::Id.eq(sess_token))
.one(db)
.await?
.ok_or("Invalid session token")?;
if expired(token.expires_on as u64) {
return Err("Token expired".into());
}
let user: user::Model = user::Entity::find()
.filter(user::Column::Id.eq(token.user))
.one(db)
.await?
.ok_or("Session token has a nonexistent user")?;
Ok(TokenInfo::SessionToken(SessionTokenInfo {
token: token.id,
user: SessionTokenUser {
id: user.id,
email: user.email,
},
expires_at: token.expires_on,
}))
}
pub async fn enforce_2fa(
req: &HttpRequest,
db: &DatabaseConnection,
) -> Result<TokenInfo, Box<dyn Error>> {
let session_data = match enforce_session(req, db).await? {
TokenInfo::SessionToken(i) => i,
_ => unreachable!(),
};
let header = req
.headers()
.get("Authorization")
.ok_or("Missing authorization header")?;
let authorization = header.to_str()?;
let authorization_split: Vec<&str> = authorization.split(' ').collect();
if authorization_split[0] != "Bearer" {
return Err("Not a bearer token".into());
}
let tokens = &authorization_split[1..];
let auth_token = tokens
.iter()
.find(|i| get_token_type(i).unwrap_or("n-auth") == "auth")
.copied()
.ok_or("Missing auth token")?;
let token: auth_token::Model = auth_token::Entity::find()
.filter(auth_token::Column::Id.eq(auth_token))
.one(db)
.await?
.ok_or("Invalid session token")?;
if expired(token.expires_on as u64) {
return Err("Token expired".into());
}
Ok(TokenInfo::AuthToken(AuthTokenInfo {
token: token.id,
session_info: session_data,
}))
}
pub async fn enforce_api_token(
req: &HttpRequest,
scopes: &[&str],
db: &DatabaseConnection,
) -> Result<TokenInfo, Box<dyn Error>> {
let header = req
.headers()
.get("Authorization")
.ok_or("Missing authorization header")?;
let authorization = header.to_str()?;
let authorization_split: Vec<&str> = authorization.split(' ').collect();
if authorization_split[0] != "Bearer" {
return Err("Not a bearer token".into());
}
let tokens = &authorization_split[1..];
let api_token = tokens
.iter()
.find(|i| get_token_type(i).unwrap_or("n-tfkey") == "tfkey")
.copied()
.ok_or("Missing api token")?;
// API tokens are special and have a different form than other keys.
// They follow the form:
// tfkey-[ID]-[TOKEN]
let api_token_split: Vec<&str> = api_token.split('-').collect();
if api_token_split.len() != 3 {
return Err("API token is missing key".into());
}
let token_id = format!("{}-{}", api_token_split[0], api_token_split[1]);
let token_key = api_token_split[2].to_string();
let token: api_key::Model = api_key::Entity::find()
.filter(
Condition::all()
.add(api_key::Column::Id.eq(token_id))
.add(api_key::Column::Key.eq(token_key)),
)
.one(db)
.await?
.ok_or("Invalid api token")?;
let token_scopes: Vec<api_key_scope::Model> = api_key_scope::Entity::find()
.filter(api_key_scope::Column::ApiKey.eq(api_token))
.all(db)
.await?;
let token_scopes: Vec<&str> = token_scopes.iter().map(|i| i.scope.as_str()).collect();
for scope in scopes {
if !token_scopes.contains(scope) {
return Err(format!("API token is missing scope {}", scope).into());
}
}
Ok(TokenInfo::ApiToken(ApiTokenInfo {
scopes: token_scopes.iter().map(|i| i.to_string()).collect(),
organization: token.organization,
}))
}

View File

@ -14,18 +14,109 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use serde::Deserialize; use log::error;
use url::Url; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::fs;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
#[derive(Deserialize)] pub static CONFIG: Lazy<TrifidConfig> = Lazy::new(|| {
pub struct TFConfig { let config_str = match fs::read_to_string("/etc/trifid/config.toml") {
pub listen_port: u16, Ok(str) => str,
pub db_url: String, Err(e) => {
pub base: Url, error!("Unable to read config file: {}", e);
pub web_root: Url, std::process::exit(1);
pub magic_links_valid_for: u64, }
pub session_tokens_valid_for: u64, };
pub totp_verification_valid_for: u64,
pub data_key: String, match toml::from_str(&config_str) {
pub ca_certs_valid_for: u64 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,
pub server: TrifidConfigServer,
pub tokens: TrifidConfigTokens,
pub crypto: TrifidConfigCryptography,
} }
#[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,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrifidConfigServer {
#[serde(default = "socketaddr_8080")]
pub bind: SocketAddr,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrifidConfigTokens {
#[serde(default = "magic_link_expiry_time")]
pub magic_link_expiry_time_seconds: u64,
#[serde(default = "session_token_expiry_time")]
pub session_token_expiry_time_seconds: u64,
#[serde(default = "totp_setup_timeout_time")]
pub totp_setup_timeout_time_seconds: u64,
#[serde(default = "mfa_tokens_expiry_time")]
pub mfa_tokens_expiry_time_seconds: u64,
#[serde(default = "enrollment_tokens_expiry_time")]
pub enrollment_tokens_expiry_time: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrifidConfigCryptography {
pub data_encryption_key: String,
}
fn max_connections_default() -> u32 {
100
}
fn min_connections_default() -> u32 {
5
}
fn time_defaults() -> u64 {
8
}
fn sqlx_logging_default() -> bool {
true
}
fn socketaddr_8080() -> SocketAddr {
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from([0, 0, 0, 0]), 8080))
}
fn magic_link_expiry_time() -> u64 {
3600
} // 1 hour
fn session_token_expiry_time() -> u64 {
15780000
} // 6 months
fn totp_setup_timeout_time() -> u64 {
600
} // 10 minutes
fn mfa_tokens_expiry_time() -> u64 {
600
} // 10 minutes
fn enrollment_tokens_expiry_time() -> u64 {
600
} // 10 minutes

View File

@ -14,25 +14,33 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error; use crate::config::TrifidConfig;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::{Aead, Payload}; use aes_gcm::aead::{Aead, Payload};
use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use rand::Rng; use rand::Rng;
use rand::rngs::OsRng; use std::error::Error;
use crate::config::TFConfig; use trifid_pki::rand_core::OsRng;
pub fn get_cipher_from_config(config: &TFConfig) -> Result<Aes256Gcm, Box<dyn Error>> { pub fn get_cipher_from_config(config: &TrifidConfig) -> Result<Aes256Gcm, Box<dyn Error>> {
let key_slice = hex::decode(&config.data_key)?; let key_slice = hex::decode(&config.crypto.data_encryption_key)?;
Ok(Aes256Gcm::new_from_slice(&key_slice)?) 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> { 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 nonce = Nonce::from_slice(&nonce);
let ciphertext = cipher.encrypt(nonce, plaintext)?; let ciphertext = cipher.encrypt(nonce, plaintext)?;
Ok(ciphertext) Ok(ciphertext)
} }
pub fn decrypt_with_nonce(ciphertext: &[u8], nonce: [u8; 12], cipher: &Aes256Gcm) -> Result<Vec<u8>, aes_gcm::Error> { 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 nonce = Nonce::from_slice(&nonce);
let plaintext = cipher.decrypt(nonce, Payload::from(ciphertext))?; let plaintext = cipher.decrypt(nonce, Payload::from(ciphertext))?;
Ok(plaintext) Ok(plaintext)

54
trifid-api/src/cursor.rs Normal file
View File

@ -0,0 +1,54 @@
// 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 serde::{Deserialize, Serialize};
use std::error::Error;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Cursor {
pub page: u64,
}
impl TryFrom<Cursor> for String {
type Error = Box<dyn Error>;
fn try_from(value: Cursor) -> Result<Self, Self::Error> {
// Serialize it to json
let json_str = serde_json::to_string(&value)?;
// Then base64-encode the json
let base64_str = base64::engine::general_purpose::STANDARD.encode(json_str);
Ok(base64_str)
}
}
impl TryFrom<String> for Cursor {
type Error = Box<dyn Error>;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
// If empty, it's page 0
return Ok(Cursor { page: 0 });
}
// Base64-decode the value
let json_bytes = base64::engine::general_purpose::STANDARD.decode(value)?;
// Convert it into a string
let json_str = String::from_utf8(json_bytes)?;
// Deserialize it from json
let cursor = serde_json::from_str(&json_str)?;
Ok(cursor)
}
}

132
trifid-api/src/error.rs Normal file
View File

@ -0,0 +1,132 @@
// 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 actix_web::error::{JsonPayloadError, PayloadError};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct APIErrorsResponse {
pub errors: Vec<APIError>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct APIError {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "is_none")]
#[serde(default)]
pub path: Option<String>,
}
fn is_none<T>(o: &Option<T>) -> bool {
o.is_none()
}
impl From<&JsonPayloadError> for APIError {
fn from(value: &JsonPayloadError) -> Self {
match value {
JsonPayloadError::OverflowKnownLength { length, limit } => {
APIError {
code: "ERR_PAYLOAD_OVERFLOW_KNOWN_LENGTH".to_string(),
message: format!("Payload size is bigger than allowed & content length header set. (length: {}, limit: {})", length, limit),
path: None
}
},
JsonPayloadError::Overflow { limit } => {
APIError {
code: "ERR_PAYLOAD_OVERFLOW".to_string(),
message: format!("Payload size is bigger than allowed but no content-length header is set. (limit: {})", limit),
path: None
}
},
JsonPayloadError::ContentType => {
APIError {
code: "ERR_NOT_JSON".to_string(),
message: "Content-Type header not set to expected application/json".to_string(),
path: None,
}
},
JsonPayloadError::Deserialize(e) => {
APIError {
code: "ERR_JSON_DESERIALIZE".to_string(),
message: format!("Error deserializing JSON: {}", e),
path: None,
}
},
JsonPayloadError::Serialize(e) => {
APIError {
code: "ERR_JSON_SERIALIZE".to_string(),
message: format!("Error serializing JSON: {}", e),
path: None,
}
},
JsonPayloadError::Payload(e) => {
e.into()
},
_ => {
APIError {
code: "ERR_UNKNOWN_ERROR".to_string(),
message: "An unknown error has occured".to_string(),
path: None,
}
}
}
}
}
impl From<&PayloadError> for APIError {
fn from(value: &PayloadError) -> Self {
match value {
PayloadError::Incomplete(e) => APIError {
code: "ERR_UNEXPECTED_EOF".to_string(),
message: match e {
None => "Payload reached EOF but was incomplete".to_string(),
Some(e) => format!("Payload reached EOF but was incomplete: {}", e),
},
path: None,
},
PayloadError::EncodingCorrupted => APIError {
code: "ERR_CORRUPTED_PAYLOAD".to_string(),
message: "Payload content encoding corrupted".to_string(),
path: None,
},
PayloadError::Overflow => APIError {
code: "ERR_PAYLOAD_OVERFLOW".to_string(),
message: "Payload reached size limit".to_string(),
path: None,
},
PayloadError::UnknownLength => APIError {
code: "ERR_PAYLOAD_UNKNOWN_LENGTH".to_string(),
message: "Unable to determine payload length".to_string(),
path: None,
},
PayloadError::Http2Payload(e) => APIError {
code: "ERR_HTTP2_ERROR".to_string(),
message: format!("HTTP/2 error: {}", e),
path: None,
},
PayloadError::Io(e) => APIError {
code: "ERR_IO_ERROR".to_string(),
message: format!("I/O error: {}", e),
path: None,
},
_ => APIError {
code: "ERR_UNKNOWN_ERROR".to_string(),
message: "An unknown error has occured".to_string(),
path: None,
},
}
}
}

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

@ -14,4 +14,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod whoami; use log::info;
use std::error::Error;
pub fn send_magic_link(token: &str) -> Result<(), Box<dyn Error>> {
// TODO: actually do this
info!("sent magic link {}", token);
Ok(())
}

View File

@ -14,200 +14,99 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
extern crate core; use actix_request_identifier::RequestIdentifier;
use actix_web::{
web::{Data, JsonConfig},
App, HttpResponse, HttpServer,
};
use log::{info, Level};
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use std::error::Error; use std::error::Error;
use std::fs; use std::time::Duration;
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;
pub mod format; use crate::config::CONFIG;
pub mod util; use crate::error::{APIError, APIErrorsResponse};
pub mod db; use crate::tokens::random_id_no_id;
use trifid_api_migration::{Migrator, MigratorTrait};
pub mod auth_tokens;
pub mod config; pub mod config;
pub mod tokens;
pub mod routes;
pub mod auth;
pub mod crypto; pub mod crypto;
pub mod org; pub mod cursor;
pub mod kv; pub mod error;
pub mod role; pub mod magic_link;
pub mod routes;
pub mod timers;
pub mod tokens;
static MIGRATOR: Migrator = sqlx::migrate!(); pub struct AppState {
pub conn: DatabaseConnection,
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>> { 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() { let db = Database::connect(opt).await?;
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 config_file = if Path::new("config.toml").exists() { info!("Performing database migration...");
"config.toml".to_string() Migrator::up(&db, None).await?;
} else {
std::env::var("CONFIG_FILE").unwrap()
};
let config_data = match fs::read_to_string(&config_file) { let data = Data::new(AppState { conn: db });
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) { HttpServer::new(move || {
Ok(c) => c, App::new()
Err(e) => { .app_data(data.clone())
error!("[tfapi] fatal: unable to parse config from {}", config_file); .app_data(JsonConfig::default().error_handler(|err, _req| {
error!("[tfapi] fatal: {}", e); let api_error: APIError = (&err).into();
std::process::exit(1); actix_web::error::InternalError::from_response(
} err,
}; HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![api_error],
info!("[tfapi] connecting to database pool"); }),
)
let pool = match PgPoolOptions::new().max_connections(5).connect(&config.db_url).await { .into()
Ok(p) => p, }))
Err(e) => { .wrap(RequestIdentifier::with_generator(random_id_no_id))
error!("[tfapi] fatal: unable to connect to database pool"); .service(routes::v1::auth::magic_link::magic_link_request)
error!("[tfapi] fatal: {}", e); .service(routes::v1::signup::signup_request)
std::process::exit(1); .service(routes::v1::auth::verify_magic_link::verify_magic_link_request)
} .service(routes::v1::totp_authenticators::totp_authenticators_request)
}; .service(routes::v1::verify_totp_authenticators::verify_totp_authenticators_request)
.service(routes::v1::auth::totp::totp_request)
info!("[tfapi] running database migrations"); .service(routes::v1::networks::get_networks)
.service(routes::v1::organization::create_org_request)
MIGRATOR.run(&pool).await?; .service(routes::v1::networks::get_network_request)
.service(routes::v1::roles::create_role_request)
info!("[tfapi] verifying encryption key"); .service(routes::v1::roles::get_roles)
.service(routes::v1::roles::get_role)
let kv_hash = kv_get("pmk_hash", &pool).await.expect("Unable to get pmk hash from kv store"); .service(routes::v1::roles::delete_role)
.service(routes::v1::roles::update_role_request)
let mut hasher = Sha256::new(); .service(routes::v1::trifid::trifid_extensions)
hasher.update(config.data_key.as_bytes()); .service(routes::v1::hosts::get_hosts)
let config_hash = hex::encode(hasher.finalize()); .service(routes::v1::hosts::create_hosts_request)
if let Some(k_hash) = kv_hash { .service(routes::v1::hosts::get_host)
if config_hash != k_hash { .service(routes::v1::hosts::delete_host)
error!("[tfapi] fatal: instance master key does not match key used to encrypt data"); .service(routes::v1::hosts::edit_host)
error!("[tfapi] fatal: datastore was encrypted with keyid {k_hash}"); .service(routes::v1::hosts::block_host)
error!("[tfapi] fatal: the key in your config has the keyid {config_hash}"); .service(routes::v1::hosts::enroll_host)
error!("[tfapi] fatal: you probably changed data_key. please return it to it's original value"); .service(routes::v1::hosts::create_host_and_enrollment_code)
std::process::exit(1); })
} else { .bind(CONFIG.server.bind)?
info!("[tfapi] data keyid is {config_hash}"); .run()
} .await?;
} 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(()) 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 +1 @@
// 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 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

@ -13,66 +13,118 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//#POST /v1/auth/magic-link t+parity:full t+type:reverse_engineered t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been reverse-engineered from the original API as the original API docs do not have this item.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use rocket::{post, State}; use crate::config::CONFIG;
use rocket::serde::json::Json; use crate::error::{APIError, APIErrorsResponse};
use serde::{Serialize, Deserialize}; use crate::magic_link::send_magic_link;
use rocket::http::{ContentType, Status}; use crate::timers::expires_in_seconds;
use sqlx::PgPool; use crate::tokens::random_token;
use crate::config::TFConfig; use crate::AppState;
use crate::tokens::send_magic_link; use actix_web::web::{Data, Json};
use rocket::options; use actix_web::{post, HttpResponse};
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::user;
use trifid_api_entities::entity::user::Entity as UserEntity;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkRequest { pub struct MagicLinkRequest {
pub email: String, pub email: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct MagicLinkResponse { pub struct MagicLinkResponse {
pub data: Option<String>, pub data: MagicLinkResponseData,
pub metadata: MagicLinkResponseMetadata, pub metadata: MagicLinkResponseMetadata,
} }
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MagicLinkResponseData {}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MagicLinkResponseMetadata {}
#[post("/v1/auth/magic-link")]
#[options("/v1/auth/magic-link")] pub async fn magic_link_request(data: Data<AppState>, req: Json<MagicLinkRequest>) -> HttpResponse {
pub async fn options() -> &'static str { let user: Option<user::Model> = match UserEntity::find()
"" .filter(user::Column::Email.eq(&req.email))
} .one(&data.conn)
.await
#[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)> { Ok(r) => r,
// 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) => { Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e))) error!("database error: {}", e);
} return HttpResponse::InternalServerError().json(APIErrorsResponse {
} errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
if id == -1 { message:
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_UNAUTHORIZED", "authorization was provided but it is expired or invalid"))) "There was an error with the database request, please try again later."
} .to_string(),
path: None,
// 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 let user = match user {
// 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) Some(u) => u,
Ok((ContentType::JSON, Json(MagicLinkResponse { None => {
data: None, return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_USER_DOES_NOT_EXIST".to_string(),
message: "That user does not exist.".to_string(),
path: None,
}],
})
}
};
let model = trifid_api_entities::entity::magic_link::Model {
id: random_token("ml"),
user: user.id,
expires_on: expires_in_seconds(CONFIG.tokens.magic_link_expiry_time_seconds) as i64,
};
match send_magic_link(&model.id) {
Ok(_) => (),
Err(e) => {
error!("error sending magic link: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_ML_ERROR".to_string(),
message:
"There was an error sending the magic link email, please try again later."
.to_string(),
path: None,
}],
});
}
}
let active_model = model.into_active_model();
match active_model.insert(&data.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
}
HttpResponse::Ok().json(MagicLinkResponse {
data: MagicLinkResponseData {},
metadata: MagicLinkResponseMetadata {}, metadata: MagicLinkResponseMetadata {},
}))) })
} }

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/>.
pub mod verify_magic_link;
pub mod magic_link; pub mod magic_link;
pub mod totp; pub mod totp;
pub mod check_session; pub mod verify_magic_link;

View File

@ -13,77 +13,170 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//#POST /v1/auth/totp t+parity:full t+type:reverse_engineered t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been reverse-engineered from the original API as the original API docs do not have this item.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use rocket::http::{ContentType, Status}; use crate::auth_tokens::{enforce_session, TokenInfo};
use rocket::serde::json::Json; use crate::error::{APIError, APIErrorsResponse};
use crate::auth::PartialUserInfo; use crate::AppState;
use serde::{Serialize, Deserialize}; use actix_web::web::{Data, Json};
use rocket::{post, State}; use actix_web::{post, HttpRequest, HttpResponse};
use sqlx::PgPool; use log::{debug, error};
use rocket::options; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use crate::tokens::{generate_auth_token, get_totpmachine, user_has_totp}; use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::totp_authenticator;
pub const TOTP_GENERIC_UNAUTHORIZED_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_INVALID_TOTP_CODE\",\"message\":\"invalid TOTP code (maybe it expired?)\",\"path\":\"code\"}]}"; use crate::config::CONFIG;
pub const TOTP_NO_TOTP_ERROR: &str = "{\"errors\":[{\"code\":\"ERR_NO_TOTP\",\"message\":\"logged-in user does not have totp enabled\",\"path\":\"code\"}]}"; use crate::timers::expires_in_seconds;
use crate::tokens::random_token;
use totp_rs::{Secret, TOTP};
use trifid_api_entities::entity::auth_token;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "rocket::serde")]
pub struct TotpRequest { pub struct TotpRequest {
pub code: String pub code: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "rocket::serde")] pub struct TotpResponse {
pub data: TotpResponseData,
pub metadata: TotpResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpResponseData { pub struct TotpResponseData {
#[serde(rename = "authToken")] #[serde(rename = "authToken")]
auth_token: String pub 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")] #[derive(Serialize, Deserialize, Debug, Clone)]
pub async fn options() -> &'static str { pub struct TotpResponseMetadata {}
""
}
#[post("/v1/auth/totp")]
#[post("/v1/auth/totp", data = "<req>")] pub async fn totp_request(
pub async fn totp_request(req: Json<TotpRequest>, user: PartialUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<TotpResponse>), (Status, String)> { req: Json<TotpRequest>,
if !match user_has_totp(user.user_id, db.inner()).await { req_data: HttpRequest,
Ok(b) => b, db: Data<AppState>,
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))) ) -> HttpResponse {
} { // require a user session
return Err((Status::UnprocessableEntity, TOTP_NO_TOTP_ERROR.to_string())) let session_token = match enforce_session(&req_data, &db.conn).await {
} Ok(r) => match r {
TokenInfo::SessionToken(i) => i,
if user.has_totp_auth { _ => unreachable!(),
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) => { 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))) error!("error enforcing session: {}", e);
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}],
});
} }
}; };
if !totpmachine.check_current(&req.0.code).unwrap_or(false) { // determine if the user has a totp authenticator
return Err((Status::Unauthorized, TOTP_GENERIC_UNAUTHORIZED_ERROR.to_string())) let auther = match totp_authenticator::Entity::find()
.filter(totp_authenticator::Column::User.eq(&session_token.user.id))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
};
let auther = match auther {
Some(a) => a,
None => {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_USER_NO_TOTP".to_string(),
message: "This user does not have a totp authenticator".to_string(),
path: None,
}],
});
}
};
let _secret = Secret::Encoded(auther.secret.clone());
let totpmachine = match TOTP::from_url(auther.url.clone()) {
Ok(m) => m,
Err(e) => {
error!("totp url error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_SECRET_ERROR".to_string(),
message: "There was an error parsing the totpmachine. Please try again later."
.to_string(),
path: None,
}],
});
}
};
let valid = match totpmachine.check_current(&req.code) {
Ok(valid) => valid,
Err(e) => {
error!("system time error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_TIME_ERROR".to_string(),
message: "There was an with the server-side time clock.".to_string(),
path: None,
}],
});
}
};
if !valid {
debug!("current: {}", totpmachine.generate_current().unwrap());
error!("user send invalid totp code");
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}],
});
} }
Ok((ContentType::JSON, Json(TotpResponse { let model: auth_token::Model = auth_token::Model {
data: TotpResponseData { auth_token: match generate_auth_token(user.user_id as i64, user.session_id, db.inner()).await { id: random_token("auth"),
Ok(t) => t, user: session_token.user.id,
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))) } expires_on: expires_in_seconds(CONFIG.tokens.mfa_tokens_expiry_time_seconds) as i64,
} }, };
let token = model.id.clone();
let active_model = model.into_active_model();
match active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error issuing the authentication token.".to_string(),
path: None,
}],
});
}
}
HttpResponse::Ok().json(TotpResponse {
data: TotpResponseData { auth_token: token },
metadata: TotpResponseMetadata {}, metadata: TotpResponseMetadata {},
}))) })
} }

View File

@ -13,79 +13,143 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//#POST /v1/auth/verify-magic-link t+parity:full t+type:reverse_engineered t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been reverse-engineered from the original API as the original API docs do not have this item.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use std::time::{SystemTime, UNIX_EPOCH}; use crate::config::CONFIG;
use rocket::http::{ContentType, Status}; use crate::error::{APIError, APIErrorsResponse};
use rocket::serde::json::Json; use crate::timers::{expired, expires_in_seconds};
use serde::{Serialize, Deserialize}; use crate::tokens::random_token;
use rocket::{post, State}; use crate::AppState;
use sqlx::PgPool; use actix_web::web::{Data, Json};
use crate::config::TFConfig; use actix_web::{post, HttpResponse};
use crate::tokens::generate_session_token; use log::error;
use rocket::options; use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter,
};
use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::magic_link;
use trifid_api_entities::entity::magic_link::Model;
use trifid_api_entities::entity::session_token;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(crate = "rocket::serde")]
pub struct VerifyMagicLinkRequest { pub struct VerifyMagicLinkRequest {
#[serde(rename = "magicLinkToken")] #[serde(rename = "magicLinkToken")]
pub magic_link_token: String, pub magic_link_token: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
#[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 struct VerifyMagicLinkResponse {
pub data: VerifyMagicLinkResponseData, pub data: VerifyMagicLinkResponseData,
pub metadata: VerifyMagicLinkResponseMetadata, pub metadata: VerifyMagicLinkResponseMetadata,
} }
#[options("/v1/auth/verify-magic-link")] #[derive(Serialize, Deserialize, Debug, Clone)]
pub async fn options() -> &'static str { pub struct VerifyMagicLinkResponseData {
"" #[serde(rename = "sessionToken")]
pub session_token: String,
} }
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyMagicLinkResponseMetadata {}
#[post("/v1/auth/verify-magic-link", data = "<req>")] #[post("/v1/auth/verify-magic-link")]
pub async fn verify_magic_link(req: Json<VerifyMagicLinkRequest>, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<VerifyMagicLinkResponse>), (Status, String)> { pub async fn verify_magic_link_request(
// get the current time to check if the token is expired db: Data<AppState>,
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 { req: Json<VerifyMagicLinkRequest>,
Ok(row) => (row.user_id, row.expires_on), ) -> HttpResponse {
let link: Option<Model> = match magic_link::Entity::find()
.filter(magic_link::Column::Id.eq(&req.magic_link_token))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => { Err(e) => {
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_UNAUTHORIZED", "this token is invalid", e))) error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
} }
}; };
let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32;
println!("expired on {}, currently {}", expired_at, current_time); let link = match link {
if expired_at < current_time { Some(l) => l,
return Err((Status::Unauthorized, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_UNAUTHORIZED", "valid authorization was provided but it is expired"))) None => {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}],
})
}
};
if expired(link.expires_on as u64) {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_EXPIRED".to_string(),
message: "Magic link token expired".to_string(),
path: None,
}],
});
} }
// generate session token let user = link.user.clone();
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 link.delete(&db.conn).await {
match sqlx::query!("DELETE FROM magic_links WHERE id = $1", req.0.magic_link_token).execute(db.inner()).await {
Ok(_) => (), Ok(_) => (),
Err(e) => { 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))) error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
} }
} }
Ok((ContentType::JSON, Json(VerifyMagicLinkResponse { let model = session_token::Model {
data: VerifyMagicLinkResponseData { session_token: token }, id: random_token("sess"),
user,
expires_on: expires_in_seconds(CONFIG.tokens.session_token_expiry_time_seconds) as i64,
};
let token = model.id.clone();
let active_model = model.into_active_model();
match active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
}
HttpResponse::Ok().json(VerifyMagicLinkResponse {
data: VerifyMagicLinkResponseData {
session_token: token,
},
metadata: VerifyMagicLinkResponseMetadata {}, 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,
})))
}

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,9 @@
// 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 auth;
pub mod signup; pub mod hosts;
pub mod networks;
pub mod totp_authenticators;
pub mod verify_totp_authenticator;
pub mod organization; pub mod organization;
pub mod user;
pub mod ca;
pub mod roles; pub mod roles;
pub mod signup;
pub mod totp_authenticators;
pub mod trifid;
pub mod verify_totp_authenticators;

View File

@ -0,0 +1,411 @@
// 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/>.
//
//#GET /v1/networks t+parity:full t+type:documented t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been recreated from the original API documentation.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
//
//#GET /v1/networks/{network_id} t+parity:full t+type:documented t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been recreated from the original API documentation.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo};
use crate::cursor::Cursor;
use crate::error::{APIError, APIErrorsResponse};
use crate::timers::TIME_FORMAT;
use crate::AppState;
use actix_web::web::{Data, Path, Query};
use actix_web::{get, HttpRequest, HttpResponse};
use chrono::{TimeZone, Utc};
use log::error;
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::network;
use trifid_api_entities::entity::organization;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponse {
pub data: Vec<GetNetworksResponseData>,
pub metadata: GetNetworksResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponseData {
pub id: String,
pub cidr: String,
#[serde(rename = "organizationID")]
pub organization_id: String,
#[serde(rename = "signingCAID")]
pub signing_ca_id: String,
#[serde(rename = "createdAt")]
pub created_at: String, // 2023-03-22T18:55:47.009Z
pub name: String,
#[serde(rename = "lighthousesAsRelays")]
pub lighthouses_as_relays: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponseMetadata {
#[serde(rename = "totalCount")]
pub total_count: u64,
#[serde(rename = "hasNextPage")]
pub has_next_page: bool,
#[serde(rename = "hasPrevPage")]
pub has_prev_page: bool,
#[serde(default, rename = "prevCursor")]
pub prev_cursor: Option<String>,
#[serde(default, rename = "nextCursor")]
pub next_cursor: Option<String>,
#[serde(default)]
pub page: Option<GetNetworksResponseMetadataPage>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponseMetadataPage {
pub count: u64,
pub start: u64,
}
fn u64_25() -> u64 {
25
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksQueryParams {
#[serde(default, rename = "includeCounts")]
pub include_counts: bool,
#[serde(default)]
pub cursor: String,
#[serde(default = "u64_25", rename = "pageSize")]
pub page_size: u64,
}
#[get("/v1/networks")]
pub async fn get_networks(
opts: Query<GetNetworksQueryParams>,
req_info: HttpRequest,
db: Data<AppState>,
) -> HttpResponse {
// For this endpoint, you either need to be a fully authenticated user OR a token with networks:list
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["networks:list"], &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
// If neither are present, throw an error
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "This endpoint requires either a fully authenticated user or a token with the networks:list scope".to_string(),
path: None,
}
],
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(),
message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(),
path: None
}
],
});
}
let org = match api_token_info {
TokenInfo::ApiToken(tkn) => tkn.organization,
_ => {
// we have a session token, which means we have to do a db request to get the organization that this user owns
let user = match session_info {
TokenInfo::AuthToken(tkn) => tkn.session_info.user,
_ => unreachable!(),
};
let org = match organization::Entity::find()
.filter(organization::Column::Owner.eq(user.id))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
};
if let Some(org) = org {
org.id
} else {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_NO_ORG".to_string(),
message: "This user does not own any organizations. Try using an API token instead.".to_string(),
path: None
}
],
});
}
}
};
let cursor: Cursor = match opts.cursor.clone().try_into() {
Ok(r) => r,
Err(e) => {
error!("invalid cursor: {}", e);
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_INVALID_CURSOR".to_string(),
message: "The provided cursor was invalid, please try again later.".to_string(),
path: None,
}],
});
}
};
let network_pages = network::Entity::find()
.filter(network::Column::Organization.eq(org))
.order_by_asc(network::Column::CreatedAt)
.paginate(&db.conn, opts.page_size);
let total = match network_pages.num_items().await {
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
};
let pages = match network_pages.num_pages().await {
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
};
let models = match network_pages.fetch_page(cursor.page).await {
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
};
let models_mapped: Vec<GetNetworksResponseData> = models
.iter()
.map(|u| GetNetworksResponseData {
id: u.id.clone(),
cidr: u.cidr.clone(),
organization_id: u.organization.clone(),
signing_ca_id: u.signing_ca.clone(),
created_at: Utc
.timestamp_opt(u.created_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
name: u.name.clone(),
lighthouses_as_relays: u.lighthouses_as_relays,
})
.collect();
let count = models_mapped.len() as u64;
HttpResponse::Ok().json(GetNetworksResponse {
data: models_mapped,
metadata: GetNetworksResponseMetadata {
total_count: total,
has_next_page: cursor.page + 1 != pages,
has_prev_page: cursor.page != 0,
prev_cursor: if cursor.page != 0 {
match (Cursor {
page: cursor.page - 1,
})
.try_into()
{
Ok(r) => Some(r),
Err(_) => None,
}
} else {
None
},
next_cursor: if cursor.page + 1 != pages {
match (Cursor {
page: cursor.page + 1,
})
.try_into()
{
Ok(r) => Some(r),
Err(_) => None,
}
} else {
None
},
page: if opts.include_counts {
Some(GetNetworksResponseMetadataPage {
count,
start: opts.page_size * cursor.page,
})
} else {
None
},
},
})
}
#[get("/v1/networks/{network_id}")]
pub async fn get_network_request(
net: Path<String>,
req_info: HttpRequest,
db: Data<AppState>,
) -> HttpResponse {
// For this endpoint, you either need to be a fully authenticated user OR a token with networks:list
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["networks:read"], &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
// If neither are present, throw an error
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "This endpoint requires either a fully authenticated user or a token with the networks:read scope".to_string(),
path: None,
}
],
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(),
message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(),
path: None
}
],
});
}
let network: Option<network::Model> = match network::Entity::find()
.filter(network::Column::Id.eq(net.into_inner()))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
};
if let Some(network) = network {
HttpResponse::Ok().json(GetNetworkResponse {
data: GetNetworksResponseData {
id: network.id,
cidr: network.cidr,
organization_id: network.organization,
signing_ca_id: network.signing_ca,
created_at: Utc
.timestamp_opt(network.created_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
name: network.name,
lighthouses_as_relays: network.lighthouses_as_relays,
},
metadata: GetNetworkResponseMetadata {},
})
} else {
HttpResponse::NotFound().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_MISSING_NETWORK".to_string(),
message: "Network does not exist".to_string(),
path: None,
}],
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworkResponse {
pub data: GetNetworksResponseData,
pub metadata: GetNetworkResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworkResponseMetadata {}

View File

@ -13,178 +13,278 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//#POST /v1/organization t+parity:none t+type:fabricated t+status:done t+status:want-reveng t+feature:definednetworking
// This is NOT a DN-compatible API. The organization create API has not yet been reverse engineered. This endpoint is a complete fabrication of trifid-api.
// While this endpoint is considered done, help is wanted with reverse engineering the original API. Major features should not be added or removed unless it is replacing this endpoint with the correct, DN-compatible endpoint.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use std::time::{Duration, SystemTime}; use crate::auth_tokens::{enforce_2fa, TokenInfo};
use ipnet::Ipv4Net; use crate::config::CONFIG;
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::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}; use crate::error::{APIError, APIErrorsResponse};
use crate::tokens::random_id;
#[options("/v1/orgs")] use crate::AppState;
pub fn options() -> &'static str { use actix_web::post;
"" use actix_web::web::{Data, Json};
} use actix_web::{HttpRequest, HttpResponse};
#[options("/v1/org/<_id>")] use log::error;
pub fn orgidoptions(_id: i32) -> &'static str { use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
"" use serde::{Deserialize, Serialize};
} use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[options("/v1/org")] use trifid_api_entities::entity::{network, organization, signing_ca};
pub fn createorgoptions() -> &'static str {""} use trifid_pki::cert::{serialize_x25519_private, NebulaCertificate, NebulaCertificateDetails};
use trifid_pki::ed25519_dalek::SigningKey;
use trifid_pki::rand_core::OsRng;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] pub struct OrgCreateRequest {
pub struct OrglistStruct { pub cidr: String,
pub org_ids: Vec<i32>
} }
#[derive(Serialize, Deserialize)]
pub struct OrgCreateResponse {
pub organization: String,
pub ca: String,
pub network: String,
}
#[get("/v1/orgs")] #[post("/v1/organization")]
pub async fn orglist_req(user: TOTPAuthenticatedUserInfo, db: &State<PgPool>) -> Result<(ContentType, Json<OrglistStruct>), (Status, String)> { pub async fn create_org_request(
// this endpoint lists the associated organizations this user has access to req: Json<OrgCreateRequest>,
let associated_orgs = match get_associated_orgs(user.user_id, db.inner()).await { req_info: HttpRequest,
Ok(r) => r, db: Data<AppState>,
Err(e) => return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_DB_QUERY_FAILED", "an error occurred while running the database query", e))) ) -> HttpResponse {
// For this endpoint, you need to be a fully authenticated user
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
// we have a session token, which means we have to do a db request to get the organization that this user owns
let user = match session_info {
TokenInfo::AuthToken(tkn) => tkn.session_info.user,
_ => {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}],
})
}
}; };
Ok((ContentType::JSON, Json(OrglistStruct { let org = match organization::Entity::find()
org_ids: associated_orgs .filter(organization::Column::Owner.eq(&user.id))
}))) .one(&db.conn)
} .await
{
#[derive(Serialize, Deserialize)] Ok(r) => r,
#[serde(crate = "rocket::serde")] Err(e) => {
pub struct OrginfoStruct { error!("database error: {}", e);
pub org_id: i32, return HttpResponse::InternalServerError().json(APIErrorsResponse {
pub owner_id: i32, errors: vec![
pub ca_crt: String, APIError {
pub authorized_users: Vec<i32> code: "ERR_DB_ERROR".to_string(),
} message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
#[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)] if org.is_some() {
#[serde(crate = "rocket::serde")] return HttpResponse::BadRequest().json(APIErrorsResponse {
pub struct CreateCARequest { errors: vec![APIError {
pub ip_ranges: Vec<Ipv4Net>, code: "ERR_USER_ALREADY_OWNS_ORG".to_string(),
pub subnet_ranges: Vec<Ipv4Net>, message: "This user already owns an organization".to_string(),
pub groups: Vec<String> path: None,
} }],
});
#[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")))
} }
let org = organization::Model {
id: random_id("org"),
name: format!("{}'s Organization", user.email),
owner: user.id.clone(),
};
// Generate the CA keypair // Generate the CA keypair
let private_key = SigningKey::generate(&mut OsRng); let private_key = SigningKey::generate(&mut OsRng);
let public_key = private_key.verifying_key(); let public_key = private_key.verifying_key();
// Create the CA certificate let mut cert = NebulaCertificate {
let mut ca_cert = NebulaCertificate {
details: NebulaCertificateDetails { details: NebulaCertificateDetails {
name: format!("{}'s Organization - Root Signing CA", user.email), name: format!("{} Signing CA", org.name),
ips: req.ip_ranges.clone(), ips: vec![],
subnets: req.subnet_ranges.clone(), subnets: vec![],
groups: req.groups.clone(), groups: vec![],
not_before: SystemTime::now(), not_before: SystemTime::now(),
not_after: SystemTime::now() + Duration::from_secs(config.ca_certs_valid_for), not_after: SystemTime::now() + Duration::from_secs(31536000 * 3), // 3 years
public_key: public_key.to_bytes(), public_key: public_key.to_bytes(),
is_ca: true, is_ca: true,
issuer: "".to_string(), // This is a self-signed certificate! There is no issuer present issuer: "".to_string(), // Self-signed certificate! No issuer present
}, },
signature: vec![], signature: vec![],
}; };
// Self-sign the CA certificate // Self-sign the CA certificate
match ca_cert.sign(&private_key) { match cert.sign(&private_key) {
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
error!("[tfapi] security: certificate signature error: {}", e); error!("[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"))) return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_CERT_SIGNING_ERROR".to_string(),
message: "There was an error signing the Certificate Authority on the server. Please try again later.".to_string(),
path: None,
}
]
});
} }
} }
// PEM-encode the CA key // PEM-encode the CA key
let ca_key_pem = serialize_ed25519_private(&private_key.to_keypair_bytes()); let ca_key_pem = serialize_x25519_private(&private_key.to_keypair_bytes());
// PEM-encode the CA cert // PEM-encode the CA cert
let ca_cert_pem = match ca_cert.serialize_to_pem() { let ca_cert_pem = match cert.serialize_to_pem() {
Ok(pem) => pem, Ok(pem) => pem,
Err(e) => { Err(e) => {
error!("[tfapi] security: certificate encoding error: {}", e); error!("[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"))) return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_CERT_ENCODING_ERROR".to_string(),
message: "There was an error encoding the certificate on the server. Please try again later.".to_string(),
path: None,
}
]
});
} }
}; };
// generate an AES iv to use for key encryption let iv = generate_random_iv(); // Generate a randomized IV to use for key encryption
let iv = generate_random_iv();
let iv_hex = hex::encode(iv); let iv_hex = hex::encode(iv);
let owner_id = user.user_id; let cipher = match get_cipher_from_config(&CONFIG) {
let cipher = match get_cipher_from_config(config) { Ok(pem) => pem,
Ok(c) => c,
Err(e) => { Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_CREATE_CIPHER", "Unable to build cipher construct, please try again later", e))); error!("[security] cipher fetch error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_CIPHER_ERROR".to_string(),
message: "There was an error encrypting the organization data. Please try again later.".to_string(),
path: None,
}
]
});
} }
}; };
let ca_key = match encrypt_with_nonce(&ca_key_pem, iv, &cipher) {
let ca_key_encrypted = match encrypt_with_nonce(&ca_key_pem, iv, &cipher) {
Ok(key) => hex::encode(key), Ok(key) => hex::encode(key),
Err(e) => { Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_CRYPTOGRAPHY_ENCRYPT_KEY", "Unable to build cipher construct, please try again later", e))); error!("[security] certificate encoding error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_CERT_ENCODING_ERROR".to_string(),
message: "There was an error encoding the certificate on the server. Please try again later.".to_string(),
path: None,
}
]
});
} }
}; };
let ca_crt = hex::encode(ca_cert_pem); let ca_crt = hex::encode(ca_cert_pem);
let result = sqlx::query!("INSERT INTO organizations (owner, ca_key, ca_crt, iv) VALUES ($1, $2, $3, $4) RETURNING id", owner_id, ca_key, ca_crt, iv_hex).fetch_one(db.inner()).await.map_err(|e| (Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e)))?; let signing_ca = signing_ca::Model {
id: random_id("ca"),
// last step: create a default role to allow pings from all hosts organization: org.id.clone(),
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 { cert: ca_key_encrypted,
Ok(r) => r.id, key: ca_crt,
Err(e) => { expires: cert
error!("[tfapi] dberror: {}", e); .details
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role"))); .not_after
} .duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64,
nonce: iv_hex,
}; };
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 { let network_model = network::Model {
Ok(_) => {}, id: random_id("network"),
cidr: req.cidr.clone(),
organization: org.id.clone(),
signing_ca: signing_ca.id.clone(),
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64,
name: "Network1".to_string(),
lighthouses_as_relays: true,
};
let new_org_id = org.id.clone();
let new_signing_ca_id = signing_ca.id.clone();
let new_network_id = network_model.id.clone();
let org_active_model = org.into_active_model();
let signing_ca_active_model = signing_ca.into_active_model();
let network_active_model = network_model.into_active_model();
match org_active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => { Err(e) => {
error!("[tfapi] dberror: {} inserting on roleid {}", e, role_id); error!("database error: {}", e);
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_INTERNAL_SERVER_ERROR", "Unable to create default role firewall rules"))); return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
}
match signing_ca_active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
}
}
match network_active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error performing the database request, please try again later.".to_string(),
path: None,
}
],
});
} }
} }
Ok((ContentType::JSON, Json( HttpResponse::Ok().json(OrgCreateResponse {
OrginfoStruct { organization: new_org_id,
org_id: result.id, ca: new_signing_ca_id,
owner_id, network: new_network_id,
ca_crt, })
authorized_users: vec![owner_id],
}
)))
} }

File diff suppressed because it is too large Load Diff

View File

@ -13,77 +13,139 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//#POST /v1/signup t+parity:full t+type:reverse_engineered t+status:done t+features:definednetworking
// This endpoint has full parity with the original API. It has been reverse-engineered from the original API as the original API docs do not have this item.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use rocket::{post, State}; use crate::config::CONFIG;
use rocket::serde::json::Json; use crate::error::{APIError, APIErrorsResponse};
use serde::{Serialize, Deserialize}; use crate::magic_link::send_magic_link;
use std::time::{SystemTime, UNIX_EPOCH}; use crate::timers::expires_in_seconds;
use rocket::http::{ContentType, Status}; use crate::tokens::{random_id, random_token};
use sqlx::PgPool; use crate::AppState;
use crate::config::TFConfig; use actix_web::web::{Data, Json};
use crate::tokens::send_magic_link; use actix_web::{post, HttpResponse};
use rocket::options; use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::user;
use trifid_api_entities::entity::user::Entity as UserEntity;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct SignupRequest { pub struct SignupRequest {
pub email: String, pub email: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(crate = "rocket::serde")]
pub struct SignupResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct SignupResponse { pub struct SignupResponse {
pub data: Option<String>, pub data: Option<SignupResponseData>,
pub metadata: SignupResponseMetadata, pub metadata: SignupResponseMetadata,
} }
/* #[derive(Serialize, Deserialize, Clone, Debug)]
created_on TIMESTAMP NOT NULL, pub struct SignupResponseData {}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SignupResponseMetadata {}
banned INTEGER NOT NULL, #[post("/v1/signup")]
ban_reason VARCHAR(1024) NOT NULL pub async fn signup_request(data: Data<AppState>, req: Json<SignupRequest>) -> HttpResponse {
*/ let user: Vec<user::Model> = match UserEntity::find()
#[options("/v1/signup")] .filter(user::Column::Email.eq(&req.email))
pub async fn options() -> &'static str { .all(&data.conn)
"" .await
} {
Ok(r) => r,
#[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) => { Err(e) => {
return Err((Status::InternalServerError, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{} - {}\"}}]}}", "ERR_QL_QUERY_FAILED", "an error occurred while running the graphql query", e))) error!("database error: {}", e);
} return HttpResponse::InternalServerError().json(APIErrorsResponse {
} errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
if id == -1 { message:
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 { "There was an error with the database request, please try again later."
Ok(row) => row.id, .to_string(),
Err(e) => { path: None,
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 if !user.is_empty() {
// 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) return HttpResponse::Unauthorized().json(APIErrorsResponse {
Ok((ContentType::JSON, Json(SignupResponse { errors: vec![APIError {
code: "ERR_USER_EXISTS".to_string(),
message: "That user already exists.".to_string(),
path: None,
}],
});
}
let model = user::Model {
id: random_id("user"),
email: req.email.clone(),
};
let id = model.id.clone();
let active_model = model.into_active_model();
match active_model.insert(&data.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
}
let model = trifid_api_entities::entity::magic_link::Model {
id: random_token("ml"),
user: id,
expires_on: expires_in_seconds(CONFIG.tokens.magic_link_expiry_time_seconds) as i64,
};
match send_magic_link(&model.id) {
Ok(_) => (),
Err(e) => {
error!("error sending magic link: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_ML_ERROR".to_string(),
message:
"There was an error sending the magic link email, please try again later."
.to_string(),
path: None,
}],
});
}
}
let active_model = model.into_active_model();
match active_model.insert(&data.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
}
HttpResponse::Ok().json(SignupResponse {
data: None, data: None,
metadata: SignupResponseMetadata {}, metadata: SignupResponseMetadata {},
}))) })
} }

View File

@ -13,64 +13,181 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//#POST /v1/totp-authenticators t+parity:full t+type:reverse_engineered t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been reverse-engineered from the original API as the original API docs do not have this item.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use rocket::http::{ContentType, Status}; use crate::auth_tokens::{enforce_session, TokenInfo};
use rocket::serde::json::Json; use crate::config::CONFIG;
use rocket::{State, post}; use crate::error::{APIError, APIErrorsResponse};
use sqlx::PgPool; use crate::timers::expires_in_seconds;
use serde::{Serialize, Deserialize}; use crate::tokens::random_token;
use crate::auth::PartialUserInfo; use crate::AppState;
use crate::config::TFConfig; use actix_web::web::{Data, Json};
use crate::tokens::{create_totp_token, user_has_totp}; use actix_web::{post, HttpRequest, HttpResponse};
use rocket::options; use log::error;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter,
};
use serde::{Deserialize, Serialize};
use totp_rs::{Algorithm, Secret, TOTP};
use trifid_api_entities::entity::totp_authenticator;
#[derive(Deserialize)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpAuthenticatorsRequest {} pub struct TotpAuthenticatorsRequest {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpAuthenticatorsResponseMetadata {} pub struct TotpAuthenticatorsResponseMetadata {}
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpAuthenticatorsResponseData { pub struct TotpAuthenticatorsResponseData {
#[serde(rename = "totpToken")] #[serde(rename = "totpToken")]
pub totp_token: String, pub totp_token: String,
pub secret: String, pub secret: String,
pub url: String, pub url: String,
} }
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpAuthenticatorsResponse { pub struct TotpAuthenticatorsResponse {
pub data: TotpAuthenticatorsResponseData, pub data: TotpAuthenticatorsResponseData,
pub metadata: TotpAuthenticatorsResponseMetadata, pub metadata: TotpAuthenticatorsResponseMetadata,
} }
#[options("/v1/totp-authenticators")] #[post("/v1/totp-authenticators")]
pub async fn options() -> &'static str { pub async fn totp_authenticators_request(
"" db: Data<AppState>,
} req_data: HttpRequest,
_req: Json<TotpAuthenticatorsRequest>,
) -> HttpResponse {
#[post("/v1/totp-authenticators", data = "<_req>")] // require a user session
pub async fn totp_authenticators_request(_req: Json<TotpAuthenticatorsRequest>, user: PartialUserInfo, db: &State<PgPool>, config: &State<TFConfig>) -> Result<(ContentType, Json<TotpAuthenticatorsResponse>), (Status, String)> { let session_token = match enforce_session(&req_data, &db.conn).await {
if match user_has_totp(user.user_id, db.inner()).await { Ok(r) => match r {
Ok(b) => b, TokenInfo::SessionToken(i) => i,
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))) _ => unreachable!(),
} { },
return Err((Status::BadRequest, format!("{{\"errors\":[{{\"code\":\"{}\",\"message\":\"{}\"}}]}}", "ERR_TOTP_ALREADY_EXISTS", "this user already has a totp authenticator on their account"))) Err(e) => {
} error!("error enforcing session: {}", e);
return HttpResponse::Unauthorized().json(APIErrorsResponse {
// generate a totp token errors: vec![APIError {
let (totptoken, totpmachine) = match create_totp_token(user.email, db.inner(), config.inner()).await { code: "ERR_UNAUTHORIZED".to_string(),
Ok(t) => t, message: "Unauthorized".to_string(),
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))) path: None,
}],
});
}
}; };
Ok((ContentType::JSON, Json(TotpAuthenticatorsResponse { // determine if the user has a totp authenticator
let auther = match totp_authenticator::Entity::find()
.filter(totp_authenticator::Column::User.eq(&session_token.user.id))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
};
if let Some(auther) = auther {
if auther.verified {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_ALREADY_HAS_TOTP".to_string(),
message: "This user already has a totp authenticator".to_string(),
path: None,
}],
});
}
match auther.delete(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
}
}
let secret = Secret::generate_secret();
let totpmachine = match TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret.to_bytes().expect("Invalid randomized data"),
Some("trifid-api".to_string()),
session_token.user.email,
) {
Ok(m) => m,
Err(e) => {
error!("totp machine create error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_SECRET_ERR".to_string(),
message:
"There was an error configuring the authenticator, please try again later."
.to_string(),
path: None,
}],
});
}
};
let model = totp_authenticator::Model {
id: random_token("totp"),
secret: Secret::Raw(totpmachine.secret.clone())
.to_encoded()
.to_string(),
url: totpmachine.get_url(),
verified: false,
expires_on: expires_in_seconds(CONFIG.tokens.totp_setup_timeout_time_seconds) as i64,
user: session_token.user.id,
};
let id = model.id.clone();
let secret = model.secret.clone();
let url = model.url.clone();
let active_model = model.into_active_model();
match active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
}
HttpResponse::Ok().json(TotpAuthenticatorsResponse {
data: TotpAuthenticatorsResponseData { data: TotpAuthenticatorsResponseData {
totp_token: totptoken, totp_token: id,
secret: totpmachine.get_secret_base32(), secret,
url: totpmachine.get_url(), url,
}, },
metadata: TotpAuthenticatorsResponseMetadata {}, metadata: TotpAuthenticatorsResponseMetadata {},
}))) })
} }

View File

@ -0,0 +1,55 @@
// 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/>.
//
//#GET /v1/trifid_extensions t+parity:none t+type:fabricated t+status:done t+status:special t+features:trifidextensions
// This is NOT a DN-compatible API. This endpoint is a fabrication of trifid-api to enable the TrifidExtensions codebase for clients. This allows clients to access additional functionality only available on servers that support it.
// This endpoint is considered done. It should not be modified unless for bugfixes.
// This endpoint is a special endpoint, and may impact what features client are able to access.
// This endpoint requires the `trifidextensions` extension to be enabled to be used.
//
// This endpoint implements the TrifidExtensions API extension framework. This allows the server to optionally provide extra features and endpoints to clients that support it,
// by providing an endpoint to allow the client to check which API extensions are enabled.
// The following extensions are available:
// - definednetworking - Base DN api, must be enabled on all servers compatible with the original DN api
// - trifidextensions - Enables the TrifidExtensions codebase
// - extended_roles - Enables extra actions when editing roles (see the list of special endpoints in roles.rs)
// - extended_hosts - Enables extra actions when editing hosts (see the list of special endpoints in hosts.rs)
//
// A client should GET /v1/trifid_extensions upon creating a new connection to an API server, to check which features it supports.
// If the request returns a non-200 response, or does not follow the typical TrifidExtensions schema, that server should be assumed to only support t+features:definednetworking.
// Endpoint specs (#REQTYPE) can indicate they require a feature by adding t+features:[feature]
use actix_web::{get, HttpResponse};
use serde::{Deserialize, Serialize};
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
"definednetworking",
"trifidextensions",
"extended_roles",
"extended_hosts",
];
#[derive(Serialize, Deserialize)]
pub struct TrifidExtensionsResponse {
pub extensions: Vec<String>,
}
#[get("/v1/trifid_extensions")]
pub async fn trifid_extensions() -> HttpResponse {
HttpResponse::Ok().json(TrifidExtensionsResponse {
extensions: SUPPORTED_EXTENSIONS.iter().map(|u| u.to_string()).collect(),
})
}

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

@ -0,0 +1,214 @@
// 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/>.
//
//#POST /v1/verify-totp-authenticators t+parity:full t+type:reverse_engineered t+status:done t+feature:definednetworking
// This endpoint has full parity with the original API. It has been reverse-engineered from the original API as the original API docs do not have this item.
// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs.
// This endpoint requires the `definednetworking` extension to be enabled to be used.
use crate::auth_tokens::{enforce_session, TokenInfo};
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::timers::expires_in_seconds;
use crate::tokens::random_token;
use crate::AppState;
use actix_web::web::{Data, Json};
use actix_web::{post, HttpRequest, HttpResponse};
use log::{debug, error};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Deserialize, Serialize};
use totp_rs::{Secret, TOTP};
use trifid_api_entities::entity::auth_token;
use trifid_api_entities::entity::totp_authenticator;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyTotpAuthenticatorsRequest {
#[serde(rename = "totpToken")]
pub totp_token: String,
pub code: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyTotpAuthenticatorsResponse {
pub data: VerifyTotpAuthenticatorsResponseData,
pub metadata: VerifyTotpAuthenticatorsResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyTotpAuthenticatorsResponseData {
#[serde(rename = "authToken")]
pub auth_token: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyTotpAuthenticatorsResponseMetadata {}
#[post("/v1/verify-totp-authenticators")]
pub async fn verify_totp_authenticators_request(
req: Json<VerifyTotpAuthenticatorsRequest>,
req_data: HttpRequest,
db: Data<AppState>,
) -> HttpResponse {
// require a user session
let session_token = match enforce_session(&req_data, &db.conn).await {
Ok(r) => match r {
TokenInfo::SessionToken(i) => i,
_ => unreachable!(),
},
Err(e) => {
error!("error enforcing session: {}", e);
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}],
});
}
};
// determine if the user has a totp authenticator
let auther = match totp_authenticator::Entity::find()
.filter(totp_authenticator::Column::Id.eq(&req.totp_token))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}],
});
}
};
let auther = match auther {
Some(a) => {
if a.verified {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_ALREADY_HAS_TOTP".to_string(),
message: "This user already has a totp authenticator".to_string(),
path: None,
}],
});
}
a
}
None => {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_USER_NO_TOTP".to_string(),
message: "This user does not have a totp authenticator".to_string(),
path: None,
}],
});
}
};
let _secret = Secret::Encoded(auther.secret.clone());
let totpmachine = match TOTP::from_url(auther.url.clone()) {
Ok(m) => m,
Err(e) => {
error!("totp url error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_SECRET_ERROR".to_string(),
message: "There was an error parsing the totpmachine. Please try again later."
.to_string(),
path: None,
}],
});
}
};
let valid = match totpmachine.check_current(&req.code) {
Ok(valid) => valid,
Err(e) => {
error!("system time error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_TIME_ERROR".to_string(),
message: "There was an with the server-side time clock.".to_string(),
path: None,
}],
});
}
};
if !valid {
debug!("current: {}", totpmachine.generate_current().unwrap());
error!("user send invalid totp code");
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}],
});
}
let mut active_model = auther.into_active_model();
active_model.verified = Set(true);
match active_model.update(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error updating the totpmachine, please try again later."
.to_string(),
path: None,
}],
});
}
}
let model: auth_token::Model = auth_token::Model {
id: random_token("auth"),
user: session_token.user.id,
expires_on: expires_in_seconds(CONFIG.tokens.mfa_tokens_expiry_time_seconds) as i64,
};
let token = model.id.clone();
let active_model = model.into_active_model();
match active_model.insert(&db.conn).await {
Ok(_) => (),
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error issuing the authentication token.".to_string(),
path: None,
}],
});
}
}
HttpResponse::Ok().json(VerifyTotpAuthenticatorsResponse {
data: VerifyTotpAuthenticatorsResponseData { auth_token: token },
metadata: VerifyTotpAuthenticatorsResponseMetadata {},
})
}

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

@ -13,3 +13,18 @@
// //
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z";
pub fn expires_in_seconds(seconds: u64) -> u64 {
(SystemTime::now() + Duration::from_secs(seconds))
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
}
pub fn expired(time: u64) -> bool {
UNIX_EPOCH + Duration::from_secs(time) < SystemTime::now()
}

View File

@ -14,96 +14,47 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::error::Error; use actix_web::http::header::HeaderValue;
use log::info; use rand::Rng;
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 const ID_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box<dyn Error>> { pub const ID_LEN: u32 = 26;
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(); pub const TOKEN_CHARSET: &[u8] =
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?; b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
// TODO: send email pub const TOKEN_LEN: u32 = 43;
info!("sent magic link {} to {}, valid for {} seconds", otp_url, email.clone(), config.magic_links_valid_for);
Ok(()) // 26
// format: [ID]-[26 chars]
pub fn random_id(identifier: &str) -> String {
format!("{}-{}", identifier, random_with_charset(ID_LEN, ID_CHARSET))
} }
pub async fn generate_session_token(user_id: i64, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> { // 26
let token = format!("st-{}", Uuid::new_v4()); // format: [ID]-[26 chars]
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?; pub fn random_id_no_id() -> HeaderValue {
Ok(token) HeaderValue::from_str(&random_with_charset(ID_LEN, ID_CHARSET)).unwrap()
}
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>> { // 43
let token = format!("at-{}", Uuid::new_v4()); // format: [TYPE]-[43 chars]
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?; pub fn random_token(identifier: &str) -> String {
Ok(token) format!(
} "{}-{}",
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box<dyn Error>> { identifier,
validate_session_token(session_id.clone(), db).await?; random_with_charset(TOKEN_LEN, TOKEN_CHARSET)
sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?; )
Ok(())
} }
fn random_with_charset(len: u32, charset: &[u8]) -> String {
/* (0..len)
CREATE TABLE totp_create_tokens ( .map(|_| {
id VARCHAR(39) NOT NULL PRIMARY KEY, let idx = rand::thread_rng().gen_range(0..charset.len());
expires_on INTEGER NOT NULL, charset[idx] as char
totp_otpurl VARCHAR(3000) NOT NULL, })
totp_secret VARCHAR(128) NOT NULL .collect()
);
*/
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>> { pub fn get_token_type(token: &str) -> Option<&str> {
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?; token.split('-').collect::<Vec<&str>>().first().copied()
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)
}

View File

@ -0,0 +1,9 @@
[package]
name = "trifid_api_entities"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sea-orm = { version = "^0" }

View File

@ -0,0 +1,41 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "api_key")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub key: String,
pub organization: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::api_key_scope::Entity")]
ApiKeyScope,
#[sea_orm(
belongs_to = "super::organization::Entity",
from = "Column::Organization",
to = "super::organization::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Organization,
}
impl Related<super::api_key_scope::Entity> for Entity {
fn to() -> RelationDef {
Relation::ApiKeyScope.def()
}
}
impl Related<super::organization::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organization.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "api_key_scope")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub scope: String,
pub api_key: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::api_key::Entity",
from = "Column::ApiKey",
to = "super::api_key::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
ApiKey,
}
impl Related<super::api_key::Entity> for Entity {
fn to() -> RelationDef {
Relation::ApiKey.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "auth_token")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub user: String,
pub expires_on: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::User",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,38 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "firewall_rule")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub role: String,
pub protocol: String,
pub description: String,
pub allowed_role_id: Option<String>,
pub port_range_from: i32,
pub port_range_to: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::role::Entity",
from = "Column::AllowedRoleId",
to = "super::role::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Role2,
#[sea_orm(
belongs_to = "super::role::Entity",
from = "Column::Role",
to = "super::role::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Role1,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,82 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "host")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
pub network: String,
pub role: String,
pub ip: String,
pub listen_port: i32,
pub is_lighthouse: bool,
pub is_relay: bool,
pub counter: i32,
pub created_at: i64,
pub is_blocked: bool,
pub last_seen_at: i64,
pub last_version: i32,
pub last_platform: String,
pub last_out_of_date: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::host_config_override::Entity")]
HostConfigOverride,
#[sea_orm(has_many = "super::host_enrollment_code::Entity")]
HostEnrollmentCode,
#[sea_orm(has_many = "super::host_static_address::Entity")]
HostStaticAddress,
#[sea_orm(
belongs_to = "super::network::Entity",
from = "Column::Network",
to = "super::network::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Network,
#[sea_orm(
belongs_to = "super::role::Entity",
from = "Column::Role",
to = "super::role::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Role,
}
impl Related<super::host_config_override::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostConfigOverride.def()
}
}
impl Related<super::host_enrollment_code::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostEnrollmentCode.def()
}
}
impl Related<super::host_static_address::Entity> for Entity {
fn to() -> RelationDef {
Relation::HostStaticAddress.def()
}
}
impl Related<super::network::Entity> for Entity {
fn to() -> RelationDef {
Relation::Network.def()
}
}
impl Related<super::role::Entity> for Entity {
fn to() -> RelationDef {
Relation::Role.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,33 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "host_config_override")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub key: String,
pub value: String,
pub host: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::host::Entity",
from = "Column::Host",
to = "super::host::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Host,
}
impl Related<super::host::Entity> for Entity {
fn to() -> RelationDef {
Relation::Host.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "host_enrollment_code")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub host: String,
pub expires_on: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::host::Entity",
from = "Column::Host",
to = "super::host::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Host,
}
impl Related<super::host::Entity> for Entity {
fn to() -> RelationDef {
Relation::Host.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "host_static_address")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub host: String,
pub address: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::host::Entity",
from = "Column::Host",
to = "super::host::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Host,
}
impl Related<super::host::Entity> for Entity {
fn to() -> RelationDef {
Relation::Host.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "magic_link")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub user: String,
pub expires_on: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::User",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,20 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
pub mod prelude;
pub mod api_key;
pub mod api_key_scope;
pub mod auth_token;
pub mod firewall_rule;
pub mod host;
pub mod host_config_override;
pub mod host_enrollment_code;
pub mod host_static_address;
pub mod magic_link;
pub mod network;
pub mod organization;
pub mod role;
pub mod session_token;
pub mod signing_ca;
pub mod totp_authenticator;
pub mod user;

View File

@ -0,0 +1,60 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "network")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub cidr: String,
#[sea_orm(unique)]
pub organization: String,
#[sea_orm(unique)]
pub signing_ca: String,
pub created_at: i64,
pub name: String,
pub lighthouses_as_relays: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::host::Entity")]
Host,
#[sea_orm(
belongs_to = "super::organization::Entity",
from = "Column::Organization",
to = "super::organization::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Organization,
#[sea_orm(
belongs_to = "super::signing_ca::Entity",
from = "Column::SigningCa",
to = "super::signing_ca::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
SigningCa,
}
impl Related<super::host::Entity> for Entity {
fn to() -> RelationDef {
Relation::Host.def()
}
}
impl Related<super::organization::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organization.def()
}
}
impl Related<super::signing_ca::Entity> for Entity {
fn to() -> RelationDef {
Relation::SigningCa.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,57 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "organization")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
#[sea_orm(unique)]
pub owner: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::api_key::Entity")]
ApiKey,
#[sea_orm(has_one = "super::network::Entity")]
Network,
#[sea_orm(has_many = "super::role::Entity")]
Role,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::Owner",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
}
impl Related<super::api_key::Entity> for Entity {
fn to() -> RelationDef {
Relation::ApiKey.def()
}
}
impl Related<super::network::Entity> for Entity {
fn to() -> RelationDef {
Relation::Network.def()
}
}
impl Related<super::role::Entity> for Entity {
fn to() -> RelationDef {
Relation::Role.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,18 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
pub use super::api_key::Entity as ApiKey;
pub use super::api_key_scope::Entity as ApiKeyScope;
pub use super::auth_token::Entity as AuthToken;
pub use super::firewall_rule::Entity as FirewallRule;
pub use super::host::Entity as Host;
pub use super::host_config_override::Entity as HostConfigOverride;
pub use super::host_enrollment_code::Entity as HostEnrollmentCode;
pub use super::host_static_address::Entity as HostStaticAddress;
pub use super::magic_link::Entity as MagicLink;
pub use super::network::Entity as Network;
pub use super::organization::Entity as Organization;
pub use super::role::Entity as Role;
pub use super::session_token::Entity as SessionToken;
pub use super::signing_ca::Entity as SigningCa;
pub use super::totp_authenticator::Entity as TotpAuthenticator;
pub use super::user::Entity as User;

View File

@ -0,0 +1,44 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "role")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub name: String,
pub description: String,
pub organization: String,
pub created_at: i64,
pub modified_at: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::host::Entity")]
Host,
#[sea_orm(
belongs_to = "super::organization::Entity",
from = "Column::Organization",
to = "super::organization::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Organization,
}
impl Related<super::host::Entity> for Entity {
fn to() -> RelationDef {
Relation::Host.def()
}
}
impl Related<super::organization::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organization.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "session_token")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub user: String,
pub expires_on: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::User",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,31 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "signing_ca")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub organization: String,
pub cert: String,
#[sea_orm(unique)]
pub key: String,
pub expires: i64,
#[sea_orm(unique)]
pub nonce: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_one = "super::network::Entity")]
Network,
}
impl Related<super::network::Entity> for Entity {
fn to() -> RelationDef {
Relation::Network.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,38 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "totp_authenticator")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub secret: String,
#[sea_orm(unique)]
pub url: String,
pub verified: bool,
pub expires_on: i64,
#[sea_orm(unique)]
pub user: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::User",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,58 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub email: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::auth_token::Entity")]
AuthToken,
#[sea_orm(has_many = "super::magic_link::Entity")]
MagicLink,
#[sea_orm(has_one = "super::organization::Entity")]
Organization,
#[sea_orm(has_many = "super::session_token::Entity")]
SessionToken,
#[sea_orm(has_one = "super::totp_authenticator::Entity")]
TotpAuthenticator,
}
impl Related<super::auth_token::Entity> for Entity {
fn to() -> RelationDef {
Relation::AuthToken.def()
}
}
impl Related<super::magic_link::Entity> for Entity {
fn to() -> RelationDef {
Relation::MagicLink.def()
}
}
impl Related<super::organization::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organization.def()
}
}
impl Related<super::session_token::Entity> for Entity {
fn to() -> RelationDef {
Relation::SessionToken.def()
}
}
impl Related<super::totp_authenticator::Entity> for Entity {
fn to() -> RelationDef {
Relation::TotpAuthenticator.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1 @@
pub mod entity;

View File

@ -0,0 +1,25 @@
[package]
name = "trifid_api_migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "trifid_api_migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
async-trait = "0.1.68"
[dependencies.sea-orm-migration]
version = "0.11.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run trifid_api_migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
"sqlx-postgres",
"runtime-actix-rustls"
]

View File

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- migrate generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View File

@ -0,0 +1,44 @@
pub use sea_orm_migration::prelude::*;
pub struct Migrator;
pub mod m20230402_162601_create_table_users;
pub mod m20230402_183515_create_table_magic_links;
pub mod m20230402_213712_create_table_session_tokens;
pub mod m20230402_232316_create_table_organizations;
pub mod m20230402_233043_create_table_api_keys;
pub mod m20230402_233047_create_table_api_keys_scopes;
pub mod m20230402_234025_create_table_totp_authenticators;
pub mod m20230403_002256_create_table_auth_tokens;
pub mod m20230403_142517_create_table_signing_cas;
pub mod m20230403_173431_create_table_networks;
mod m20230404_133809_create_table_roles;
mod m20230404_133813_create_table_firewall_rules;
mod m20230427_170037_create_table_hosts;
mod m20230427_171517_create_table_hosts_static_addresses;
mod m20230427_171529_create_table_hosts_config_overrides;
mod m20230511_120511_create_table_host_enrollment_codes;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20230402_162601_create_table_users::Migration),
Box::new(m20230402_183515_create_table_magic_links::Migration),
Box::new(m20230402_213712_create_table_session_tokens::Migration),
Box::new(m20230402_232316_create_table_organizations::Migration),
Box::new(m20230402_233043_create_table_api_keys::Migration),
Box::new(m20230402_233047_create_table_api_keys_scopes::Migration),
Box::new(m20230402_234025_create_table_totp_authenticators::Migration),
Box::new(m20230403_002256_create_table_auth_tokens::Migration),
Box::new(m20230403_142517_create_table_signing_cas::Migration),
Box::new(m20230403_173431_create_table_networks::Migration),
Box::new(m20230404_133809_create_table_roles::Migration),
Box::new(m20230404_133813_create_table_firewall_rules::Migration),
Box::new(m20230427_170037_create_table_hosts::Migration),
Box::new(m20230427_171517_create_table_hosts_static_addresses::Migration),
Box::new(m20230427_171529_create_table_hosts_config_overrides::Migration),
Box::new(m20230511_120511_create_table_host_enrollment_codes::Migration),
]
}
}

View File

@ -0,0 +1,34 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(ColumnDef::new(User::Id).string().not_null().primary_key())
.col(ColumnDef::new(User::Email).string().not_null().unique_key())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum User {
Table,
Id,
Email,
}

View File

@ -0,0 +1,54 @@
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(MagicLink::Table)
.if_not_exists()
.col(
ColumnDef::new(MagicLink::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(MagicLink::User).string().not_null())
.col(
ColumnDef::new(MagicLink::ExpiresOn)
.big_integer()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("fk_magiclink_user_users_id")
.from(MagicLink::Table, MagicLink::User)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(MagicLink::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum MagicLink {
Table,
Id,
User,
ExpiresOn,
}

View File

@ -0,0 +1,53 @@
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(SessionToken::Table)
.if_not_exists()
.col(
ColumnDef::new(SessionToken::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(SessionToken::User).string().not_null())
.col(
ColumnDef::new(SessionToken::ExpiresOn)
.big_integer()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.from(SessionToken::Table, SessionToken::User)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(SessionToken::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum SessionToken {
Table,
Id,
User,
ExpiresOn,
}

View File

@ -0,0 +1,53 @@
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Organization::Table)
.col(
ColumnDef::new(Organization::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Organization::Name).string().not_null())
.col(
ColumnDef::new(Organization::Owner)
.string()
.not_null()
.unique_key(),
)
.foreign_key(
ForeignKey::create()
.from(Organization::Table, Organization::Owner)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Organization::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Organization {
Table,
Id,
Name,
Owner,
}

View File

@ -0,0 +1,43 @@
use crate::m20230402_232316_create_table_organizations::Organization;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(ApiKey::Table)
.col(ColumnDef::new(ApiKey::Id).string().not_null().primary_key())
.col(ColumnDef::new(ApiKey::Key).string().not_null().unique_key())
.col(ColumnDef::new(ApiKey::Organization).string().not_null())
.foreign_key(
ForeignKey::create()
.from(ApiKey::Table, ApiKey::Organization)
.to(Organization::Table, Organization::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(ApiKey::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum ApiKey {
Table,
Id,
Key,
Organization,
}

View File

@ -0,0 +1,48 @@
use crate::m20230402_233043_create_table_api_keys::ApiKey;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(ApiKeyScope::Table)
.col(
ColumnDef::new(ApiKeyScope::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(ApiKeyScope::Scope).string().not_null())
.col(ColumnDef::new(ApiKeyScope::ApiKey).string().not_null())
.foreign_key(
ForeignKey::create()
.from(ApiKeyScope::Table, ApiKeyScope::ApiKey)
.to(ApiKey::Table, ApiKey::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(ApiKeyScope::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum ApiKeyScope {
Table,
Id,
Scope,
ApiKey,
}

View File

@ -0,0 +1,77 @@
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(TotpAuthenticator::Table)
.col(
ColumnDef::new(TotpAuthenticator::Id)
.string()
.not_null()
.primary_key(),
)
.col(
ColumnDef::new(TotpAuthenticator::Secret)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(TotpAuthenticator::Url)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(TotpAuthenticator::Verified)
.boolean()
.not_null(),
)
.col(
ColumnDef::new(TotpAuthenticator::ExpiresOn)
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(TotpAuthenticator::User)
.string()
.not_null()
.unique_key(),
)
.foreign_key(
ForeignKey::create()
.from(TotpAuthenticator::Table, TotpAuthenticator::User)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(TotpAuthenticator::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum TotpAuthenticator {
Table,
Id,
Secret,
Url,
Verified,
ExpiresOn,
User,
}

View File

@ -0,0 +1,53 @@
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(AuthToken::Table)
.if_not_exists()
.col(
ColumnDef::new(AuthToken::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AuthToken::User).string().not_null())
.col(
ColumnDef::new(AuthToken::ExpiresOn)
.big_integer()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.from(AuthToken::Table, AuthToken::User)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(AuthToken::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum AuthToken {
Table,
Id,
User,
ExpiresOn,
}

View File

@ -0,0 +1,56 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(SigningCA::Table)
.col(
ColumnDef::new(SigningCA::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(SigningCA::Organization).string().not_null())
.col(ColumnDef::new(SigningCA::Cert).string().not_null())
.col(
ColumnDef::new(SigningCA::Key)
.string()
.not_null()
.unique_key(),
)
.col(ColumnDef::new(SigningCA::Expires).big_integer().not_null())
.col(
ColumnDef::new(SigningCA::Nonce)
.string()
.not_null()
.unique_key(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(SigningCA::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum SigningCA {
Table,
Id,
Organization,
Cert,
Key,
Expires,
Nonce,
}

View File

@ -0,0 +1,78 @@
use crate::m20230402_232316_create_table_organizations::Organization;
use crate::m20230403_142517_create_table_signing_cas::SigningCA;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Network::Table)
.col(
ColumnDef::new(Network::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Network::Cidr).string().not_null())
.col(
ColumnDef::new(Network::Organization)
.string()
.not_null()
.unique_key(),
)
.col(
ColumnDef::new(Network::SigningCA)
.string()
.not_null()
.unique_key(),
)
.col(ColumnDef::new(Network::CreatedAt).big_integer().not_null())
.col(ColumnDef::new(Network::Name).string().not_null())
.col(
ColumnDef::new(Network::LighthousesAsRelays)
.boolean()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.from(Network::Table, Network::Organization)
.to(Organization::Table, Organization::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(Network::Table, Network::SigningCA)
.to(SigningCA::Table, SigningCA::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Network::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Network {
Table,
Id,
Cidr,
Organization,
SigningCA,
CreatedAt,
Name,
LighthousesAsRelays,
}

View File

@ -0,0 +1,49 @@
use crate::m20230402_232316_create_table_organizations::Organization;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Role::Table)
.col(ColumnDef::new(Role::Id).string().not_null().primary_key())
.col(ColumnDef::new(Role::Name).string().not_null().unique_key())
.col(ColumnDef::new(Role::Description).string().not_null())
.col(ColumnDef::new(Role::Organization).string().not_null())
.col(ColumnDef::new(Role::CreatedAt).big_integer().not_null())
.col(ColumnDef::new(Role::ModifiedAt).big_integer().not_null())
.foreign_key(
ForeignKey::create()
.from(Role::Table, Role::Organization)
.to(Organization::Table, Organization::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Role::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Role {
Table,
Id,
Name,
Description,
Organization,
CreatedAt,
ModifiedAt,
}

View File

@ -0,0 +1,75 @@
use crate::m20230404_133809_create_table_roles::Role;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(FirewallRule::Table)
.col(
ColumnDef::new(FirewallRule::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(FirewallRule::Role).string().not_null())
.col(ColumnDef::new(FirewallRule::Protocol).string().not_null())
.col(
ColumnDef::new(FirewallRule::Description)
.string()
.not_null(),
)
.col(ColumnDef::new(FirewallRule::AllowedRoleID).string().null())
.col(
ColumnDef::new(FirewallRule::PortRangeFrom)
.integer()
.not_null(),
)
.col(
ColumnDef::new(FirewallRule::PortRangeTo)
.integer()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.from(FirewallRule::Table, FirewallRule::Role)
.to(Role::Table, Role::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(FirewallRule::Table, FirewallRule::AllowedRoleID)
.to(Role::Table, Role::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(FirewallRule::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum FirewallRule {
Table,
Id,
Role,
Protocol,
Description,
AllowedRoleID,
PortRangeFrom,
PortRangeTo,
}

View File

@ -0,0 +1,91 @@
use crate::m20230403_173431_create_table_networks::Network;
use crate::m20230404_133809_create_table_roles::Role;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Host::Table)
.col(ColumnDef::new(Host::Id).string().not_null().primary_key())
.col(ColumnDef::new(Host::Name).string().not_null())
.col(ColumnDef::new(Host::Network).string().not_null())
.col(ColumnDef::new(Host::Role).string().not_null())
.col(ColumnDef::new(Host::IP).string().not_null())
.col(ColumnDef::new(Host::ListenPort).unsigned().not_null())
.col(ColumnDef::new(Host::IsLighthouse).boolean().not_null())
.col(ColumnDef::new(Host::IsRelay).boolean().not_null())
.col(ColumnDef::new(Host::Counter).unsigned().not_null())
.col(ColumnDef::new(Host::CreatedAt).big_integer().not_null())
.col(ColumnDef::new(Host::IsBlocked).boolean().not_null())
.col(ColumnDef::new(Host::LastSeenAt).big_integer().not_null())
.col(ColumnDef::new(Host::LastVersion).integer().not_null())
.col(ColumnDef::new(Host::LastPlatform).string().not_null())
.col(ColumnDef::new(Host::LastOutOfDate).boolean().not_null())
.foreign_key(
ForeignKey::create()
.from(Host::Table, Host::Network)
.to(Network::Table, Network::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.foreign_key(
ForeignKey::create()
.from(Host::Table, Host::Role)
.to(Role::Table, Role::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.index(
Index::create()
.name("idx-hosts-net-name-unique")
.table(Host::Table)
.col(Host::Network)
.col(Host::Name)
.unique(),
)
.index(
Index::create()
.name("idx-hosts-net-ip-unique")
.table(Host::Table)
.col(Host::Network)
.col(Host::IP)
.unique(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Host::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Host {
Table,
Id,
Name,
Network,
Role,
IP,
ListenPort,
IsLighthouse,
IsRelay,
Counter,
CreatedAt,
IsBlocked,
LastSeenAt,
LastVersion,
LastPlatform,
LastOutOfDate,
}

View File

@ -0,0 +1,52 @@
use crate::m20230427_170037_create_table_hosts::Host;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(HostStaticAddress::Table)
.col(
ColumnDef::new(HostStaticAddress::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(HostStaticAddress::Host).string().not_null())
.col(
ColumnDef::new(HostStaticAddress::Address)
.string()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.from(HostStaticAddress::Table, HostStaticAddress::Host)
.to(Host::Table, Host::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(HostStaticAddress::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum HostStaticAddress {
Table,
Id,
Host,
Address,
}

View File

@ -0,0 +1,62 @@
use crate::m20230427_170037_create_table_hosts::Host;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(HostConfigOverride::Table)
.col(
ColumnDef::new(HostConfigOverride::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(HostConfigOverride::Key).string().not_null())
.col(
ColumnDef::new(HostConfigOverride::Value)
.string()
.not_null(),
)
.col(ColumnDef::new(HostConfigOverride::Host).string().not_null())
.foreign_key(
ForeignKey::create()
.from(HostConfigOverride::Table, HostConfigOverride::Host)
.to(Host::Table, Host::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.index(
Index::create()
.name("idx_hosts_config_overrides-key-host-unique")
.table(HostConfigOverride::Table)
.col(HostConfigOverride::Key)
.col(HostConfigOverride::Id)
.unique(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(HostConfigOverride::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum HostConfigOverride {
Table,
Host,
Id,
Key,
Value,
}

View File

@ -0,0 +1,52 @@
use crate::m20230427_170037_create_table_hosts::Host;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(HostEnrollmentCode::Table)
.col(
ColumnDef::new(HostEnrollmentCode::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(HostEnrollmentCode::Host).string().not_null())
.col(
ColumnDef::new(HostEnrollmentCode::ExpiresOn)
.big_integer()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.from(HostEnrollmentCode::Table, HostEnrollmentCode::Host)
.to(Host::Table, Host::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(HostEnrollmentCode::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum HostEnrollmentCode {
Table,
Id,
Host,
ExpiresOn,
}

View File

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(trifid_api_migration::Migrator).await;
}

View File

@ -0,0 +1,41 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "api_key")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub key: String,
pub organization: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::api_key_scope::Entity")]
ApiKeyScope,
#[sea_orm(
belongs_to = "super::organization::Entity",
from = "Column::Organization",
to = "super::organization::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
Organization,
}
impl Related<super::api_key_scope::Entity> for Entity {
fn to() -> RelationDef {
Relation::ApiKeyScope.def()
}
}
impl Related<super::organization::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organization.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "api_key_scope")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub scope: String,
pub api_key: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::api_key::Entity",
from = "Column::ApiKey",
to = "super::api_key::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
ApiKey,
}
impl Related<super::api_key::Entity> for Entity {
fn to() -> RelationDef {
Relation::ApiKey.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,32 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "auth_token")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub user: String,
pub expires_on: i64,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::User",
to = "super::user::Column::Id",
on_update = "Cascade",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

Some files were not shown because too many files have changed in this diff Show More