host blocking + general code cleanup

This commit is contained in:
c0repwn3r 2023-05-10 20:32:19 -04:00
parent a068741986
commit 6d01d8703b
Signed by: core
GPG Key ID: FDBF740DADDCEECF
49 changed files with 2745 additions and 1598 deletions

View File

@ -14,93 +14,125 @@
// 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 actix_web::HttpRequest;
use std::error::Error;
use sea_orm::{ColumnTrait, Condition, DatabaseConnection, EntityTrait, QueryFilter};
use crate::timers::expired;
use crate::tokens::get_token_type;
use trifid_api_entities::entity::{auth_token, session_token};
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 crate::timers::expired;
use trifid_api_entities::entity::{auth_token, session_token};
pub enum TokenInfo {
SessionToken(SessionTokenInfo),
AuthToken(AuthTokenInfo),
ApiToken(ApiTokenInfo),
NotPresent
NotPresent,
}
pub struct SessionTokenInfo {
pub token: String,
pub user: SessionTokenUser,
pub expires_at: i64
pub expires_at: i64,
}
pub struct SessionTokenUser {
pub id: String,
pub email: String
pub email: String,
}
pub struct ApiTokenInfo {
pub scopes: Vec<String>,
pub organization: String
pub organization: String,
}
pub struct AuthTokenInfo {
pub token: String,
pub session_info: SessionTokenInfo
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")?;
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())
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 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")?;
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")?;
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
email: user.email,
},
expires_at: token.expires_on,
}))
}
pub async fn enforce_2fa(req: &HttpRequest, db: &DatabaseConnection) -> Result<TokenInfo, Box<dyn Error>> {
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!()
_ => unreachable!(),
};
let header = req.headers().get("Authorization").ok_or("Missing authorization header")?;
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())
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 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")?;
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());
@ -112,17 +144,28 @@ pub async fn enforce_2fa(req: &HttpRequest, db: &DatabaseConnection) -> Result<T
}))
}
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")?;
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())
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")?;
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:
@ -135,10 +178,19 @@ pub async fn enforce_api_token(req: &HttpRequest, scopes: &[&str], db: &Database
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: 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 {

View File

@ -14,11 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::fs;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use log::error;
use once_cell::sync::Lazy;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use std::fs;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
pub static CONFIG: Lazy<TrifidConfig> = Lazy::new(|| {
let config_str = match fs::read_to_string("/etc/trifid/config.toml") {
@ -43,7 +43,7 @@ pub struct TrifidConfig {
pub database: TrifidConfigDatabase,
pub server: TrifidConfigServer,
pub tokens: TrifidConfigTokens,
pub crypto: TrifidConfigCryptography
pub crypto: TrifidConfigCryptography,
}
#[derive(Serialize, Deserialize, Debug)]
@ -62,13 +62,13 @@ pub struct TrifidConfigDatabase {
#[serde(default = "time_defaults")]
pub max_lifetime: u64,
#[serde(default = "sqlx_logging_default")]
pub sqlx_logging: bool
pub sqlx_logging: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrifidConfigServer {
#[serde(default = "socketaddr_8080")]
pub bind: SocketAddr
pub bind: SocketAddr,
}
#[derive(Serialize, Deserialize, Debug)]
@ -80,20 +80,38 @@ pub struct TrifidConfigTokens {
#[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
pub mfa_tokens_expiry_time_seconds: u64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TrifidConfigCryptography {
pub data_encryption_key: String
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 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

View File

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

View File

@ -14,13 +14,13 @@
// 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 base64::Engine;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use std::error::Error;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Cursor {
pub page: u64
pub page: u64,
}
impl TryFrom<Cursor> for String {
@ -41,9 +41,7 @@ impl TryFrom<String> for Cursor {
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
// If empty, it's page 0
return Ok(Cursor {
page: 0
})
return Ok(Cursor { page: 0 });
}
// Base64-decode the value
let json_bytes = base64::engine::general_purpose::STANDARD.decode(value)?;

View File

@ -15,11 +15,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use actix_web::error::{JsonPayloadError, PayloadError};
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct APIErrorsResponse {
pub errors: Vec<APIError>
pub errors: Vec<APIError>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct APIError {
@ -27,10 +27,12 @@ pub struct APIError {
pub message: String,
#[serde(skip_serializing_if = "is_none")]
#[serde(default)]
pub path: Option<String>
pub path: Option<String>,
}
fn is_none<T>(o: &Option<T>) -> bool { o.is_none() }
fn is_none<T>(o: &Option<T>) -> bool {
o.is_none()
}
impl From<&JsonPayloadError> for APIError {
fn from(value: &JsonPayloadError) -> Self {
@ -87,58 +89,44 @@ impl From<&JsonPayloadError> for APIError {
impl From<&PayloadError> for APIError {
fn from(value: &PayloadError) -> Self {
match value {
PayloadError::Incomplete(e) => {
APIError {
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)
Some(e) => format!("Payload reached EOF but was incomplete: {}", e),
},
path: None,
}
}
PayloadError::EncodingCorrupted => {
APIError {
},
PayloadError::EncodingCorrupted => APIError {
code: "ERR_CORRUPTED_PAYLOAD".to_string(),
message: "Payload content encoding corrupted".to_string(),
path: None,
}
}
PayloadError::Overflow => {
APIError {
},
PayloadError::Overflow => APIError {
code: "ERR_PAYLOAD_OVERFLOW".to_string(),
message: "Payload reached size limit".to_string(),
path: None,
}
}
PayloadError::UnknownLength => {
APIError {
},
PayloadError::UnknownLength => APIError {
code: "ERR_PAYLOAD_UNKNOWN_LENGTH".to_string(),
message: "Unable to determine payload length".to_string(),
path: None,
}
}
PayloadError::Http2Payload(e) => {
APIError {
},
PayloadError::Http2Payload(e) => APIError {
code: "ERR_HTTP2_ERROR".to_string(),
message: format!("HTTP/2 error: {}", e),
path: None,
}
}
PayloadError::Io(e) => {
APIError {
},
PayloadError::Io(e) => APIError {
code: "ERR_IO_ERROR".to_string(),
message: format!("I/O error: {}", e),
path: None,
}
}
_ => {
APIError {
},
_ => APIError {
code: "ERR_UNKNOWN_ERROR".to_string(),
message: "An unknown error has occured".to_string(),
path: None,
}
}
},
}
}
}

View File

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

View File

@ -14,30 +14,33 @@
// 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 std::time::Duration;
use actix_request_identifier::RequestIdentifier;
use actix_web::{App, HttpResponse, HttpServer, web::{Data, JsonConfig}};
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::time::Duration;
use trifid_api_migration::{Migrator, MigratorTrait};
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::tokens::random_id_no_id;
use trifid_api_migration::{Migrator, MigratorTrait};
pub mod config;
pub mod routes;
pub mod error;
pub mod tokens;
pub mod timers;
pub mod magic_link;
pub mod auth_tokens;
pub mod cursor;
pub mod config;
pub mod crypto;
pub mod cursor;
pub mod error;
pub mod magic_link;
pub mod routes;
pub mod timers;
pub mod tokens;
pub struct AppState {
pub conn: DatabaseConnection
pub conn: DatabaseConnection,
}
#[actix_web::main]
@ -61,9 +64,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
info!("Performing database migration...");
Migrator::up(&db, None).await?;
let data = Data::new(AppState {
conn: db
});
let data = Data::new(AppState { conn: db });
HttpServer::new(move || {
App::new()
@ -73,11 +74,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
actix_web::error::InternalError::from_response(
err,
HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
api_error
],
})
).into()
errors: vec![api_error],
}),
)
.into()
}))
.wrap(RequestIdentifier::with_generator(random_id_no_id))
.service(routes::v1::auth::magic_link::magic_link_request)
@ -100,7 +100,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
.service(routes::v1::hosts::get_host)
.service(routes::v1::hosts::delete_host)
.service(routes::v1::hosts::edit_host)
}).bind(CONFIG.server.bind)?.run().await?;
.service(routes::v1::hosts::block_host)
})
.bind(CONFIG.server.bind)?
.run()
.await?;
Ok(())
}

