// trifid-api, an open source reimplementation of the Defined Networking nebula management server. // Copyright (C) 2023 c0repwn3r // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // //#GET /v1/hosts t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#POST /v1/hosts t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#GET /v1/hosts/{host_id} t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#DELETE /v1/hosts/{host_id} t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#PUT /v1/hosts/{host_id} t+parity:full t+type:documented t+status:done t+feature:definednetworking t+ext:t+feature:extended_hosts // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // This endpoint has additional functionality enabled by the extended_hosts feature flag. // //#POST /v1/hosts/{host_id}/block t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#POST /v1/hosts/{host_id}/enrollment-code t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#POST /v1/host-and-enrollment-code t+parity:full t+type:documented t+status:done t+feature:definednetworking // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. // This endpoint requires the `definednetworking` extension to be enabled to be used. use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo}; use crate::config::CONFIG; use crate::cursor::Cursor; use crate::error::{APIError, APIErrorsResponse}; use crate::routes::v1::trifid::SUPPORTED_EXTENSIONS; use crate::timers::{expires_in_seconds, TIME_FORMAT}; use crate::tokens::{random_id, random_token}; use crate::AppState; use actix_web::web::{Data, Json, Path, Query}; use actix_web::{delete, get, post, put, HttpRequest, HttpResponse}; use chrono::{TimeZone, Utc}; use log::{debug, error}; use sea_orm::ActiveValue::Set; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, }; use serde::{Deserialize, Serialize}; use std::net::{Ipv4Addr, SocketAddrV4}; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; use trifid_api_entities::entity::{host, host_static_address, network, organization}; #[derive(Serialize, Deserialize)] pub struct ListHostsRequestOpts { #[serde(default, rename = "includeCounts")] pub include_counts: bool, #[serde(default)] pub cursor: String, #[serde(default = "page_default", rename = "pageSize")] pub page_size: u64, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ListHostsResponse { pub data: Vec, pub metadata: ListHostsResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ListHostsResponseMetadata { #[serde(rename = "totalCount")] pub total_count: u64, #[serde(rename = "hasNextPage")] pub has_next_page: bool, #[serde(rename = "hasPrevPage")] pub has_prev_page: bool, #[serde(default, rename = "prevCursor")] pub prev_cursor: Option, #[serde(default, rename = "nextCursor")] pub next_cursor: Option, #[serde(default)] pub page: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ListHostsResponseMetadataPage { pub count: u64, pub start: u64, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct HostResponse { pub id: String, #[serde(rename = "organizationID")] pub organization_id: String, #[serde(rename = "networkID")] pub network_id: String, #[serde(rename = "roleID")] pub role_id: String, pub name: String, #[serde(rename = "ipAddress")] pub ip_address: String, #[serde(rename = "staticAddresses")] pub static_addresses: Vec, #[serde(rename = "listenPort")] pub listen_port: u16, #[serde(rename = "isLighthouse")] pub is_lighthouse: bool, #[serde(rename = "isRelay")] pub is_relay: bool, #[serde(rename = "createdAt")] pub created_at: String, #[serde(rename = "isBlocked")] pub is_blocked: bool, pub metadata: HostResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct HostResponseMetadata { #[serde(rename = "lastSeenAt")] pub last_seen_at: Option, pub version: String, pub platform: String, #[serde(rename = "updateAvailable")] pub update_available: bool, } fn page_default() -> u64 { 25 } #[get("/v1/hosts")] pub async fn get_hosts( opts: Query, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:list let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts:list"], &db.conn) .await .unwrap_or(TokenInfo::NotPresent); // If neither are present, throw an error if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:list scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } let cursor: Cursor = match opts.cursor.clone().try_into() { Ok(r) => r, Err(e) => { error!("invalid cursor: {}", e); return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_INVALID_CURSOR".to_string(), message: "The provided cursor was invalid, please try again later.".to_string(), path: None, }], }); } }; let host_pages = host::Entity::find() .filter(host::Column::Network.eq(&net_id)) .order_by_asc(host::Column::CreatedAt) .paginate(&db.conn, opts.page_size); let total = match host_pages.num_items().await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; let pages = match host_pages.num_pages().await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; let models = match host_pages.fetch_page(cursor.page).await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; let mut models_mapped: Vec = vec![]; for u in models { // fetch static addresses let ips = match host_static_address::Entity::find() .filter(host_static_address::Column::Host.eq(&u.id)) .all(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; models_mapped.push(HostResponse { id: u.id, organization_id: org_id.clone(), network_id: u.network, role_id: u.role, name: u.name, ip_address: u.ip, static_addresses: ips .iter() .map(|u| SocketAddrV4::from_str(&u.address).unwrap()) .collect(), listen_port: u.listen_port as u16, is_lighthouse: u.is_lighthouse, is_relay: u.is_relay, created_at: Utc .timestamp_opt(u.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), is_blocked: u.is_blocked, metadata: HostResponseMetadata { last_seen_at: Some( Utc.timestamp_opt(u.last_seen_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), ), version: u.last_version.to_string(), platform: u.last_platform, update_available: u.last_out_of_date, }, }) } let count = models_mapped.len() as u64; HttpResponse::Ok().json(ListHostsResponse { data: models_mapped, metadata: ListHostsResponseMetadata { total_count: total, has_next_page: cursor.page + 1 != pages, has_prev_page: cursor.page != 0, prev_cursor: if cursor.page != 0 { match (Cursor { page: cursor.page - 1, }) .try_into() { Ok(r) => Some(r), Err(_) => None, } } else { None }, next_cursor: if cursor.page + 1 != pages { match (Cursor { page: cursor.page + 1, }) .try_into() { Ok(r) => Some(r), Err(_) => None, } } else { None }, page: if opts.include_counts { Some(ListHostsResponseMetadataPage { count, start: opts.page_size * cursor.page, }) } else { None }, }, }) } #[derive(Serialize, Deserialize)] pub struct CreateHostRequest { pub name: String, #[serde(rename = "networkID")] pub network_id: String, #[serde(rename = "roleID", default)] pub role_id: Option, #[serde(rename = "ipAddress")] pub ip_address: Ipv4Addr, #[serde(rename = "staticAddresses", default)] pub static_addresses: Vec, #[serde(rename = "listenPort")] pub listen_port: u16, #[serde(rename = "isLighthouse")] pub is_lighthouse: bool, #[serde(rename = "isRelay")] pub is_relay: bool, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CreateHostResponse { pub data: HostResponse, pub metadata: CreateHostResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CreateHostResponseMetadata {} #[post("/v1/hosts")] pub async fn create_hosts_request( req: Json, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:create let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts: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) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:create scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } if net_id != req.network_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_WRONG_NET".to_string(), message: "The network on the request does not match the network associated with this token or user.".to_string(), path: None } ], }); } if req.is_lighthouse && req.is_relay { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_CANNOT_BE_RELAY_AND_LIGHTHOUSE".to_string(), message: "A host cannot be a relay and a lighthouse at the same time.".to_string(), path: None, }], }); } if req.is_lighthouse || req.is_relay && req.static_addresses.is_empty() { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NEEDS_STATIC_ADDR".to_string(), message: "A relay or lighthouse requires at least one static address.".to_string(), path: None, }], }); } let new_host_model = host::Model { id: random_id("host"), name: req.name.clone(), network: net_id.clone(), role: req.role_id.clone().unwrap_or("".to_string()), ip: req.ip_address.to_string(), listen_port: req.listen_port as i32, is_lighthouse: req.is_lighthouse, is_relay: req.is_relay, counter: 0, created_at: SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time went backwards") .as_secs() as i64, is_blocked: false, last_seen_at: 0, last_version: 0, last_platform: "".to_string(), last_out_of_date: false, }; let static_addresses: Vec = req .static_addresses .iter() .map(|u| host_static_address::Model { id: random_id("hsaddress"), host: new_host_model.id.clone(), address: u.to_string(), }) .collect(); let new_host_model_clone = new_host_model.clone(); let static_addresses_clone = static_addresses.clone(); let new_host_active_model = new_host_model.into_active_model(); match new_host_active_model.insert(&db.conn).await { Ok(_) => (), Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error creating the new host. Please try again later" .to_string(), path: None, }], }); } } for rule in &static_addresses_clone { let active_model = rule.clone().into_active_model(); match active_model.insert(&db.conn).await { Ok(_) => (), Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error creating the new host. Please try again later" .to_string(), path: None, }], }); } } } HttpResponse::Ok().json(CreateHostResponse { data: HostResponse { id: new_host_model_clone.id, organization_id: org_id, network_id: net_id, role_id: new_host_model_clone.role, name: new_host_model_clone.name, ip_address: req.ip_address.to_string(), static_addresses: req.static_addresses.clone(), listen_port: req.listen_port, is_lighthouse: req.is_lighthouse, is_relay: req.is_relay, created_at: Utc .timestamp_opt(new_host_model_clone.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), is_blocked: false, metadata: HostResponseMetadata { last_seen_at: Some( Utc.timestamp_opt(new_host_model_clone.last_seen_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), ), version: new_host_model_clone.last_version.to_string(), platform: new_host_model_clone.last_platform, update_available: new_host_model_clone.last_out_of_date, }, }, metadata: CreateHostResponseMetadata {}, }) } #[derive(Serialize, Deserialize)] pub struct GetHostResponse { pub data: HostResponse, pub metadata: GetHostResponseMetadata, } #[derive(Serialize, Deserialize)] pub struct GetHostResponseMetadata {} #[get("/v1/hosts/{host_id}")] pub async fn get_host(id: Path, req_info: HttpRequest, db: Data) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:read let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts:read"], &db.conn) .await .unwrap_or(TokenInfo::NotPresent); // If neither are present, throw an error if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:read scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } let host = match host::Entity::find() .filter(host::Column::Id.eq(id.into_inner())) .one(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let host = match host { Some(h) => h, None => { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }) } }; if host.network != net_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }); } let static_addresses = match host_static_address::Entity::find() .filter(host_static_address::Column::Host.eq(&host.id)) .all(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; HttpResponse::Ok().json(GetHostResponse { data: HostResponse { id: host.id, organization_id: org_id, network_id: net_id, role_id: host.role, name: host.name, ip_address: host.ip.to_string(), static_addresses: static_addresses .iter() .map(|u| SocketAddrV4::from_str(&u.address).unwrap()) .collect(), listen_port: host.listen_port as u16, is_lighthouse: host.is_lighthouse, is_relay: host.is_relay, created_at: Utc .timestamp_opt(host.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), is_blocked: host.is_blocked, metadata: HostResponseMetadata { last_seen_at: Some( Utc.timestamp_opt(host.last_seen_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), ), version: host.last_version.to_string(), platform: host.last_platform, update_available: host.last_out_of_date, }, }, metadata: GetHostResponseMetadata {}, }) } #[derive(Serialize, Deserialize)] pub struct DeleteHostResponse { pub data: DeleteHostData, pub metadata: DeleteHostMetadata, } #[derive(Serialize, Deserialize)] pub struct DeleteHostData {} #[derive(Serialize, Deserialize)] pub struct DeleteHostMetadata {} #[delete("/v1/hosts/{host_id}")] pub async fn delete_host( id: Path, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:delete let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts: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) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:delete scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } let host = match host::Entity::find() .filter(host::Column::Id.eq(id.into_inner())) .one(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let host = match host { Some(h) => h, None => { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }) } }; if host.network != net_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }); } let static_addresses = match host_static_address::Entity::find() .filter(host_static_address::Column::Host.eq(&host.id)) .all(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; match host.delete(&db.conn).await { Ok(_) => (), Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } } for address in static_addresses { match address.delete(&db.conn).await { Ok(_) => (), Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } } } HttpResponse::Ok().json(DeleteHostResponse { data: DeleteHostData {}, metadata: DeleteHostMetadata {}, }) } #[derive(Serialize, Deserialize)] pub struct EditHostRequest { #[serde(rename = "staticAddresses")] pub static_addresses: Vec, #[serde(rename = "listenPort")] pub listen_port: u16, // t+features:extended_hosts pub name: Option, // t+features:extended_hosts pub ip: Option, } #[derive(Serialize, Deserialize)] pub struct EditHostExtensionQuery { pub extension: Option, } #[derive(Serialize, Deserialize)] pub struct EditHostResponse { pub data: HostResponse, pub metadata: EditHostResponseMetadata, } #[derive(Serialize, Deserialize)] pub struct EditHostResponseMetadata {} #[put("/v1/hosts/{host_id}")] pub async fn edit_host( id: Path, query: Query, req: Json, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:edit let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts:edit"], &db.conn) .await .unwrap_or(TokenInfo::NotPresent); // If neither are present, throw an error if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:edit scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } let host = match host::Entity::find() .filter(host::Column::Id.eq(id.into_inner())) .one(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let host = match host { Some(h) => h, None => { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }) } }; if host.network != net_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }); } let static_addresses = match host_static_address::Entity::find() .filter(host_static_address::Column::Host.eq(&host.id)) .all(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; for address in static_addresses { match address.delete(&db.conn).await { Ok(_) => (), Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } } } let host_clone = host.clone(); let mut host_active_model = host_clone.into_active_model(); host_active_model.listen_port = Set(req.listen_port as i32); debug!( "{:?} {} {:?} {:?} {}", query.extension, SUPPORTED_EXTENSIONS.contains(&"extended_hosts"), req.name, req.ip, query.extension == Some("extended_hosts".to_string()) ); if query.extension == Some("extended_hosts".to_string()) && SUPPORTED_EXTENSIONS.contains(&"extended_hosts") { if let Some(new_host_name) = req.name.clone() { debug!("updated host name"); host_active_model.name = Set(new_host_name); } if let Some(new_host_ip) = req.ip { debug!("updated host ip"); host_active_model.ip = Set(new_host_ip.to_string()); } } let host = match host_active_model.update(&db.conn).await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let static_addresses: Vec = req .static_addresses .iter() .map(|u| host_static_address::Model { id: random_id("hsaddress"), host: host.id.clone(), address: u.to_string(), }) .collect(); for rule in &static_addresses { let active_model = rule.clone().into_active_model(); match active_model.insert(&db.conn).await { Ok(_) => (), Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error creating the new host. Please try again later" .to_string(), path: None, }], }); } } } HttpResponse::Ok().json(EditHostResponse { data: HostResponse { id: host.id, organization_id: org_id, network_id: net_id, role_id: host.role, name: host.name, ip_address: host.ip.to_string(), static_addresses: req.static_addresses.clone(), listen_port: host.listen_port as u16, is_lighthouse: host.is_lighthouse, is_relay: host.is_relay, created_at: Utc .timestamp_opt(host.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), is_blocked: host.is_blocked, metadata: HostResponseMetadata { last_seen_at: Some( Utc.timestamp_opt(host.last_seen_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), ), version: host.last_version.to_string(), platform: host.last_platform, update_available: host.last_out_of_date, }, }, metadata: EditHostResponseMetadata {}, }) } #[derive(Serialize, Deserialize)] pub struct BlockHostResponse { pub data: BlockHostResponseData, pub metadata: BlockHostResponseMetadata, } #[derive(Serialize, Deserialize)] pub struct BlockHostResponseData { pub host: HostResponse, } #[derive(Serialize, Deserialize)] pub struct BlockHostResponseMetadata {} #[post("/v1/hosts/{host_id}/block")] pub async fn block_host( id: Path, req_info: HttpRequest, db: Data, ) -> HttpResponse { let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts:block"], &db.conn) .await .unwrap_or(TokenInfo::NotPresent); // If neither are present, throw an error if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:block scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } let host = match host::Entity::find() .filter(host::Column::Id.eq(id.into_inner())) .one(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let host = match host { Some(h) => h, None => { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }) } }; if host.network != net_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }); } let mut host_active = host.into_active_model(); host_active.is_blocked = Set(true); let host = match host_active.update(&db.conn).await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let static_addresses = match host_static_address::Entity::find() .filter(host_static_address::Column::Host.eq(&host.id)) .all(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; HttpResponse::Ok().json(BlockHostResponse { data: BlockHostResponseData { host: HostResponse { id: host.id, organization_id: org_id, network_id: net_id, role_id: host.role, name: host.name, ip_address: host.ip.to_string(), static_addresses: static_addresses .iter() .map(|u| SocketAddrV4::from_str(&u.address).unwrap()) .collect(), listen_port: host.listen_port as u16, is_lighthouse: host.is_lighthouse, is_relay: host.is_relay, created_at: Utc .timestamp_opt(host.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), is_blocked: host.is_blocked, metadata: HostResponseMetadata { last_seen_at: Some( Utc.timestamp_opt(host.last_seen_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), ), version: host.last_version.to_string(), platform: host.last_platform, update_available: host.last_out_of_date, }, }, }, metadata: BlockHostResponseMetadata {}, }) } #[derive(Serialize, Deserialize)] pub struct CodeResponse { pub code: String, #[serde(rename = "lifetimeSeconds")] pub lifetime_seconds: u64, } #[derive(Serialize, Deserialize)] pub struct EnrollmentCodeResponse { pub data: EnrollmentCodeResponseData, pub metadata: EnrollmentCodeResponseMetadata, } #[derive(Serialize, Deserialize)] pub struct EnrollmentCodeResponseData { #[serde(rename = "enrollmentCode")] pub enrollment_code: CodeResponse, } #[derive(Serialize, Deserialize)] pub struct EnrollmentCodeResponseMetadata {} #[post("/v1/hosts/{host_id}/enrollment-code")] pub async fn enroll_host( id: Path, req_info: HttpRequest, db: Data, ) -> HttpResponse { let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts:enroll"], &db.conn) .await .unwrap_or(TokenInfo::NotPresent); // If neither are present, throw an error if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:enroll scope".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } let host = match host::Entity::find() .filter(host::Column::Id.eq(id.into_inner())) .one(&db.conn) .await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; let host = match host { Some(h) => h, None => { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }) } }; if host.network != net_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { 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, }], }); } let enrollment_code = trifid_api_entities::entity::host_enrollment_code::Model { id: random_token("ec"), host: host.id, expires_on: expires_in_seconds(CONFIG.tokens.enrollment_tokens_expiry_time) as i64, }; let ec_am = enrollment_code.into_active_model(); let code = match ec_am.insert(&db.conn).await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; HttpResponse::Ok().json(EnrollmentCodeResponse { data: EnrollmentCodeResponseData { enrollment_code: CodeResponse { code: code.id, lifetime_seconds: CONFIG.tokens.enrollment_tokens_expiry_time, }, }, metadata: EnrollmentCodeResponseMetadata {}, }) } #[derive(Serialize, Deserialize)] pub struct CreateHostAndCodeResponse { pub data: CreateHostAndCodeResponseData, pub metadata: CreateHostAndCodeResponseMetadata, } #[derive(Serialize, Deserialize)] pub struct CreateHostAndCodeResponseData { pub host: HostResponse, #[serde(rename = "enrollmentCode")] pub enrollment_code: CodeResponse, } #[derive(Serialize, Deserialize)] pub struct CreateHostAndCodeResponseMetadata {} #[post("/v1/host-and-enrollment-code")] pub async fn create_host_and_enrollment_code( req: Json, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:create and hosts:enroll let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["hosts:create", "hosts:enroll"], &db.conn) .await .unwrap_or(TokenInfo::NotPresent); // If neither are present, throw an error if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_UNAUTHORIZED".to_string(), message: "This endpoint requires either a fully authenticated user or a token with the hosts:create and hosts:enroll scopes".to_string(), path: None, } ], }); } // If both are present, throw an error if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), path: None } ], }); } let org_id = match api_token_info { TokenInfo::ApiToken(tkn) => tkn.organization, _ => { // we have a session token, which means we have to do a db request to get the organization that this user owns let user = match session_info { TokenInfo::AuthToken(tkn) => tkn.session_info.user, _ => unreachable!(), }; let org = match organization::Entity::find() .filter(organization::Column::Owner.eq(user.id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(org) = org { org.id } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_NO_ORG".to_string(), message: "This user does not own any organizations. Try using an API token instead.".to_string(), path: None } ], }); } } }; let net_id; let net = match network::Entity::find() .filter(network::Column::Organization.eq(&org_id)) .one(&db.conn) .await { Ok(r) => r, Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error performing the database request, please try again later.".to_string(), path: None, } ], }); } }; if let Some(net) = net { net_id = net.id; } else { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NO_NET".to_string(), message: "This user does not own any networks. Try using an API token instead." .to_string(), path: None, }], }); } if net_id != req.network_id { return HttpResponse::Unauthorized().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_WRONG_NET".to_string(), message: "The network on the request does not match the network associated with this token or user.".to_string(), path: None } ], }); } if req.is_lighthouse && req.is_relay { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_CANNOT_BE_RELAY_AND_LIGHTHOUSE".to_string(), message: "A host cannot be a relay and a lighthouse at the same time.".to_string(), path: None, }], }); } if req.is_lighthouse || req.is_relay && req.static_addresses.is_empty() { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NEEDS_STATIC_ADDR".to_string(), message: "A relay or lighthouse requires at least one static address.".to_string(), path: None, }], }); } let new_host_model = host::Model { id: random_id("host"), name: req.name.clone(), network: net_id.clone(), role: req.role_id.clone().unwrap_or("".to_string()), ip: req.ip_address.to_string(), listen_port: req.listen_port as i32, is_lighthouse: req.is_lighthouse, is_relay: req.is_relay, counter: 0, created_at: SystemTime::now() .duration_since(UNIX_EPOCH) .expect("time went backwards") .as_secs() as i64, is_blocked: false, last_seen_at: 0, last_version: 0, last_platform: "".to_string(), last_out_of_date: false, }; let static_addresses: Vec = req .static_addresses .iter() .map(|u| host_static_address::Model { id: random_id("hsaddress"), host: new_host_model.id.clone(), address: u.to_string(), }) .collect(); let new_host_model_clone = new_host_model.clone(); let static_addresses_clone = static_addresses.clone(); let new_host_active_model = new_host_model.into_active_model(); match new_host_active_model.insert(&db.conn).await { Ok(_) => (), Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error creating the new host. Please try again later" .to_string(), path: None, }], }); } } for rule in &static_addresses_clone { let active_model = rule.clone().into_active_model(); match active_model.insert(&db.conn).await { Ok(_) => (), Err(e) => { error!("database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error creating the new host. Please try again later" .to_string(), path: None, }], }); } } } let enrollment_code = trifid_api_entities::entity::host_enrollment_code::Model { id: random_token("ec"), host: new_host_model_clone.id.clone(), expires_on: expires_in_seconds(CONFIG.tokens.enrollment_tokens_expiry_time) as i64, }; let ec_am = enrollment_code.into_active_model(); let code = match ec_am.insert(&db.conn).await { Ok(h) => h, Err(e) => { error!("Database error: {}", e); return HttpResponse::InternalServerError().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_DB_ERROR".to_string(), message: "There was an error with the database query. Please try again later." .to_string(), path: None, }], }); } }; HttpResponse::Ok().json(CreateHostAndCodeResponse { data: CreateHostAndCodeResponseData { host: HostResponse { id: new_host_model_clone.id, organization_id: org_id, network_id: net_id, role_id: new_host_model_clone.role, name: new_host_model_clone.name, ip_address: req.ip_address.to_string(), static_addresses: req.static_addresses.clone(), listen_port: req.listen_port, is_lighthouse: req.is_lighthouse, is_relay: req.is_relay, created_at: Utc .timestamp_opt(new_host_model_clone.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), is_blocked: false, metadata: HostResponseMetadata { last_seen_at: Some( Utc.timestamp_opt(new_host_model_clone.last_seen_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), ), version: new_host_model_clone.last_version.to_string(), platform: new_host_model_clone.last_platform, update_available: new_host_model_clone.last_out_of_date, }, }, enrollment_code: CodeResponse { code: code.id, lifetime_seconds: CONFIG.tokens.enrollment_tokens_expiry_time, }, }, metadata: CreateHostAndCodeResponseMetadata {}, }) }