forgot about config overrides for a second there

This commit is contained in:
core 2023-06-21 21:03:11 -04:00
parent 0ffb04c1e7
commit 11320ba04a
Signed by: core
GPG Key ID: FDBF740DADDCEECF
4 changed files with 756 additions and 3 deletions

View File

@ -4,7 +4,7 @@ use std::net::{Ipv4Addr, SocketAddrV4};
use serde::{Deserialize, Serialize};
use url::{Url};
use crate::api::APIErrorResponse;
use crate::{HostCommands};
use crate::{HostCommands, HostOverrideCommands};
pub async fn host_main(command: HostCommands, server: Url) -> Result<(), Box<dyn Error>> {
match command {
@ -14,7 +14,12 @@ pub async fn host_main(command: HostCommands, server: Url) -> Result<(), Box<dyn
HostCommands::Delete { id } => delete_host(id, server).await,
HostCommands::Update { id, listen_port, static_address, name, ip, role } => update_host(id, listen_port, static_address, name, ip, role, server).await,
HostCommands::Block { id } => block_host(id, server).await,
HostCommands::Enroll { id } => enroll_host(id, server).await
HostCommands::Enroll { id } => enroll_host(id, server).await,
HostCommands::Overrides { command } => match command {
HostOverrideCommands::List { id } => list_overrides(id, server).await,
HostOverrideCommands::Set { id, key, boolean, string, numeric } => set_override(id, key, boolean, numeric, string, server).await,
HostOverrideCommands::Unset { id, key } => unset_override(id, key, server).await
}
}
}
@ -412,5 +417,224 @@ pub async fn block_host(id: String, server: Url) -> Result<(), Box<dyn Error>> {
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideResponse {
pub data: HostConfigOverrideData
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideData {
pub overrides: Vec<HostConfigOverrideDataOverride>
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideDataOverride {
pub key: String,
pub value: HostConfigOverrideDataOverrideValue
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum HostConfigOverrideDataOverrideValue {
Boolean(bool),
Numeric(i64),
Other(String)
}
pub async fn list_overrides(id: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!("Value: {}", match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v)
});
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error looking up config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
#[derive(Serialize, Deserialize)]
pub struct SetOverrideRequest {
pub overrides: Vec<HostConfigOverrideDataOverride>
}
pub async fn set_override(id: String, key: String, boolean: Option<bool>, numeric: Option<i64>, other: Option<String>, server: Url) -> Result<(), Box<dyn Error>> {
if boolean.is_none() && numeric.is_none() && other.is_none() {
eprintln!("[error] no value provided: you must provide at least --boolean, --numeric, or --string");
std::process::exit(1);
} else if boolean.is_some() && numeric.is_some() || boolean.is_some() && other.is_some() || numeric.is_some() && other.is_some() {
eprintln!("[error] multiple values provided: you must provide only one of --boolean, --numeric, or --string");
std::process::exit(1);
}
let val;
if let Some(v) = boolean {
val = HostConfigOverrideDataOverrideValue::Boolean(v);
} else if let Some(v) = numeric {
val = HostConfigOverrideDataOverrideValue::Numeric(v);
} else if let Some(v) = other {
val = HostConfigOverrideDataOverrideValue::Other(v);
} else {
unreachable!();
}
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
let mut others: Vec<HostConfigOverrideDataOverride> = vec![];
for c_override in resp.data.overrides {
if c_override.key != key {
others.push(c_override);
}
}
others.push(HostConfigOverrideDataOverride {
key,
value: val,
});
let res = client.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).json(&SetOverrideRequest {
overrides: others,
}).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!("Value: {}", match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v)
});
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
println!("Override set successfully");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error setting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error setting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}
pub async fn unset_override(id: String, key: String, server: Url) -> Result<(), Box<dyn Error>> {
let client = reqwest::Client::new();
// load session token
let sess_token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
let session_token = fs::read_to_string(&sess_token_store)?;
let auth_token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
let auth_token = fs::read_to_string(&auth_token_store)?;
let token = format!("{} {}", session_token, auth_token);
let res = client.get(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
let mut others: Vec<HostConfigOverrideDataOverride> = vec![];
for c_override in resp.data.overrides {
if c_override.key != key {
others.push(c_override);
}
}
let res = client.put(server.join(&format!("/v1/hosts/{}/config-overrides", id))?).bearer_auth(token.clone()).json(&SetOverrideRequest {
overrides: others,
}).send().await?;
if res.status().is_success() {
let resp: HostConfigOverrideResponse = res.json().await?;
for c_override in &resp.data.overrides {
println!(" Key: {}", c_override.key);
println!("Value: {}", match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => format!("bool:{}", v),
HostConfigOverrideDataOverrideValue::Numeric(v) => format!("numeric:{}", v),
HostConfigOverrideDataOverrideValue::Other(v) => format!("string:{}", v)
});
}
if resp.data.overrides.is_empty() {
println!("No overrides found");
}
println!("Override unset successfully");
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error unsetting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
} else {
let resp: APIErrorResponse = res.json().await?;
eprintln!("[error] Error unsetting config overrides: {} {}", resp.errors[0].code, resp.errors[0].message);
std::process::exit(1);
}
Ok(())
}

View File