View File

@ -19,29 +19,29 @@
// 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 actix_web::{HttpResponse, post};
use actix_web::web::{Data, Json};
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Serialize, Deserialize};
use trifid_api_entities::entity::user::Entity as UserEntity;
use trifid_api_entities::entity::user;
use crate::AppState;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::magic_link::send_magic_link;
use crate::timers::expires_in_seconds;
use crate::tokens::random_token;
use crate::AppState;
use actix_web::web::{Data, Json};
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, Clone, Debug)]
pub struct MagicLinkRequest {
pub email: String
pub email: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MagicLinkResponse {
pub data: MagicLinkResponseData,
pub metadata: MagicLinkResponseMetadata
pub metadata: MagicLinkResponseMetadata,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MagicLinkResponseData {}
@ -50,19 +50,23 @@ pub struct MagicLinkResponseMetadata {}
#[post("/v1/auth/magic-link")]
pub async fn magic_link_request(data: Data<AppState>, req: Json<MagicLinkRequest>) -> HttpResponse {
let user: Option<user::Model> = match UserEntity::find().filter(user::Column::Email.eq(&req.email)).one(&data.conn).await {
let user: Option<user::Model> = match UserEntity::find()
.filter(user::Column::Email.eq(&req.email))
.one(&data.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
};
@ -70,13 +74,11 @@ pub async fn magic_link_request(data: Data<AppState>, req: Json<MagicLinkRequest
Some(u) => u,
None => {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_USER_DOES_NOT_EXIST".to_string(),
message: "That user does not exist.".to_string(),
path: None,
}
],
}],
})
}
};
@ -92,14 +94,14 @@ pub async fn magic_link_request(data: Data<AppState>, req: Json<MagicLinkRequest
Err(e) => {
error!("error sending magic link: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
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(),
message:
"There was an error sending the magic link email, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}
@ -110,19 +112,19 @@ pub async fn magic_link_request(data: Data<AppState>, req: Json<MagicLinkRequest
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".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,3 +1,3 @@
pub mod magic_link;
pub mod verify_magic_link;
pub mod totp;
pub mod verify_magic_link;

View File

@ -19,95 +19,95 @@
// 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 actix_web::{HttpRequest, HttpResponse, post};
use actix_web::web::{Data, Json};
use log::{debug, error};
use serde::{Serialize, Deserialize};
use trifid_api_entities::entity::totp_authenticator;
use crate::AppState;
use crate::auth_tokens::{enforce_session, TokenInfo};
use crate::error::{APIError, APIErrorsResponse};
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, IntoActiveModel, ActiveModelTrait};
use crate::AppState;
use actix_web::web::{Data, Json};
use actix_web::{post, HttpRequest, HttpResponse};
use log::{debug, error};
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::totp_authenticator;
use totp_rs::{Secret, TOTP};
use trifid_api_entities::entity::auth_token;
use crate::config::CONFIG;
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, Debug, Clone)]
pub struct TotpRequest {
pub code: String
pub code: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpResponse {
pub data: TotpResponseData,
pub metadata: TotpResponseMetadata
pub metadata: TotpResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpResponseData {
#[serde(rename = "authToken")]
pub auth_token: String
pub auth_token: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TotpResponseMetadata {}
#[post("/v1/auth/totp")]
pub async fn totp_request(req: Json<TotpRequest>, req_data: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn totp_request(
req: Json<TotpRequest>,
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 {
Ok(r) => match r {
TokenInfo::SessionToken(i) => i,
_ => unreachable!()
}
}
_ => unreachable!(),
},
Err(e) => {
error!("error enforcing session: {}", e);
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
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::User.eq(&session_token.user.id)).one(&db.conn).await {
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 {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".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
},
Some(a) => a,
None => {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_USER_NO_TOTP".to_string(),
message: "This user does not have a totp authenticator".to_string(),
path: None,
}
]
}],
});
}
};
@ -118,30 +118,26 @@ pub async fn totp_request(req: Json<TotpRequest>, req_data: HttpRequest, db: Dat
Err(e) => {
error!("totp url error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_SECRET_ERROR".to_string(),
message: "There was an error parsing the totpmachine. Please try again later.".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 {
errors: vec![APIError {
code: "ERR_TIME_ERROR".to_string(),
message: "There was an with the server-side time clock.".to_string(),
path: None,
}
],
}],
});
}
};
@ -150,14 +146,12 @@ pub async fn totp_request(req: Json<TotpRequest>, req_data: HttpRequest, db: Dat
debug!("current: {}", totpmachine.generate_current().unwrap());
error!("user send invalid totp code");
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}
],
})
}],
});
}
let model: auth_token::Model = auth_token::Model {
@ -172,13 +166,11 @@ pub async fn totp_request(req: Json<TotpRequest>, req_data: HttpRequest, db: Dat
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error issuing the authentication token.".to_string(),
path: None,
}
],
}],
});
}
}

View File

@ -19,56 +19,65 @@
// 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 actix_web::{HttpResponse, post};
use actix_web::web::{Data, Json};
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter};
use serde::{Serialize, Deserialize};
use crate::AppState;
use trifid_api_entities::entity::magic_link;
use trifid_api_entities::entity::magic_link::Model;
use trifid_api_entities::entity::session_token;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::timers::{expired, expires_in_seconds};
use crate::tokens::random_token;
use crate::AppState;
use actix_web::web::{Data, Json};
use actix_web::{post, HttpResponse};
use log::error;
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, Debug, Clone)]
pub struct VerifyMagicLinkRequest {
#[serde(rename = "magicLinkToken")]
pub magic_link_token: String
pub magic_link_token: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyMagicLinkResponse {
pub data: VerifyMagicLinkResponseData,
pub metadata: VerifyMagicLinkResponseMetadata
pub metadata: VerifyMagicLinkResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyMagicLinkResponseData {
#[serde(rename = "sessionToken")]
pub session_token: String
pub session_token: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyMagicLinkResponseMetadata {}
#[post("/v1/auth/verify-magic-link")]
pub async fn verify_magic_link_request(db: Data<AppState>, req: Json<VerifyMagicLinkRequest>) -> HttpResponse {
let link: Option<Model> = match magic_link::Entity::find().filter(magic_link::Column::Id.eq(&req.magic_link_token)).one(&db.conn).await {
pub async fn verify_magic_link_request(
db: Data<AppState>,
req: Json<VerifyMagicLinkRequest>,
) -> 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) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
};
@ -76,27 +85,23 @@ pub async fn verify_magic_link_request(db: Data<AppState>, req: Json<VerifyMagic
Some(l) => l,
None => {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None
}
]
path: None,
}],
})
}
};
if expired(link.expires_on as u64) {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_EXPIRED".to_string(),
message: "Magic link token expired".to_string(),
path: None
}
]
})
path: None,
}],
});
}
let user = link.user.clone();
@ -106,14 +111,14 @@ pub async fn verify_magic_link_request(db: Data<AppState>, req: Json<VerifyMagic
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}
@ -130,23 +135,21 @@ pub async fn verify_magic_link_request(db: Data<AppState>, req: Json<VerifyMagic
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}
HttpResponse::Ok().json(
VerifyMagicLinkResponse {
HttpResponse::Ok().json(VerifyMagicLinkResponse {
data: VerifyMagicLinkResponseData {
session_token: token,
},
metadata: VerifyMagicLinkResponseMetadata {},
}
)
})
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
pub mod auth;
pub mod signup;
pub mod totp_authenticators;
pub mod verify_totp_authenticators;
pub mod hosts;
pub mod networks;
pub mod organization;
pub mod roles;
pub mod signup;
pub mod totp_authenticators;
pub mod trifid;
pub mod hosts;
pub mod verify_totp_authenticators;

