/v1/hosts
This commit is contained in:
parent
26f5db6b61
commit
fe67c34fd5
|
@ -95,6 +95,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.service(routes::v1::roles::delete_role)
|
.service(routes::v1::roles::delete_role)
|
||||||
.service(routes::v1::roles::update_role_request)
|
.service(routes::v1::roles::update_role_request)
|
||||||
.service(routes::v1::trifid::trifid_extensions)
|
.service(routes::v1::trifid::trifid_extensions)
|
||||||
|
.service(routes::v1::hosts::get_hosts)
|
||||||
}).bind(CONFIG.server.bind)?.run().await?;
|
}).bind(CONFIG.server.bind)?.run().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -0,0 +1,358 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
use std::net::SocketAddrV4;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use actix_web::{HttpRequest, HttpResponse, get};
|
||||||
|
use actix_web::web::{Data, Query};
|
||||||
|
use log::error;
|
||||||
|
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use trifid_api_entities::entity::{host, host_static_address, network, organization};
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo};
|
||||||
|
use crate::cursor::Cursor;
|
||||||
|
use crate::error::{APIError, APIErrorsResponse};
|
||||||
|
|
||||||
|
#[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 roles:list
|
||||||
|
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
|
||||||
|
let api_token_info = enforce_api_token(&req_info, &["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: false,
|
||||||
|
is_relay: false,
|
||||||
|
created_at: "".to_string(),
|
||||||
|
is_blocked: false,
|
||||||
|
metadata: HostResponseMetadata {
|
||||||
|
last_seen_at: None,
|
||||||
|
version: "".to_string(),
|
||||||
|
platform: "".to_string(),
|
||||||
|
update_available: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,4 +5,5 @@ pub mod verify_totp_authenticators;
|
||||||
pub mod networks;
|
pub mod networks;
|
||||||
pub mod organization;
|
pub mod organization;
|
||||||
pub mod roles;
|
pub mod roles;
|
||||||
pub mod trifid;
|
pub mod trifid;
|
||||||
|
pub mod hosts;
|
|
@ -15,6 +15,7 @@ pub struct Model {
|
||||||
pub is_lighthouse: bool,
|
pub is_lighthouse: bool,
|
||||||
pub is_relay: bool,
|
pub is_relay: bool,
|
||||||
pub counter: i32,
|
pub counter: i32,
|
||||||
|
pub created_at: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
@ -20,6 +20,7 @@ impl MigrationTrait for Migration {
|
||||||
.col(ColumnDef::new(Host::IsLighthouse).boolean().not_null())
|
.col(ColumnDef::new(Host::IsLighthouse).boolean().not_null())
|
||||||
.col(ColumnDef::new(Host::IsRelay).boolean().not_null())
|
.col(ColumnDef::new(Host::IsRelay).boolean().not_null())
|
||||||
.col(ColumnDef::new(Host::Counter).unsigned().not_null())
|
.col(ColumnDef::new(Host::Counter).unsigned().not_null())
|
||||||
|
.col(ColumnDef::new(Host::CreatedAt).big_integer().not_null())
|
||||||
.foreign_key(
|
.foreign_key(
|
||||||
ForeignKey::create()
|
ForeignKey::create()
|
||||||
.from(Host::Table, Host::Network)
|
.from(Host::Table, Host::Network)
|
||||||
|
@ -65,5 +66,6 @@ pub enum Host {
|
||||||
ListenPort,
|
ListenPort,
|
||||||
IsLighthouse,
|
IsLighthouse,
|
||||||
IsRelay,
|
IsRelay,
|
||||||
Counter
|
Counter,
|
||||||
|
CreatedAt
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue