diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index ffaea11..9e588f6 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -102,6 +102,7 @@ async fn main() -> Result<(), Box> { .service(routes::v1::hosts::edit_host) .service(routes::v1::hosts::block_host) .service(routes::v1::hosts::enroll_host) + .service(routes::v1::hosts::create_host_and_enrollment_code) }) .bind(CONFIG.server.bind)? .run() diff --git a/trifid-api/src/routes/v1/hosts.rs b/trifid-api/src/routes/v1/hosts.rs index 0da947f..a0978e2 100644 --- a/trifid-api/src/routes/v1/hosts.rs +++ b/trifid-api/src/routes/v1/hosts.rs @@ -49,6 +49,11 @@ // 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::cursor::Cursor; @@ -2004,4 +2009,306 @@ pub async fn enroll_host(id: Path, req_info: HttpRequest, db: Data, + req_info: HttpRequest, + db: Data, +) -> 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 = 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 {}, + }) } \ No newline at end of file