View File

@ -24,24 +24,24 @@
// 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 serde::{Serialize, Deserialize};
use actix_web::{get, HttpRequest, HttpResponse};
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 crate::AppState;
use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo};
use crate::error::{APIError, APIErrorsResponse};
use trifid_api_entities::entity::organization;
use serde::{Deserialize, Serialize};
use trifid_api_entities::entity::network;
use crate::cursor::Cursor;
use crate::timers::TIME_FORMAT;
use trifid_api_entities::entity::organization;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponse {
pub data: Vec<GetNetworksResponseData>,
pub metadata: GetNetworksResponseMetadata
pub metadata: GetNetworksResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -56,7 +56,7 @@ pub struct GetNetworksResponseData {
pub created_at: String, // 2023-03-22T18:55:47.009Z
pub name: String,
#[serde(rename = "lighthousesAsRelays")]
pub lighthouses_as_relays: bool
pub lighthouses_as_relays: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -72,16 +72,18 @@ pub struct GetNetworksResponseMetadata {
#[serde(default, rename = "nextCursor")]
pub next_cursor: Option<String>,
#[serde(default)]
pub page: Option<GetNetworksResponseMetadataPage>
pub page: Option<GetNetworksResponseMetadataPage>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponseMetadataPage {
pub count: u64,
pub start: u64
pub start: u64,
}
fn u64_25() -> u64 { 25 }
fn u64_25() -> u64 {
25
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksQueryParams {
@ -90,17 +92,27 @@ pub struct GetNetworksQueryParams {
#[serde(default)]
pub cursor: String,
#[serde(default = "u64_25", rename = "pageSize")]
pub page_size: u64
pub page_size: u64,
}
#[get("/v1/networks")]
pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
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);
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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -109,11 +121,13 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -122,7 +136,7 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
path: None
}
],
})
});
}
let org = match api_token_info {
@ -131,10 +145,14 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
// 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!()
_ => unreachable!(),
};
let org = match organization::Entity::find().filter(organization::Column::Owner.eq(user.id)).one(&db.conn).await {
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);
@ -161,7 +179,7 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
path: None
}
],
})
});
}
}
};
@ -171,18 +189,19 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
Err(e) => {
error!("invalid cursor: {}", e);
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_INVALID_CURSOR".to_string(),
message: "The provided cursor was invalid, please try again later.".to_string(),
path: None
}
],
})
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 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,
@ -230,17 +249,22 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
});
}
};
let models_mapped: Vec<GetNetworksResponseData> = models.iter().map(|u| {
GetNetworksResponseData {
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(),
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();
})
.collect();
let count = models_mapped.len() as u64;
@ -251,17 +275,25 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
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() {
match (Cursor {
page: cursor.page - 1,
})
.try_into()
{
Ok(r) => Some(r),
Err(_) => None
Err(_) => None,
}
} else {
None
},
next_cursor: if cursor.page + 1 != pages {
match (Cursor { page: cursor.page + 1 }).try_into() {
match (Cursor {
page: cursor.page + 1,
})
.try_into()
{
Ok(r) => Some(r),
Err(_) => None
Err(_) => None,
}
} else {
None
@ -271,19 +303,31 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
count,
start: opts.page_size * cursor.page,
})
} else { None },
} else {
None
},
},
})
}
#[get("/v1/networks/{network_id}")]
pub async fn get_network_request(net: Path<String>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
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);
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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -292,11 +336,13 @@ pub async fn get_network_request(net: Path<String>, req_info: HttpRequest, db: D
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -305,10 +351,14 @@ pub async fn get_network_request(net: Path<String>, req_info: HttpRequest, db: D
path: None
}
],
})
});
}
let network: Option<network::Model> = match network::Entity::find().filter(network::Column::Id.eq(net.into_inner())).one(&db.conn).await {
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);
@ -331,7 +381,11 @@ pub async fn get_network_request(net: Path<String>, req_info: HttpRequest, db: D
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(),
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,
},
@ -339,22 +393,19 @@ pub async fn get_network_request(net: Path<String>, req_info: HttpRequest, db: D
})
} else {
HttpResponse::NotFound().json(APIErrorsResponse {
errors: vec![
APIError {
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
pub metadata: GetNetworkResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworkResponseMetadata {}

View File

@ -19,59 +19,66 @@
// 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, UNIX_EPOCH};
use actix_web::{HttpRequest, HttpResponse};
use actix_web::web::{Data, Json};
use serde::{Serialize, Deserialize};
use crate::AppState;
use actix_web::post;
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use trifid_pki::cert::{NebulaCertificate, NebulaCertificateDetails, serialize_x25519_private};
use trifid_pki::ed25519_dalek::SigningKey;
use trifid_pki::rand_core::OsRng;
use trifid_api_entities::entity::{network, organization, signing_ca};
use crate::auth_tokens::{enforce_2fa, TokenInfo};
use crate::config::CONFIG;
use crate::crypto::{encrypt_with_nonce, generate_random_iv, get_cipher_from_config};
use crate::error::{APIError, APIErrorsResponse};
use crate::tokens::random_id;
use crate::AppState;
use actix_web::post;
use actix_web::web::{Data, Json};
use actix_web::{HttpRequest, HttpResponse};
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use trifid_api_entities::entity::{network, organization, signing_ca};
use trifid_pki::cert::{serialize_x25519_private, NebulaCertificate, NebulaCertificateDetails};
use trifid_pki::ed25519_dalek::SigningKey;
use trifid_pki::rand_core::OsRng;
#[derive(Serialize, Deserialize)]
pub struct OrgCreateRequest {
pub cidr: String
pub cidr: String,
}
#[derive(Serialize, Deserialize)]
pub struct OrgCreateResponse {
pub organization: String,
pub ca: String,
pub network: String
pub network: String,
}
#[post("/v1/organization")]
pub async fn create_org_request(req: Json<OrgCreateRequest>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn create_org_request(
req: Json<OrgCreateRequest>,
req_info: HttpRequest,
db: Data<AppState>,
) -> 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);
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 {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}
],
}],
})
}
};
let org = match organization::Entity::find().filter(organization::Column::Owner.eq(&user.id)).one(&db.conn).await {
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);
@ -89,14 +96,12 @@ pub async fn create_org_request(req: Json<OrgCreateRequest>, req_info: HttpReque
if org.is_some() {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_USER_ALREADY_OWNS_ORG".to_string(),
message: "This user already owns an organization".to_string(),
path: None,
}
],
})
}],
});
}
let org = organization::Model {
@ -201,7 +206,12 @@ pub async fn create_org_request(req: Json<OrgCreateRequest>, req_info: HttpReque
organization: org.id.clone(),
cert: ca_key_encrypted,
key: ca_crt,
expires: cert.details.not_after.duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
expires: cert
.details
.not_after
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64,
nonce: iv_hex,
};
@ -210,7 +220,10 @@ pub async fn create_org_request(req: Json<OrgCreateRequest>, req_info: HttpReque
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,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64,
name: "Network1".to_string(),
lighthouses_as_relays: true,
};

View File

@ -35,24 +35,27 @@
// 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.
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{get, HttpRequest, HttpResponse, post, put};
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::tokens::random_id;
use crate::AppState;
use actix_web::delete;
use actix_web::web::{Data, Json, Path, Query};
use actix_web::{get, post, put, HttpRequest, HttpResponse};
use chrono::{TimeZone, Utc};
use log::error;
use serde::{Deserialize, Serialize};
use crate::AppState;
use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo};
use crate::error::{APIError, APIErrorsResponse};
use trifid_api_entities::entity::organization;
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, IntoActiveModel, ActiveModelTrait, QueryOrder, PaginatorTrait, ModelTrait};
use trifid_api_entities::entity::firewall_rule;
use trifid_api_entities::entity::role;
use crate::cursor::Cursor;
use crate::tokens::random_id;
use actix_web::delete;
use sea_orm::ActiveValue::Set;
use crate::timers::TIME_FORMAT;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait,
QueryFilter, QueryOrder,
};
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
use trifid_api_entities::entity::firewall_rule;
use trifid_api_entities::entity::organization;
use trifid_api_entities::entity::role;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateRoleRequest {
@ -61,7 +64,7 @@ pub struct CreateRoleRequest {
#[serde(default)]
pub description: String,
#[serde(default, rename = "firewallRules")]
pub firewall_rules: Vec<RoleFirewallRule>
pub firewall_rules: Vec<RoleFirewallRule>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -70,7 +73,7 @@ pub struct UpdateRoleRequest {
#[serde(default)]
pub description: String,
#[serde(default, rename = "firewallRules")]
pub firewall_rules: Vec<RoleFirewallRule>
pub firewall_rules: Vec<RoleFirewallRule>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -93,19 +96,19 @@ pub enum RoleProtocol {
#[serde(rename = "UDP")]
Udp,
#[serde(rename = "ICMP")]
Icmp
Icmp,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RolePortRange {
pub from: u16,
pub to: u16
pub to: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RoleCreateResponse {
pub data: RoleResponse,
pub metadata: RoleCreateResponseMetadata
pub metadata: RoleCreateResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -118,20 +121,30 @@ pub struct RoleResponse {
#[serde(rename = "createdAt")]
pub created_at: String,
#[serde(rename = "modifiedAt")]
pub modified_at: String
pub modified_at: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RoleCreateResponseMetadata {}
#[post("/v1/roles")]
pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn create_role_request(
req: Json<CreateRoleRequest>,
req_info: HttpRequest,
db: Data<AppState>,
) -> HttpResponse {
// For this endpoint, you either need to be a fully authenticated user OR a token with roles:create
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:create"], &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:create"], &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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -140,11 +153,13 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -153,7 +168,7 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
path: None
}
],
})
});
}
let org = match api_token_info {
@ -162,10 +177,14 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
// 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!()
_ => unreachable!(),
};
let org = match organization::Entity::find().filter(organization::Column::Owner.eq(user.id)).one(&db.conn).await {
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);
@ -192,7 +211,7 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
path: None
}
],
})
});
}
}
};
@ -202,20 +221,36 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
name: req.name.clone(),
description: req.description.clone(),
organization: org,
created_at: SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
modified_at: SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64,
created_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64,
modified_at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64,
};
let firewall_rules: Vec<firewall_rule::Model> = req.firewall_rules.iter().map(|i| {
firewall_rule::Model {
let firewall_rules: Vec<firewall_rule::Model> = req
.firewall_rules
.iter()
.map(|i| firewall_rule::Model {
id: random_id("rule"),
role: new_role_model.id.clone(),
protocol: i.protocol.to_string(),
description: i.description.clone(),
allowed_role_id: i.allowed_role_id.clone(),
port_range_from: i.port_range.as_ref().unwrap_or(&RolePortRange { from: 0, to: 65535 }).from as i32,
port_range_to: i.port_range.as_ref().unwrap_or(&RolePortRange { from: 0, to: 65535 }).to as i32,
}
}).collect();
port_range_from: i
.port_range
.as_ref()
.unwrap_or(&RolePortRange { from: 0, to: 65535 })
.from as i32,
port_range_to: i
.port_range
.as_ref()
.unwrap_or(&RolePortRange { from: 0, to: 65535 })
.to as i32,
})
.collect();
let new_role_model_clone = new_role_model.clone();
let firewall_rules_clone = firewall_rules.clone();
@ -226,14 +261,13 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error creating the new role. Please try again later".to_string(),
path: None
}
],
})
message: "There was an error creating the new role. Please try again later"
.to_string(),
path: None,
}],
});
}
}
@ -244,14 +278,13 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error creating the new role. Please try again later".to_string(),
path: None
}
],
})
message: "There was an error creating the new role. Please try again later"
.to_string(),
path: None,
}],
});
}
}
}
@ -262,8 +295,16 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
name: Some(new_role_model_clone.name.clone()),
description: Some(new_role_model_clone.description),
firewall_rules: req.firewall_rules.clone(),
created_at: Utc.timestamp_opt(new_role_model_clone.created_at, 0).unwrap().format(TIME_FORMAT).to_string(),
modified_at: Utc.timestamp_opt(new_role_model_clone.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
created_at: Utc
.timestamp_opt(new_role_model_clone.created_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
modified_at: Utc
.timestamp_opt(new_role_model_clone.modified_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
},
metadata: RoleCreateResponseMetadata {},
})
@ -275,7 +316,7 @@ impl ToString for RoleProtocol {
RoleProtocol::Any => "ANY".to_string(),
RoleProtocol::Tcp => "TCP".to_string(),
RoleProtocol::Udp => "UDP".to_string(),
RoleProtocol::Icmp => "ICMP".to_string()
RoleProtocol::Icmp => "ICMP".to_string(),
}
}
}
@ -287,15 +328,17 @@ pub struct ListRolesRequestOpts {
#[serde(default)]
pub cursor: String,
#[serde(default = "page_default", rename = "pageSize")]
pub page_size: u64
pub page_size: u64,
}
fn page_default() -> u64 { 25 }
fn page_default() -> u64 {
25
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetRolesResponse {
pub data: Vec<RoleResponse>,
pub metadata: GetRolesResponseMetadata
pub metadata: GetRolesResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -311,23 +354,33 @@ pub struct GetRolesResponseMetadata {
#[serde(default, rename = "nextCursor")]
pub next_cursor: Option<String>,
#[serde(default)]
pub page: Option<GetRolesResponseMetadataPage>
pub page: Option<GetRolesResponseMetadataPage>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetRolesResponseMetadataPage {
pub count: u64,
pub start: u64
pub start: u64,
}
#[get("/v1/roles")]
pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn get_roles(
opts: Query<ListRolesRequestOpts>,
req_info: HttpRequest,
db: Data<AppState>,
) -> HttpResponse {
// For this endpoint, you either need to be a fully authenticated user OR a token with roles:list
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:list"], &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles: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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -336,11 +389,13 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -349,7 +404,7 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
path: None
}
],
})
});
}
let org = match api_token_info {
@ -358,10 +413,14 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
// 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!()
_ => unreachable!(),
};
let org = match organization::Entity::find().filter(organization::Column::Owner.eq(user.id)).one(&db.conn).await {
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);
@ -388,7 +447,7 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
path: None
}
],
})
});
}
}
};
@ -398,18 +457,19 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
Err(e) => {
error!("invalid cursor: {}", e);
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_INVALID_CURSOR".to_string(),
message: "The provided cursor was invalid, please try again later.".to_string(),
path: None
}
],
})
path: None,
}],
});
}
};
let network_pages = role::Entity::find().filter(role::Column::Organization.eq(org)).order_by_asc(role::Column::CreatedAt).paginate(&db.conn, opts.page_size);
let network_pages = role::Entity::find()
.filter(role::Column::Organization.eq(org))
.order_by_asc(role::Column::CreatedAt)
.paginate(&db.conn, opts.page_size);
let total = match network_pages.num_items().await {
Ok(r) => r,
@ -462,7 +522,11 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
for u in models {
// fetch firewall rules
let rules = match firewall_rule::Entity::find().filter(firewall_rule::Column::Role.eq(&u.id)).all(&db.conn).await {
let rules = match firewall_rule::Entity::find()
.filter(firewall_rule::Column::Role.eq(&u.id))
.all(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
@ -478,18 +542,20 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
}
};
let rules: Vec<RoleFirewallRule> = rules.iter().map(|r| {
let rules: Vec<RoleFirewallRule> = rules
.iter()
.map(|r| {
let protocol = match r.protocol.as_str() {
"ANY" => RoleProtocol::Any,
"TCP" => RoleProtocol::Tcp,
"UDP" => RoleProtocol::Udp,
"ICMP" => RoleProtocol::Icmp,
_ => unreachable!("database has been corrupted or manually edited")
_ => unreachable!("database has been corrupted or manually edited"),
};
let port_range = if r.port_range_from == 0 && r.port_range_to == 65535 || matches!(protocol, RoleProtocol::Icmp) {
let port_range = if r.port_range_from == 0 && r.port_range_to == 65535
|| matches!(protocol, RoleProtocol::Icmp)
{
None
} else {
Some(RolePortRange {
@ -504,15 +570,24 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
allowed_role_id: r.allowed_role_id.clone(),
port_range,
}
}).collect();
})
.collect();
models_mapped.push(RoleResponse {
id: Some(u.id.clone()),
name: Some(u.name),
description: Some(u.description),
firewall_rules: rules,
created_at: Utc.timestamp_opt(u.created_at, 0).unwrap().format(TIME_FORMAT).to_string(),
modified_at: Utc.timestamp_opt(u.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
created_at: Utc
.timestamp_opt(u.created_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
modified_at: Utc
.timestamp_opt(u.modified_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
})
}
@ -525,17 +600,25 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
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() {
match (Cursor {
page: cursor.page - 1,
})
.try_into()
{
Ok(r) => Some(r),
Err(_) => None
Err(_) => None,
}
} else {
None
},
next_cursor: if cursor.page + 1 != pages {
match (Cursor { page: cursor.page + 1 }).try_into() {
match (Cursor {
page: cursor.page + 1,
})
.try_into()
{
Ok(r) => Some(r),
Err(_) => None
Err(_) => None,
}
} else {
None
@ -545,19 +628,31 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
count,
start: opts.page_size * cursor.page,
})
} else { None },
} else {
None
},
},
})
}
#[get("/v1/roles/{role_id}")]
pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn get_role(
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 roles:read
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:read"], &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles: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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -566,11 +661,13 @@ pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppStat
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -579,10 +676,14 @@ pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppStat
path: None
}
],
})
});
}
let role: Option<role::Model> = match role::Entity::find().filter(role::Column::Id.eq(net.into_inner())).one(&db.conn).await {
let role: Option<role::Model> = match role::Entity::find()
.filter(role::Column::Id.eq(net.into_inner()))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
@ -598,8 +699,13 @@ pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppStat
}
};
if let Some(role) = role {// fetch firewall rules
let rules = match firewall_rule::Entity::find().filter(firewall_rule::Column::Role.eq(&role.id)).all(&db.conn).await {
if let Some(role) = role {
// fetch firewall rules
let rules = match firewall_rule::Entity::find()
.filter(firewall_rule::Column::Role.eq(&role.id))
.all(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
@ -615,18 +721,20 @@ pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppStat
}
};
let rules: Vec<RoleFirewallRule> = rules.iter().map(|r| {
let rules: Vec<RoleFirewallRule> = rules
.iter()
.map(|r| {
let protocol = match r.protocol.as_str() {
"ANY" => RoleProtocol::Any,
"TCP" => RoleProtocol::Tcp,
"UDP" => RoleProtocol::Udp,
"ICMP" => RoleProtocol::Icmp,
_ => unreachable!("database has been corrupted or manually edited")
_ => unreachable!("database has been corrupted or manually edited"),
};
let port_range = if r.port_range_from == 0 && r.port_range_to == 65535 || matches!(protocol, RoleProtocol::Icmp) {
let port_range = if r.port_range_from == 0 && r.port_range_to == 65535
|| matches!(protocol, RoleProtocol::Icmp)
{
None
} else {
Some(RolePortRange {
@ -641,8 +749,8 @@ pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppStat
allowed_role_id: r.allowed_role_id.clone(),
port_range,
}
}).collect();
})
.collect();
HttpResponse::Ok().json(GetRoleResponse {
data: RoleResponse {
@ -650,41 +758,56 @@ pub async fn get_role(net: Path<String>, req_info: HttpRequest, db: Data<AppStat
name: Some(role.name.clone()),
description: Some(role.description.clone()),
firewall_rules: rules,
created_at: Utc.timestamp_opt(role.created_at, 0).unwrap().format(TIME_FORMAT).to_string(),
modified_at: Utc.timestamp_opt(role.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
created_at: Utc
.timestamp_opt(role.created_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
modified_at: Utc
.timestamp_opt(role.modified_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
},
metadata: GetRoleResponseMetadata {},
})
} else {
HttpResponse::NotFound().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_MISSING_ROLE".to_string(),
message: "Role does not exist".to_string(),
path: None,
}
],
}],
})
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetRoleResponse {
pub data: RoleResponse,
pub metadata: GetRoleResponseMetadata
pub metadata: GetRoleResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetRoleResponseMetadata {}
#[delete("/v1/roles/{role_id}")]
pub async fn delete_role(role: Path<String>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn delete_role(
role: 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 roles:delete
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:delete"], &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:delete"], &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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -693,11 +816,13 @@ pub async fn delete_role(role: Path<String>, req_info: HttpRequest, db: Data<App
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -706,10 +831,14 @@ pub async fn delete_role(role: Path<String>, req_info: HttpRequest, db: Data<App
path: None
}
],
})
});
}
let role: Option<role::Model> = match role::Entity::find().filter(role::Column::Id.eq(role.into_inner())).one(&db.conn).await {
let role: Option<role::Model> = match role::Entity::find()
.filter(role::Column::Id.eq(role.into_inner()))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
@ -748,13 +877,11 @@ pub async fn delete_role(role: Path<String>, req_info: HttpRequest, db: Data<App
})
} else {
HttpResponse::NotFound().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_MISSING_ROLE".to_string(),
message: "Role does not exist".to_string(),
path: None,
}
],
}],
})
}
}
@ -762,7 +889,7 @@ pub async fn delete_role(role: Path<String>, req_info: HttpRequest, db: Data<App
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RoleDeleteResponse {
data: RoleDeleteResponseData,
metadata: RoleDeleteResponseMetadata
metadata: RoleDeleteResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@ -775,17 +902,28 @@ pub struct RoleDeleteResponseMetadata {}
pub struct RoleUpdateRequest {
pub description: String,
#[serde(rename = "firewallRules")]
pub firewall_rules: Vec<RoleFirewallRule>
pub firewall_rules: Vec<RoleFirewallRule>,
}
#[put("/v1/roles/{role_id}")]
pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
pub async fn update_role_request(
role: Path<String>,
req: Json<RoleUpdateRequest>,
req_info: HttpRequest,
db: Data<AppState>,
) -> HttpResponse {
// For this endpoint, you either need to be a fully authenticated user OR a token with roles:create
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:create"], &db.conn).await.unwrap_or(TokenInfo::NotPresent);
let session_info = enforce_2fa(&req_info, &db.conn)
.await
.unwrap_or(TokenInfo::NotPresent);
let api_token_info = enforce_api_token(&req_info, &["roles:create"], &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) {
if matches!(session_info, TokenInfo::NotPresent)
&& matches!(api_token_info, TokenInfo::NotPresent)
{
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -794,11 +932,13 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
path: None,
}
],
})
});
}
// If both are present, throw an error
if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) {
if matches!(session_info, TokenInfo::AuthToken(_))
&& matches!(api_token_info, TokenInfo::ApiToken(_))
{
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
@ -807,19 +947,23 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
path: None
}
],
})
});
}
let org = match api_token_info {
TokenInfo::ApiToken(tkn) => tkn.organization,
match api_token_info {
TokenInfo::ApiToken(_) => (),
_ => {
// 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!()
_ => unreachable!(),
};
let org = match organization::Entity::find().filter(organization::Column::Owner.eq(user.id)).one(&db.conn).await {
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);
@ -835,9 +979,7 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
}
};
if let Some(org) = org {
org.id
} else {
if org.is_none() {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
@ -846,12 +988,16 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
path: None
}
],
})
});
}
}
};
let existing_rules: Vec<firewall_rule::Model> = match firewall_rule::Entity::find().filter(firewall_rule::Column::Role.eq(role.clone())).all(&db.conn).await {
let existing_rules: Vec<firewall_rule::Model> = match firewall_rule::Entity::find()
.filter(firewall_rule::Column::Role.eq(role.clone()))
.all(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
@ -885,7 +1031,11 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
};
}
let role = match role::Entity::find().filter(role::Column::Id.eq(role.as_str())).one(&db.conn).await {
let role = match role::Entity::find()
.filter(role::Column::Id.eq(role.as_str()))
.one(&db.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
@ -905,20 +1055,23 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
Some(r) => r,
None => {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "This resource does not exist or you do not have permission to access it.".to_string(),
path: None
}
]
message:
"This resource does not exist or you do not have permission to access it."
.to_string(),
path: None,
}],
})
}
};
let mut role_active_model = role.clone().into_active_model();
role_active_model.modified_at = Set(SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64);
role_active_model.modified_at = Set(SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs() as i64);
role_active_model.description = Set(req.description.clone());
let role = match role_active_model.update(&db.conn).await {
@ -937,17 +1090,27 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
}
};
let firewall_rules: Vec<firewall_rule::Model> = req.firewall_rules.iter().map(|i| {
firewall_rule::Model {
let firewall_rules: Vec<firewall_rule::Model> = req
.firewall_rules
.iter()
.map(|i| firewall_rule::Model {
id: random_id("rule"),
role: role.id.clone(),
protocol: i.protocol.to_string(),
description: i.description.clone(),
allowed_role_id: i.allowed_role_id.clone(),
port_range_from: i.port_range.as_ref().unwrap_or(&RolePortRange { from: 0, to: 65535 }).from as i32,
port_range_to: i.port_range.as_ref().unwrap_or(&RolePortRange { from: 0, to: 65535 }).to as i32,
}
}).collect();
port_range_from: i
.port_range
.as_ref()
.unwrap_or(&RolePortRange { from: 0, to: 65535 })
.from as i32,
port_range_to: i
.port_range
.as_ref()
.unwrap_or(&RolePortRange { from: 0, to: 65535 })
.to as i32,
})
.collect();
let firewall_rules_clone = firewall_rules.clone();
@ -958,14 +1121,13 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error creating the new role. Please try again later".to_string(),
path: None
}
],
})
message: "There was an error creating the new role. Please try again later"
.to_string(),
path: None,
}],
});
}
}
}
@ -976,8 +1138,16 @@ pub async fn update_role_request(role: Path<String>, req: Json<RoleUpdateRequest
name: Some(role.name.clone()),
description: Some(role.description),
firewall_rules: req.firewall_rules.clone(),
created_at: Utc.timestamp_opt(role.created_at, 0).unwrap().format(TIME_FORMAT).to_string(),
modified_at: Utc.timestamp_opt(role.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
created_at: Utc
.timestamp_opt(role.created_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
modified_at: Utc
.timestamp_opt(role.modified_at, 0)
.unwrap()
.format(TIME_FORMAT)
.to_string(),
},
metadata: RoleCreateResponseMetadata {},
})

View File

@ -19,29 +19,29 @@
// 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 actix_web::{HttpResponse, post};
use actix_web::web::{Data, Json};
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, QueryFilter};
use serde::{Serialize, Deserialize};
use trifid_api_entities::entity::user::Entity as UserEntity;
use trifid_api_entities::entity::user;
use crate::AppState;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::magic_link::send_magic_link;
use crate::timers::expires_in_seconds;
use crate::tokens::{random_id, random_token};
use crate::AppState;
use actix_web::web::{Data, Json};
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, Clone, Debug)]
pub struct SignupRequest {
pub email: String
pub email: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SignupResponse {
pub data: Option<SignupResponseData>,
pub metadata: SignupResponseMetadata
pub metadata: SignupResponseMetadata,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SignupResponseData {}
@ -50,37 +50,39 @@ pub struct SignupResponseMetadata {}
#[post("/v1/signup")]
pub async fn signup_request(data: Data<AppState>, req: Json<SignupRequest>) -> HttpResponse {
let user: Vec<user::Model> = match UserEntity::find().filter(user::Column::Email.eq(&req.email)).all(&data.conn).await {
let user: Vec<user::Model> = match UserEntity::find()
.filter(user::Column::Email.eq(&req.email))
.all(&data.conn)
.await
{
Ok(r) => r,
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
};
if !user.is_empty() {
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
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()
email: req.email.clone(),
};
let id = model.id.clone();
@ -91,14 +93,14 @@ pub async fn signup_request(data: Data<AppState>, req: Json<SignupRequest>) -> H
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}
@ -113,14 +115,14 @@ pub async fn signup_request(data: Data<AppState>, req: Json<SignupRequest>) -> H
Err(e) => {
error!("error sending magic link: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
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(),
message:
"There was an error sending the magic link email, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}
@ -131,19 +133,19 @@ pub async fn signup_request(data: Data<AppState>, req: Json<SignupRequest>) -> H
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".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,
metadata: SignupResponseMetadata {}
metadata: SignupResponseMetadata {},
})
}

View File

@ -19,19 +19,21 @@
// 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 serde::{Serialize, Deserialize};
use actix_web::{HttpRequest, HttpResponse, post};
use actix_web::web::{Data, Json};
use log::error;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter};
use totp_rs::{Algorithm, Secret, TOTP};
use crate::AppState;
use crate::auth_tokens::{enforce_session, TokenInfo};
use crate::error::{APIError, APIErrorsResponse};
use trifid_api_entities::entity::totp_authenticator;
use crate::config::CONFIG;
use crate::error::{APIError, APIErrorsResponse};
use crate::timers::expires_in_seconds;
use crate::tokens::{random_token};
use crate::tokens::random_token;
use crate::AppState;
use actix_web::web::{Data, Json};
use actix_web::{post, HttpRequest, HttpResponse};
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(Serialize, Deserialize, Debug, Clone)]
pub struct TotpAuthenticatorsRequest {}
@ -54,55 +56,57 @@ pub struct TotpAuthenticatorsResponse {
}
#[post("/v1/totp-authenticators")]
pub async fn totp_authenticators_request(db: Data<AppState>, req_data: HttpRequest, _req: Json<TotpAuthenticatorsRequest>) -> HttpResponse {
pub async fn totp_authenticators_request(
db: Data<AppState>,
req_data: HttpRequest,
_req: Json<TotpAuthenticatorsRequest>,
) -> HttpResponse {
// require a user session
let session_token = match enforce_session(&req_data, &db.conn).await {
Ok(r) => {
match r {
Ok(r) => match r {
TokenInfo::SessionToken(i) => i,
_ => unreachable!()
}
}
_ => unreachable!(),
},
Err(e) => {
error!("error enforcing session: {}", e);
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
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::User.eq(&session_token.user.id)).one(&db.conn).await {
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 {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".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 {
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 {
@ -110,38 +114,48 @@ pub async fn totp_authenticators_request(db: Data<AppState>, req_data: HttpReque
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".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) {
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 {
errors: vec![APIError {
code: "ERR_SECRET_ERR".to_string(),
message: "There was an error configuring the authenticator, please try again later.".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(),
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,
@ -157,14 +171,14 @@ pub async fn totp_authenticators_request(db: Data<AppState>, req_data: HttpReque
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}

View File

@ -32,14 +32,19 @@
// 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::{HttpResponse, get};
use actix_web::{get, HttpResponse};
use serde::{Deserialize, Serialize};
pub const SUPPORTED_EXTENSIONS: &[&str] = &["definednetworking", "trifidextensions", "extended_roles", "extended_hosts"];
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
"definednetworking",
"trifidextensions",
"extended_roles",
"extended_hosts",
];
#[derive(Serialize, Deserialize)]
pub struct TrifidExtensionsResponse {
pub extensions: Vec<String>
pub extensions: Vec<String>,
}
#[get("/v1/trifid_extensions")]

View File

@ -19,81 +19,85 @@
// 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 actix_web::{HttpRequest, HttpResponse, post};
use actix_web::web::{Data, Json};
use log::{debug, error};
use serde::{Serialize, Deserialize};
use trifid_api_entities::entity::totp_authenticator;
use crate::AppState;
use crate::auth_tokens::{enforce_session, TokenInfo};
use crate::error::{APIError, APIErrorsResponse};
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, IntoActiveModel, ActiveModelTrait};
use sea_orm::ActiveValue::Set;
use totp_rs::{Secret, TOTP};
use trifid_api_entities::entity::auth_token;
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
pub code: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyTotpAuthenticatorsResponse {
pub data: VerifyTotpAuthenticatorsResponseData,
pub metadata: VerifyTotpAuthenticatorsResponseMetadata
pub metadata: VerifyTotpAuthenticatorsResponseMetadata,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VerifyTotpAuthenticatorsResponseData {
#[serde(rename = "authToken")]
pub auth_token: String
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 {
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 {
Ok(r) => match r {
TokenInfo::SessionToken(i) => i,
_ => unreachable!()
}
}
_ => unreachable!(),
},
Err(e) => {
error!("error enforcing session: {}", e);
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
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 {
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 {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error with the database request, please try again later.".to_string(),
message:
"There was an error with the database request, please try again later."
.to_string(),
path: None,
}
],
}],
});
}
};
@ -101,26 +105,22 @@ pub async fn verify_totp_authenticators_request(req: Json<VerifyTotpAuthenticato
Some(a) => {
if a.verified {
return HttpResponse::BadRequest().json(APIErrorsResponse {
errors: vec![
APIError {
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 {
errors: vec![APIError {
code: "ERR_USER_NO_TOTP".to_string(),
message: "This user does not have a totp authenticator".to_string(),
path: None,
}
]
}],
});
}
};
@ -131,30 +131,26 @@ pub async fn verify_totp_authenticators_request(req: Json<VerifyTotpAuthenticato
Err(e) => {
error!("totp url error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_SECRET_ERROR".to_string(),
message: "There was an error parsing the totpmachine. Please try again later.".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 {
errors: vec![APIError {
code: "ERR_TIME_ERROR".to_string(),
message: "There was an with the server-side time clock.".to_string(),
path: None,
}
],
}],
});
}
};
@ -163,14 +159,12 @@ pub async fn verify_totp_authenticators_request(req: Json<VerifyTotpAuthenticato
debug!("current: {}", totpmachine.generate_current().unwrap());
error!("user send invalid totp code");
return HttpResponse::Unauthorized().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_UNAUTHORIZED".to_string(),
message: "Unauthorized".to_string(),
path: None,
}
],
})
}],
});
}
let mut active_model = auther.into_active_model();
@ -182,14 +176,13 @@ pub async fn verify_totp_authenticators_request(req: Json<VerifyTotpAuthenticato
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error updating the totpmachine, please try again later.".to_string(),
message: "There was an error updating the totpmachine, please try again later."
.to_string(),
path: None,
}
],
})
}],
});
}
}
@ -205,13 +198,11 @@ pub async fn verify_totp_authenticators_request(req: Json<VerifyTotpAuthenticato
Err(e) => {
error!("database error: {}", e);
return HttpResponse::InternalServerError().json(APIErrorsResponse {
errors: vec![
APIError {
errors: vec![APIError {
code: "ERR_DB_ERROR".to_string(),
message: "There was an error issuing the authentication token.".to_string(),
path: None,
}
],
}],
});
}
}

View File

@ -19,7 +19,10 @@ 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()
(SystemTime::now() + Duration::from_secs(seconds))
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
}
pub fn expired(time: u64) -> bool {

View File

@ -20,7 +20,8 @@ use rand::Rng;
pub const ID_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
pub const ID_LEN: u32 = 26;
pub const TOKEN_CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
pub const TOKEN_CHARSET: &[u8] =
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
pub const TOKEN_LEN: u32 = 43;
// 26
@ -38,16 +39,22 @@ pub fn random_id_no_id() -> HeaderValue {
// 43
// format: [TYPE]-[43 chars]
pub fn random_token(identifier: &str) -> String {
format!("{}-{}", identifier, random_with_charset(TOKEN_LEN, TOKEN_CHARSET))
format!(
"{}-{}",
identifier,
random_with_charset(TOKEN_LEN, TOKEN_CHARSET)
)
}
fn random_with_charset(len: u32, charset: &[u8]) -> String {
(0..len).map(|_| {
(0..len)
.map(|_| {
let idx = rand::thread_rng().gen_range(0..charset.len());
charset[idx] as char
}).collect()
})
.collect()
}
pub fn get_token_type(token: &str) -> Option<&str> {
token.split('-').collect::<Vec<&str>>().get(0).copied()
token.split('-').collect::<Vec<&str>>().first().copied()
}

View File

@ -13,12 +13,15 @@ impl MigrationTrait for Migration {
.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
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(User::Table).to_owned()).await
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
@ -27,5 +30,5 @@ impl MigrationTrait for Migration {
pub enum User {
Table,
Id,
Email
Email,
}

View File

@ -1,6 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -8,26 +7,40 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
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::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(MagicLink::User).string().not_null())
.col(ColumnDef::new(MagicLink::ExpiresOn).big_integer().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
.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
manager
.drop_table(Table::drop().table(MagicLink::Table).to_owned())
.await
}
}
@ -37,5 +50,5 @@ pub enum MagicLink {
Table,
Id,
User,
ExpiresOn
ExpiresOn,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,26 +7,39 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
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::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(SessionToken::User).string().not_null())
.col(ColumnDef::new(SessionToken::ExpiresOn).big_integer().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)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned()
).await
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(SessionToken::Table).to_owned()).await
manager
.drop_table(Table::drop().table(SessionToken::Table).to_owned())
.await
}
}
@ -36,5 +49,5 @@ pub enum SessionToken {
Table,
Id,
User,
ExpiresOn
ExpiresOn,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,23 +7,39 @@ 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())
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())
.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
.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
manager
.drop_table(Table::drop().table(Organization::Table).to_owned())
.await
}
}
@ -33,5 +49,5 @@ pub enum Organization {
Table,
Id,
Name,
Owner
Owner,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_232316_create_table_organizations::Organization;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,7 +7,8 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(ApiKey::Table)
.col(ColumnDef::new(ApiKey::Id).string().not_null().primary_key())
@ -18,14 +19,17 @@ impl MigrationTrait for Migration {
.from(ApiKey::Table, ApiKey::Organization)
.to(Organization::Table, Organization::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned()
).await
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(ApiKey::Table).to_owned()).await
manager
.drop_table(Table::drop().table(ApiKey::Table).to_owned())
.await
}
}
@ -35,5 +39,5 @@ pub enum ApiKey {
Table,
Id,
Key,
Organization
Organization,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_233043_create_table_api_keys::ApiKey;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,10 +7,16 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(ApiKeyScope::Table)
.col(ColumnDef::new(ApiKeyScope::Id).string().not_null().primary_key())
.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(
@ -18,13 +24,17 @@ impl MigrationTrait for Migration {
.from(ApiKeyScope::Table, ApiKeyScope::ApiKey)
.to(ApiKey::Table, ApiKey::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade)
).to_owned()
).await
.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
manager
.drop_table(Table::drop().table(ApiKeyScope::Table).to_owned())
.await
}
}
@ -34,5 +44,5 @@ pub enum ApiKeyScope {
Table,
Id,
Scope,
ApiKey
ApiKey,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,27 +7,60 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
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())
.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
.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
manager
.drop_table(Table::drop().table(TotpAuthenticator::Table).to_owned())
.await
}
}
@ -40,5 +73,5 @@ pub enum TotpAuthenticator {
Url,
Verified,
ExpiresOn,
User
User,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_162601_create_table_users::User;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,26 +7,39 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
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::Id)
.string()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(AuthToken::User).string().not_null())
.col(ColumnDef::new(AuthToken::ExpiresOn).big_integer().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)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned()
).await
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(AuthToken::Table).to_owned()).await
manager
.drop_table(Table::drop().table(AuthToken::Table).to_owned())
.await
}
}
@ -36,5 +49,5 @@ pub enum AuthToken {
Table,
Id,
User,
ExpiresOn
ExpiresOn,
}

