2322 lines
82 KiB
Rust
2322 lines
82 KiB
Rust
// trifid-api, an open source reimplementation of the Defined Networking nebula management server.
|
|
// Copyright (C) 2023 c0repwn3r
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
//
|
|
//#GET /v1/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<HostResponse>,
|
|
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<String>,
|
|
#[serde(default, rename = "nextCursor")]
|
|
pub next_cursor: Option<String>,
|
|
#[serde(default)]
|
|
pub page: Option<ListHostsResponseMetadataPage>,
|
|
}
|
|
|
|
#[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<SocketAddrV4>,
|
|
#[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<String>,
|
|
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<ListHostsRequestOpts>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<HostResponse> = 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<String>,
|
|
#[serde(rename = "ipAddress")]
|
|
pub ip_address: Ipv4Addr,
|
|
#[serde(rename = "staticAddresses", default)]
|
|
pub static_addresses: Vec<SocketAddrV4>,
|
|
#[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<CreateHostRequest>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<host_static_address::Model> = 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<String>, req_info: HttpRequest, db: Data<AppState>) -> 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<String>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<SocketAddrV4>,
|
|
#[serde(rename = "listenPort")]
|
|
pub listen_port: u16,
|
|
// t+features:extended_hosts
|
|
pub name: Option<String>,
|
|
// t+features:extended_hosts
|
|
pub ip: Option<Ipv4Addr>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct EditHostExtensionQuery {
|
|
pub extension: Option<String>,
|
|
}
|
|
|
|
#[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<String>,
|
|
query: Query<EditHostExtensionQuery>,
|
|
req: Json<EditHostRequest>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<host_static_address::Model> = 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<String>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<String>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<CreateHostRequest>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<host_static_address::Model> = 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 {},
|
|
})
|
|
}
|