diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index e3f7c7f..a3b39f9 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -97,6 +97,7 @@ async fn main() -> Result<(), Box> { .service(routes::v1::trifid::trifid_extensions) .service(routes::v1::hosts::get_hosts) .service(routes::v1::hosts::create_hosts_request) + .service(routes::v1::hosts::get_host) }).bind(CONFIG.server.bind)?.run().await?; Ok(()) diff --git a/trifid-api/src/routes/v1/hosts.rs b/trifid-api/src/routes/v1/hosts.rs index c047638..698a40b 100644 --- a/trifid-api/src/routes/v1/hosts.rs +++ b/trifid-api/src/routes/v1/hosts.rs @@ -23,12 +23,17 @@ // 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. use std::net::{Ipv4Addr, SocketAddrV4}; use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; use actix_web::{HttpRequest, HttpResponse, get, post}; -use actix_web::web::{Data, Json, Query}; +use actix_web::web::{Data, Json, Path, Query}; use chrono::{TimeZone, Utc}; use log::error; use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait, IntoActiveModel, ActiveModelTrait}; @@ -38,6 +43,7 @@ use crate::AppState; use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo}; use crate::cursor::Cursor; use crate::error::{APIError, APIErrorsResponse}; +use crate::timers::TIME_FORMAT; use crate::tokens::random_id; #[derive(Serialize, Deserialize)] @@ -319,10 +325,10 @@ pub async fn get_hosts(opts: Query, req_info: HttpRequest, 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("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + 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("%Y-%m-%dT%H-%M-%S%.3fZ").to_string()), + 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, @@ -610,10 +616,10 @@ pub async fn create_hosts_request(req: Json, req_info: HttpRe listen_port: req.listen_port, is_lighthouse: req.is_lighthouse, is_relay: req.is_relay, - created_at: "".to_string(), + 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("%Y-%m-%dT%H-%M-%S%.3fZ").to_string()), + 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, @@ -621,4 +627,201 @@ pub async fn create_hosts_request(req: Json, req_info: HttpRe }, metadata: CreateHostResponseMetadata {}, }) +} + +#[derive(Serialize, Deserialize)] +pub struct GetHostResponse { + pub data: HostResponse, + pub metadata: GetHostResponseMetadata +} +#[derive(Serialize, Deserialize)] +pub struct GetHostResponseMetadata {} + +#[get("/v1/hosts/{host_id}")] +pub async fn get_host(id: Path, req_info: HttpRequest, db: Data) -> HttpResponse { + // For this endpoint, you either need to be a fully authenticated user OR a token with hosts:read + let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent); + let api_token_info = enforce_api_token(&req_info, &["hosts:read"], &db.conn).await.unwrap_or(TokenInfo::NotPresent); + + // If neither are present, throw an error + if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_UNAUTHORIZED".to_string(), + message: "This endpoint requires either a fully authenticated user or a token with the hosts:read scope".to_string(), + path: None, + } + ], + }) + } + + // If both are present, throw an error + if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { + return HttpResponse::BadRequest().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), + message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), + path: None + } + ], + }) + } + + let org_id = match api_token_info { + TokenInfo::ApiToken(tkn) => tkn.organization, + _ => { + // we have a session token, which means we have to do a db request to get the organization that this user owns + let user = match session_info { + TokenInfo::AuthToken(tkn) => tkn.session_info.user, + _ => unreachable!() + }; + + let org = match organization::Entity::find().filter(organization::Column::Owner.eq(user.id)).one(&db.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error performing the database request, please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + + if let Some(org) = org { + org.id + } else { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_NO_ORG".to_string(), + message: "This user does not own any organizations. Try using an API token instead.".to_string(), + path: None + } + ], + }) + } + } + }; + + let net_id; + + let net = match network::Entity::find().filter(network::Column::Organization.eq(&org_id)).one(&db.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error performing the database request, please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + + if let Some(net) = net { + net_id = net.id; + } else { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_NO_NET".to_string(), + message: "This user does not own any networks. Try using an API token instead.".to_string(), + path: None + } + ], + }) + } + + let host = match host::Entity::find().filter(host::Column::Id.eq(id.into_inner())).one(&db.conn).await { + Ok(h) => h, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error with the database query. Please try again later.".to_string(), + path: None + } + ], + }) + } + }; + + let host = match host { + Some(h) => h, + None => { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_UNAUTHORIZED".to_string(), + message: "This resource does not exist or you do not have permission to access it.".to_string(), + path: None + } + ], + }) + } + }; + + if host.network != net_id { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_UNAUTHORIZED".to_string(), + message: "This resource does not exist or you do not have permission to access it.".to_string(), + path: None + } + ], + }) + } + + let static_addresses = match host_static_address::Entity::find().filter(host_static_address::Column::Host.eq(&host.id)).all(&db.conn).await { + Ok(h) => h, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error with the database query. Please try again later.".to_string(), + path: None + } + ], + }) + } + }; + + HttpResponse::Ok().json(GetHostResponse { + data: HostResponse { + id: host.id, + organization_id: org_id, + network_id: net_id, + role_id: host.role, + name: host.name, + ip_address: host.ip.to_string(), + static_addresses: static_addresses.iter().map(|u| SocketAddrV4::from_str(&u.address).unwrap()).collect(), + listen_port: host.listen_port as u16, + is_lighthouse: host.is_lighthouse, + is_relay: host.is_relay, + created_at: Utc.timestamp_opt(host.created_at, 0).unwrap().format(TIME_FORMAT).to_string(), + is_blocked: host.is_blocked, + metadata: HostResponseMetadata { + last_seen_at: Some(Utc.timestamp_opt(host.last_seen_at, 0).unwrap().format(TIME_FORMAT).to_string()), + version: host.last_version.to_string(), + platform: host.last_platform, + update_available: host.last_out_of_date, + }, + }, + metadata: GetHostResponseMetadata {}, + }) } \ No newline at end of file diff --git a/trifid-api/src/routes/v1/networks.rs b/trifid-api/src/routes/v1/networks.rs index 22f6a6e..8a2c010 100644 --- a/trifid-api/src/routes/v1/networks.rs +++ b/trifid-api/src/routes/v1/networks.rs @@ -36,6 +36,7 @@ use crate::error::{APIError, APIErrorsResponse}; use trifid_api_entities::entity::organization; use trifid_api_entities::entity::network; use crate::cursor::Cursor; +use crate::timers::TIME_FORMAT; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetNetworksResponse { @@ -52,7 +53,7 @@ pub struct GetNetworksResponseData { #[serde(rename = "signingCAID")] pub signing_ca_id: String, #[serde(rename = "createdAt")] - pub created_at: String, // 2023-03-22T18:55:47.009Z, %Y-%m-%dT%H-%M-%S%.3fZ + pub created_at: String, // 2023-03-22T18:55:47.009Z pub name: String, #[serde(rename = "lighthousesAsRelays")] pub lighthouses_as_relays: bool @@ -235,7 +236,7 @@ pub async fn get_networks(opts: Query, req_info: HttpReq cidr: u.cidr.clone(), organization_id: u.organization.clone(), signing_ca_id: u.signing_ca.clone(), - created_at: Utc.timestamp_opt(u.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + created_at: Utc.timestamp_opt(u.created_at, 0).unwrap().format(TIME_FORMAT).to_string(), name: u.name.clone(), lighthouses_as_relays: u.lighthouses_as_relays, } @@ -330,7 +331,7 @@ pub async fn get_network_request(net: Path, req_info: HttpRequest, db: D cidr: network.cidr, organization_id: network.organization, signing_ca_id: network.signing_ca, - created_at: Utc.timestamp_opt(network.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + created_at: Utc.timestamp_opt(network.created_at, 0).unwrap().format(TIME_FORMAT).to_string(), name: network.name, lighthouses_as_relays: network.lighthouses_as_relays, }, diff --git a/trifid-api/src/routes/v1/roles.rs b/trifid-api/src/routes/v1/roles.rs index fd9d500..fcb9fbe 100644 --- a/trifid-api/src/routes/v1/roles.rs +++ b/trifid-api/src/routes/v1/roles.rs @@ -51,6 +51,7 @@ use trifid_api_entities::entity::role; use crate::cursor::Cursor; use crate::tokens::random_id; use actix_web::delete; +use crate::timers::TIME_FORMAT; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CreateRoleRequest { @@ -260,8 +261,8 @@ pub async fn create_role_request(req: Json, req_info: HttpReq name: Some(new_role_model_clone.name.clone()), description: Some(new_role_model_clone.description), firewall_rules: req.firewall_rules.clone(), - created_at: Utc.timestamp_opt(new_role_model_clone.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), - modified_at: Utc.timestamp_opt(new_role_model_clone.modified_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + created_at: Utc.timestamp_opt(new_role_model_clone.created_at, 0).unwrap().format(TIME_FORMAT).to_string(), + modified_at: Utc.timestamp_opt(new_role_model_clone.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(), }, metadata: RoleCreateResponseMetadata {}, }) @@ -509,8 +510,8 @@ pub async fn get_roles(opts: Query, req_info: HttpRequest, name: Some(u.name), description: Some(u.description), firewall_rules: rules, - created_at: Utc.timestamp_opt(u.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), - modified_at: Utc.timestamp_opt(u.modified_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + created_at: Utc.timestamp_opt(u.created_at, 0).unwrap().format(TIME_FORMAT).to_string(), + modified_at: Utc.timestamp_opt(u.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(), }) } @@ -648,8 +649,8 @@ pub async fn get_role(net: Path, req_info: HttpRequest, db: Data, req: Json u64 { (SystemTime::now() + Duration::from_secs(seconds)).duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() }