View File

@ -6,21 +6,40 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(SigningCA::Table)
.col(ColumnDef::new(SigningCA::Id).string().not_null().primary_key())
.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::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
.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
manager
.drop_table(Table::drop().table(SigningCA::Table).to_owned())
.await
}
}
@ -33,5 +52,5 @@ pub enum SigningCA {
Cert,
Key,
Expires,
Nonce
Nonce,
}

View File

@ -1,6 +1,6 @@
use sea_orm_migration::prelude::*;
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;
@ -8,36 +8,59 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(Network::Table)
.col(ColumnDef::new(Network::Id).string().not_null().primary_key())
.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::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())
.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)
.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)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned()
).await
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(Network::Table).to_owned()).await
manager
.drop_table(Table::drop().table(Network::Table).to_owned())
.await
}
}
@ -51,5 +74,5 @@ pub enum Network {
SigningCA,
CreatedAt,
Name,
LighthousesAsRelays
LighthousesAsRelays,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230402_232316_create_table_organizations::Organization;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,7 +7,8 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(Role::Table)
.col(ColumnDef::new(Role::Id).string().not_null().primary_key())
@ -21,13 +22,17 @@ impl MigrationTrait for Migration {
.from(Role::Table, Role::Organization)
.to(Organization::Table, Organization::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(ForeignKeyAction::Cascade)
).to_owned()
).await
.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
manager
.drop_table(Table::drop().table(Role::Table).to_owned())
.await
}
}
@ -40,5 +45,5 @@ pub enum Role {
Description,
Organization,
CreatedAt,
ModifiedAt
ModifiedAt,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230404_133809_create_table_roles::Role;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,35 +7,57 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(FirewallRule::Table)
.col(ColumnDef::new(FirewallRule::Id).string().not_null().primary_key())
.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::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())
.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)
.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
.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
manager
.drop_table(Table::drop().table(FirewallRule::Table).to_owned())
.await
}
}
@ -49,5 +71,5 @@ pub enum FirewallRule {
Description,
AllowedRoleID,
PortRangeFrom,
PortRangeTo
PortRangeTo,
}

