/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::update_role_request)
|
||||
.service(routes::v1::trifid::trifid_extensions)
|
||||
.service(routes::v1::hosts::get_hosts)
|
||||
}).bind(CONFIG.server.bind)?.run().await?;
|
||||
|
||||
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 },
|
||||
},
|
||||
})
|
||||
}
|
|
@ -6,3 +6,4 @@ pub mod networks;
|
|||
pub mod organization;
|
||||
pub mod roles;
|
||||
pub mod trifid;
|
||||
pub mod hosts;
|
|
@ -15,6 +15,7 @@ pub struct Model {
|
|||
pub is_lighthouse: bool,
|
||||
pub is_relay: bool,
|
||||
pub counter: i32,
|
||||
pub created_at: i64,
|
||||
}
|
||||
|
||||
#[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::IsRelay).boolean().not_null())
|
||||
.col(ColumnDef::new(Host::Counter).unsigned().not_null())
|
||||
.col(ColumnDef::new(Host::CreatedAt).big_integer().not_null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Host::Table, Host::Network)
|
||||
|
@ -65,5 +66,6 @@ pub enum Host {
|
|||
ListenPort,
|
||||
IsLighthouse,
|
||||
IsRelay,
|
||||
Counter
|
||||
Counter,
|
||||
CreatedAt
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue