add /v1/hosts/host - bugfix! correct time formatting and use constant for it

This commit is contained in:
core 2023-05-08 20:57:08 -04:00
parent d421abdb7a
commit 4ca0b54686
Signed by: core
GPG Key ID: FDBF740DADDCEECF
5 changed files with 224 additions and 16 deletions

View File

@ -97,6 +97,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.service(routes::v1::trifid::trifid_extensions) .service(routes::v1::trifid::trifid_extensions)
.service(routes::v1::hosts::get_hosts) .service(routes::v1::hosts::get_hosts)
.service(routes::v1::hosts::create_hosts_request) .service(routes::v1::hosts::create_hosts_request)
.service(routes::v1::hosts::get_host)
}).bind(CONFIG.server.bind)?.run().await?; }).bind(CONFIG.server.bind)?.run().await?;
Ok(()) Ok(())

View File

@ -23,12 +23,17 @@
// This endpoint has full parity with the original API. It has been recreated from the original API documentation. // 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 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 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::net::{Ipv4Addr, SocketAddrV4};
use std::str::FromStr; use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{HttpRequest, HttpResponse, get, post}; 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 chrono::{TimeZone, Utc};
use log::error; use log::error;
use sea_orm::{EntityTrait, QueryFilter, ColumnTrait, QueryOrder, PaginatorTrait, IntoActiveModel, ActiveModelTrait}; 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::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo};
use crate::cursor::Cursor; use crate::cursor::Cursor;
use crate::error::{APIError, APIErrorsResponse}; use crate::error::{APIError, APIErrorsResponse};
use crate::timers::TIME_FORMAT;
use crate::tokens::random_id; use crate::tokens::random_id;
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -319,10 +325,10 @@ pub async fn get_hosts(opts: Query<ListHostsRequestOpts>, req_info: HttpRequest,
listen_port: u.listen_port as u16, listen_port: u.listen_port as u16,
is_lighthouse: u.is_lighthouse, is_lighthouse: u.is_lighthouse,
is_relay: u.is_relay, 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, is_blocked: u.is_blocked,
metadata: HostResponseMetadata { 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(), version: u.last_version.to_string(),
platform: u.last_platform, platform: u.last_platform,
update_available: u.last_out_of_date, update_available: u.last_out_of_date,
@ -610,10 +616,10 @@ pub async fn create_hosts_request(req: Json<CreateHostRequest>, req_info: HttpRe
listen_port: req.listen_port, listen_port: req.listen_port,
is_lighthouse: req.is_lighthouse, is_lighthouse: req.is_lighthouse,
is_relay: req.is_relay, 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, is_blocked: false,
metadata: HostResponseMetadata { 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(), version: new_host_model_clone.last_version.to_string(),
platform: new_host_model_clone.last_platform, platform: new_host_model_clone.last_platform,
update_available: new_host_model_clone.last_out_of_date, update_available: new_host_model_clone.last_out_of_date,
@ -621,4 +627,201 @@ pub async fn create_hosts_request(req: Json<CreateHostRequest>, req_info: HttpRe
}, },
metadata: CreateHostResponseMetadata {}, 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 {},
})
} }

View File