View File

@ -1,6 +1,6 @@
use sea_orm_migration::prelude::*;
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;
@ -8,7 +8,8 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(Host::Table)
.col(ColumnDef::new(Host::Id).string().not_null().primary_key())
@ -31,14 +32,14 @@ impl MigrationTrait for Migration {
.from(Host::Table, Host::Network)
.to(Network::Table, Network::Id)
.on_update(ForeignKeyAction::Cascade)
.on_delete(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)
.on_delete(ForeignKeyAction::Cascade),
)
.index(
Index::create()
@ -46,7 +47,7 @@ impl MigrationTrait for Migration {
.table(Host::Table)
.col(Host::Network)
.col(Host::Name)
.unique()
.unique(),
)
.index(
Index::create()
@ -54,10 +55,11 @@ impl MigrationTrait for Migration {
.table(Host::Table)
.col(Host::Network)
.col(Host::IP)
.unique()
.unique(),
)
.to_owned()
).await
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
@ -85,5 +87,5 @@ pub enum Host {
LastSeenAt,
LastVersion,
LastPlatform,
LastOutOfDate
LastOutOfDate,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230427_170037_create_table_hosts::Host;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -11,15 +11,24 @@ impl MigrationTrait for Migration {
.create_table(
Table::create()
.table(HostStaticAddress::Table)
.col(ColumnDef::new(HostStaticAddress::Id).string().not_null().primary_key())
.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())
.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)
.on_delete(ForeignKeyAction::Cascade),
)
.to_owned(),
)
@ -39,5 +48,5 @@ pub enum HostStaticAddress {
Table,
Id,
Host,
Address
Address,
}