@ -203,6 +203,40 @@ pub enum HostCommands {
Enroll {
#[clap(short, long)]
id: String
},
/// Manage config overrides set on the host
Overrides {
#[command(subcommand)]
command: HostOverrideCommands
}
}
#[derive(Subcommand, Debug)]
pub enum HostOverrideCommands {
/// List the config overrides set on the host
List {
#[clap(short, long)]
id: String
},
/// Set a config override on the host
Set {
#[clap(short, long)]
id: String,
#[clap(short, long)]
key: String,
#[clap(short, long)]
boolean: Option<bool>,
#[clap(short, long)]
numeric: Option<i64>,
#[clap(short, long)]
string: Option<String>
},
/// Unset a config override on the host
Unset {
#[clap(short, long)]
id: String,
#[clap(short, long)]
key: String
}
}

View File

@ -119,6 +119,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
.service(routes::v2::enroll::enroll)
.service(routes::v1::dnclient::dnclient)
.service(routes::v2::whoami::whoami)
.service(routes::v1::hosts::get_host_overrides)
.service(routes::v1::hosts::update_host_overrides)
})
.bind(CONFIG.server.bind)?
.run()

View File

@ -76,7 +76,8 @@ use serde::{Deserialize, Serialize};
use std::net::{Ipv4Addr, SocketAddrV4};
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use trifid_api_entities::entity::{host, host_static_address, network, organization};
use trifid_api_entities::entity::{host, host_config_override, host_static_address, network, organization};
use trifid_api_entities::entity::prelude::HostConfigOverride;
#[derive(Serialize, Deserialize)]
pub struct ListHostsRequestOpts {
@ -2328,3 +2329,495 @@ pub async fn create_host_and_enrollment_code(
metadata: CreateHostAndCodeResponseMetadata {},
})
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideResponse {
pub data: HostConfigOverrideData
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideData {
pub overrides: Vec<HostConfigOverrideDataOverride>
}
#[derive(Serialize, Deserialize)]
pub struct HostConfigOverrideDataOverride {
pub key: String,
pub value: HostConfigOverrideDataOverrideValue
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub enum HostConfigOverrideDataOverrideValue {
Boolean(bool),
Numeric(i64),
Other(String)
}
#[get("/v1/hosts/{host_id}/config-overrides")]
pub async fn get_host_overrides(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 config_overrides = match trifid_api_entities::entity::host_config_override::Entity::find().filter(host_config_override::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,
}],
});
}
};
let overrides: Vec<HostConfigOverrideDataOverride> = config_overrides.iter().map(|u| {
let val;
if u.value == "true" || u.value == "false" {
val = HostConfigOverrideDataOverrideValue::Boolean(u.value == "true");
} else if u.value.chars().all(|c| c.is_numeric()) {
val = HostConfigOverrideDataOverrideValue::Numeric(u.value.parse().unwrap());
} else {
val = HostConfigOverrideDataOverrideValue::Other(u.value.clone());
}
HostConfigOverrideDataOverride {
key: u.key.clone(),
value: val,
}
}).collect();
HttpResponse::Ok().json(HostConfigOverrideResponse {
data: HostConfigOverrideData {
overrides,
},
})
}
#[derive(Serialize, Deserialize)]
pub struct UpdateOverridesRequest {
pub overrides: Vec<HostConfigOverrideDataOverride>
}
#[put("/v1/hosts/{host_id}/config-overrides")]
pub async fn update_host_overrides(id: Path<String>, req: Json<UpdateOverridesRequest>, 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 config_overrides = match trifid_api_entities::entity::host_config_override::Entity::find().filter(host_config_override::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,
}],
});
}
};
for c_override in config_overrides {
match c_override.delete(&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 with the database query. Please try again later."
.to_string(),
path: None,
}],
});
}
}
}
for c_override in &req.overrides {
let db_override = host_config_override::Model {
id: random_id("override"),
key: c_override.key.clone(),
value: match &c_override.value {
HostConfigOverrideDataOverrideValue::Boolean(v) => v.to_string(),
HostConfigOverrideDataOverrideValue::Numeric(v) => v.to_string(),
HostConfigOverrideDataOverrideValue::Other(v) => v.clone(),
},
host: host.id.clone(),
};
match db_override.into_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 with the database query. Please try again later."
.to_string(),
path: None,
}],
});
}
}
}
let config_overrides = match trifid_api_entities::entity::host_config_override::Entity::find().filter(host_config_override::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,
}],
});
}
};
let overrides: Vec<HostConfigOverrideDataOverride> = config_overrides.iter().map(|u| {
let val;
if u.value == "true" || u.value == "false" {
val = HostConfigOverrideDataOverrideValue::Boolean(u.value == "true");
} else if u.value.chars().all(|c| c.is_numeric()) || u.value.starts_with('-') && u.value.chars().collect::<Vec<_>>()[1..].iter().all(|c| c.is_numeric()) {
val = HostConfigOverrideDataOverrideValue::Numeric(u.value.parse().unwrap());
} else {
val = HostConfigOverrideDataOverrideValue::Other(u.value.clone());
}
HostConfigOverrideDataOverride {
key: u.key.clone(),
value: val,
}
}).collect();
HttpResponse::Ok().json(HostConfigOverrideResponse {
data: HostConfigOverrideData {
overrides,
},
})
}