@ -36,6 +36,7 @@ use crate::error::{APIError, APIErrorsResponse};
use trifid_api_entities::entity::organization; use trifid_api_entities::entity::organization;
use trifid_api_entities::entity::network; use trifid_api_entities::entity::network;
use crate::cursor::Cursor; use crate::cursor::Cursor;
use crate::timers::TIME_FORMAT;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetNetworksResponse { pub struct GetNetworksResponse {
@ -52,7 +53,7 @@ pub struct GetNetworksResponseData {
#[serde(rename = "signingCAID")] #[serde(rename = "signingCAID")]
pub signing_ca_id: String, pub signing_ca_id: String,
#[serde(rename = "createdAt")] #[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, pub name: String,
#[serde(rename = "lighthousesAsRelays")] #[serde(rename = "lighthousesAsRelays")]
pub lighthouses_as_relays: bool pub lighthouses_as_relays: bool
@ -235,7 +236,7 @@ pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpReq
cidr: u.cidr.clone(), cidr: u.cidr.clone(),
organization_id: u.organization.clone(), organization_id: u.organization.clone(),
signing_ca_id: u.signing_ca.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(), name: u.name.clone(),
lighthouses_as_relays: u.lighthouses_as_relays, lighthouses_as_relays: u.lighthouses_as_relays,
} }
@ -330,7 +331,7 @@ pub async fn get_network_request(net: Path<String>, req_info: HttpRequest, db: D
cidr: network.cidr, cidr: network.cidr,
organization_id: network.organization, organization_id: network.organization,
signing_ca_id: network.signing_ca, 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, name: network.name,
lighthouses_as_relays: network.lighthouses_as_relays, lighthouses_as_relays: network.lighthouses_as_relays,
}, },

View File

@ -51,6 +51,7 @@ use trifid_api_entities::entity::role;
use crate::cursor::Cursor; use crate::cursor::Cursor;
use crate::tokens::random_id; use crate::tokens::random_id;
use actix_web::delete; use actix_web::delete;
use crate::timers::TIME_FORMAT;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateRoleRequest { pub struct CreateRoleRequest {
@ -260,8 +261,8 @@ pub async fn create_role_request(req: Json<CreateRoleRequest>, req_info: HttpReq
name: Some(new_role_model_clone.name.clone()), name: Some(new_role_model_clone.name.clone()),
description: Some(new_role_model_clone.description), description: Some(new_role_model_clone.description),
firewall_rules: req.firewall_rules.clone(), 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(), 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("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), modified_at: Utc.timestamp_opt(new_role_model_clone.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
}, },
metadata: RoleCreateResponseMetadata {}, metadata: RoleCreateResponseMetadata {},
}) })
@ -509,8 +510,8 @@ pub async fn get_roles(opts: Query<ListRolesRequestOpts>, req_info: HttpRequest,
name: Some(u.name), name: Some(u.name),
description: Some(u.description), description: Some(u.description),
firewall_rules: rules, firewall_rules: rules,
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(),
modified_at: Utc.timestamp_opt(u.modified_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").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<String>, req_info: HttpRequest, db: Data<AppStat
name: Some(role.name.clone()), name: Some(role.name.clone()),
description: Some(role.description.clone()), description: Some(role.description.clone()),
firewall_rules: rules, firewall_rules: rules,
created_at: Utc.timestamp_opt(role.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), created_at: Utc.timestamp_opt(role.created_at, 0).unwrap().format(TIME_FORMAT).to_string(),
modified_at: Utc.timestamp_opt(role.modified_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), modified_at: Utc.timestamp_opt(role.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
}, },
metadata: GetRoleResponseMetadata {}, metadata: GetRoleResponseMetadata {},
}) })
@ -943,8 +944,8 @@ pub async fn update_role_request(role: Path<String>, req: Json<CreateRoleRequest
name: Some(new_role_model_clone.name.clone()), name: Some(new_role_model_clone.name.clone()),
description: Some(new_role_model_clone.description), description: Some(new_role_model_clone.description),
firewall_rules: req.firewall_rules.clone(), 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(), 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("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), modified_at: Utc.timestamp_opt(new_role_model_clone.modified_at, 0).unwrap().format(TIME_FORMAT).to_string(),
}, },
metadata: RoleCreateResponseMetadata {}, metadata: RoleCreateResponseMetadata {},
}) })

View File

@ -16,6 +16,8 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const TIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z";
pub fn expires_in_seconds(seconds: u64) -> u64 { pub fn expires_in_seconds(seconds: u64) -> u64 {
(SystemTime::now() + Duration::from_secs(seconds)).duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() (SystemTime::now() + Duration::from_secs(seconds)).duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs()
} }