View File

@ -1,5 +1,5 @@
use sea_orm_migration::prelude::*;
use crate::m20230427_170037_create_table_hosts::Host;
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
@ -7,19 +7,29 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(
manager
.create_table(
Table::create()
.table(HostConfigOverride::Table)
.col(ColumnDef::new(HostConfigOverride::Id).string().not_null().primary_key())
.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::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)
.on_update(ForeignKeyAction::Cascade),
)
.index(
Index::create()
@ -27,10 +37,11 @@ impl MigrationTrait for Migration {
.table(HostConfigOverride::Table)
.col(HostConfigOverride::Key)
.col(HostConfigOverride::Id)
.unique()
.unique(),
)
.to_owned()
).await
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {

View File

@ -1,15 +1,52 @@
//! `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(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 (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 , }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[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 :: organization :: Entity > for Entity { fn to () -> RelationDef { Relation :: Organization . 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 Related<super::signing_ca::Entity> for Entity {
fn to() -> RelationDef {
Relation::SigningCa.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,19 +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(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 , }
#[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::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::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::role::Entity> for Entity {
fn to() -> RelationDef {
Relation::Role.def()
}
}
impl Related < super :: user :: Entity > for Entity { fn to () -> RelationDef { Relation :: User . def () } }
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,13 +1,36 @@
//! `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(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 (belongs_to = "super::organization::Entity" , from = "Column::Organization" , to = "super::organization::Column::Id" , on_update = "Cascade" , on_delete = "Cascade" ,)] Organization , }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[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 :: organization :: Entity > for Entity { fn to () -> RelationDef { Relation :: Organization . def () } }
impl Related<super::organization::Entity> for Entity {
fn to() -> RelationDef {
Relation::Organization.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,13 +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(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 , }
#[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 Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,13 +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(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 , }
#[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 Related<super::network::Entity> for Entity {
fn to() -> RelationDef {
Relation::Network.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,13 +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(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 , }
#[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 Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,21 +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(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 , }
#[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::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::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::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::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 Related<super::totp_authenticator::Entity> for Entity {
fn to() -> RelationDef {
Relation::TotpAuthenticator.def()
}
}
impl ActiveModelBehavior for ActiveModel {}