host and enrollment codes
This commit is contained in:
parent
7e1627e165
commit
68c120a5ab
|
@ -102,6 +102,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.service(routes::v1::hosts::edit_host)
|
.service(routes::v1::hosts::edit_host)
|
||||||
.service(routes::v1::hosts::block_host)
|
.service(routes::v1::hosts::block_host)
|
||||||
.service(routes::v1::hosts::enroll_host)
|
.service(routes::v1::hosts::enroll_host)
|
||||||
|
.service(routes::v1::hosts::create_host_and_enrollment_code)
|
||||||
})
|
})
|
||||||
.bind(CONFIG.server.bind)?
|
.bind(CONFIG.server.bind)?
|
||||||
.run()
|
.run()
|
||||||
|
|
|
@ -49,6 +49,11 @@
|
||||||
// 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.
|
||||||
|
//
|
||||||
|
//#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::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo};
|
||||||
use crate::cursor::Cursor;
|
use crate::cursor::Cursor;
|
||||||
|
@ -2004,4 +2009,306 @@ pub async fn enroll_host(id: Path<String>, req_info: HttpRequest, db: Data<AppSt
|
||||||
data: EnrollmentCodeResponseData { enrollment_code: CodeResponse { code: code.id, lifetime_seconds: CONFIG.tokens.enrollment_tokens_expiry_time } },
|
data: EnrollmentCodeResponseData { enrollment_code: CodeResponse { code: code.id, lifetime_seconds: CONFIG.tokens.enrollment_tokens_expiry_time } },
|
||||||
metadata: EnrollmentCodeResponseMetadata {},
|
metadata: EnrollmentCodeResponseMetadata {},
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CreateHostAndCodeResponse {
|
||||||
|
pub data: CreateHostAndCodeResponseData,
|
||||||
|
pub metadata: CreateHostAndCodeResponseMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CreateHostAndCodeResponseData {
|
||||||
|
pub host: HostResponse,
|
||||||
|
#[serde(rename = "enrollmentCode")]
|
||||||
|
pub enrollment_code: CodeResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CreateHostAndCodeResponseMetadata {}
|
||||||
|
|
||||||
|
|
||||||
|
#[post("/v1/host-and-enrollment-code")]
|
||||||
|
pub async fn create_host_and_enrollment_code(
|
||||||
|
req: Json<CreateHostRequest>,
|
||||||
|
req_info: HttpRequest,
|
||||||
|
db: Data<AppState>,
|
||||||
|
) -> 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<host_static_address::Model> = 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 {},
|
||||||
|
})
|
||||||
}
|
}
|
Loading…
Reference in New Issue