cli work
This commit is contained in:
parent
f52db518e7
commit
a0db9a7777
|
@ -8,6 +8,7 @@
|
||||||
<sourceFolder url="file://$MODULE_DIR$/dnapi-rs/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/dnapi-rs/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/trifid-api/trifid_api_entities/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/trifid-api/trifid_api_entities/src" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/trifid-api/trifid_api_migration/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/trifid-api/trifid_api_migration/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tfcli/src" isTestSource="false" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|
|
@ -781,6 +781,12 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "checked_int_cast"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.24"
|
version = "0.4.24"
|
||||||
|
@ -993,6 +999,31 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot 0.12.1",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -2347,6 +2378,25 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qr2term"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c2a1e77b5cd714b04247ad912b7c8fe9a1fe1d58425048249def91bcf690e4c"
|
||||||
|
dependencies = [
|
||||||
|
"crossterm",
|
||||||
|
"qrcode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f"
|
||||||
|
dependencies = [
|
||||||
|
"checked_int_cast",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-protobuf"
|
name = "quick-protobuf"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
@ -2972,6 +3022,27 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.1"
|
version = "1.4.1"
|
||||||
|
@ -3242,6 +3313,20 @@ version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tfcli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"clap 4.2.7",
|
||||||
|
"dirs 5.0.1",
|
||||||
|
"ipnet",
|
||||||
|
"qr2term",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tfclient"
|
name = "tfclient"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
@ -3371,9 +3456,21 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-native-tls"
|
name = "tokio-native-tls"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
|
@ -5,5 +5,6 @@ members = [
|
||||||
"trifid-api/trifid_api_entities",
|
"trifid-api/trifid_api_entities",
|
||||||
"tfclient",
|
"tfclient",
|
||||||
"trifid-pki",
|
"trifid-pki",
|
||||||
"dnapi-rs"
|
"dnapi-rs",
|
||||||
|
"tfcli"
|
||||||
]
|
]
|
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "tfcli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive", "env"] }
|
||||||
|
url = "2.3.1"
|
||||||
|
reqwest = { version = "0.11.17", features = ["json"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
dirs = "5.0.1"
|
||||||
|
qr2term = "0.3.1"
|
||||||
|
ipnet = "2.7.2"
|
|
@ -0,0 +1,239 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use crate::AccountCommands;
|
||||||
|
use crate::api::APIErrorResponse;
|
||||||
|
|
||||||
|
pub async fn account_main(command: AccountCommands, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
match command {
|
||||||
|
AccountCommands::Create { email } => create_account(email, server).await,
|
||||||
|
AccountCommands::MagicLink { magic_link_token } => auth_magic_link(magic_link_token, server).await,
|
||||||
|
AccountCommands::MfaSetup {} => create_mfa_authenticator(server).await,
|
||||||
|
AccountCommands::MfaSetupFinish {code, token} => finish_mfa_authenticator(token, code, server).await,
|
||||||
|
AccountCommands::Mfa {code} => mfa_auth(code, server).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct CreateAccountBody {
|
||||||
|
pub email: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_account(email: String, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client.post(server.join("/v1/signup")?).json(&CreateAccountBody { email }).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
println!("Account created successfully, check your email.");
|
||||||
|
println!("Finish creating your account with 'tfcli account magic-link --magic-link-token [magic-link-token]'.");
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error creating account: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MagicLinkBody {
|
||||||
|
#[serde(rename = "magicLinkToken")]
|
||||||
|
pub magic_link_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MagicLinkSuccess {
|
||||||
|
pub data: MagicLinkSuccessBody
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MagicLinkSuccessBody {
|
||||||
|
#[serde(rename = "sessionToken")]
|
||||||
|
pub session_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn auth_magic_link(magic_token: String, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client.post(server.join("/v1/auth/verify-magic-link")?).json(&MagicLinkBody { magic_link_token: magic_token }).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let resp: MagicLinkSuccess = res.json().await?;
|
||||||
|
|
||||||
|
let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
|
||||||
|
|
||||||
|
fs::write(&token_store, resp.data.session_token)?;
|
||||||
|
|
||||||
|
println!("Session token saved to {}", token_store.display());
|
||||||
|
println!("You will likely need to authorize with 2fa, run 'tfcli account mfa --code=[code]' to finish logging in, or 'tfcli account mfa-setup' to setup 2FA.");
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error getting session token: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiResponse {
|
||||||
|
pub data: WhoamiResponseData,
|
||||||
|
pub metadata: WhoamiResponseMetadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiResponseData {
|
||||||
|
#[serde(rename = "actorType")]
|
||||||
|
pub actor_type: String,
|
||||||
|
pub actor: WhoamiResponseDataActor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiResponseDataActor {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "organizationID")]
|
||||||
|
pub organization_id: Option<String>,
|
||||||
|
pub email: String,
|
||||||
|
#[serde(rename = "createdAt")]
|
||||||
|
pub created_at: String,
|
||||||
|
#[serde(rename = "hasTOTPAuthenticator")]
|
||||||
|
pub has_totp_authenticator: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WhoamiResponseMetadata {}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateMfaResponse {
|
||||||
|
pub data: CreateMfaResponseData
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateMfaResponseData {
|
||||||
|
#[serde(rename = "totpToken")]
|
||||||
|
pub totp_token: String,
|
||||||
|
pub secret: String,
|
||||||
|
pub url: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_mfa_authenticator(server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// load session token
|
||||||
|
let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
|
||||||
|
let session_token = fs::read_to_string(&token_store)?;
|
||||||
|
|
||||||
|
// do we have mfa already?
|
||||||
|
let whoami: WhoamiResponse = client.get(server.join("/v2/whoami")?).bearer_auth(&session_token).send().await?.json().await?;
|
||||||
|
|
||||||
|
if whoami.data.actor.has_totp_authenticator {
|
||||||
|
eprintln!("[error] user already has a totp authenticator, cannot add another one");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = client.post(server.join("/v1/totp-authenticators")?).bearer_auth(&session_token).body("{}").send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let resp: CreateMfaResponse = res.json().await?;
|
||||||
|
|
||||||
|
println!("--- TOTP SETUP INFORMATION ---");
|
||||||
|
println!("To complete setup, you'll need a TOTP-compatible app, such as Google Authenticator or Authy.");
|
||||||
|
println!("Scan the following code with your authenticator app:");
|
||||||
|
qr2term::print_qr(resp.data.url)?;
|
||||||
|
println!("Alternatively, enter the following secret into your authenticator app: '{}'", resp.data.secret);
|
||||||
|
println!("Once done, enable TOTP by running the following command with the code shown on your authenticator app:");
|
||||||
|
println!("tfcli account mfa-setup-finish --token {} --code [CODE IN AUTHENTICATOR]", resp.data.totp_token);
|
||||||
|
println!("This code will expire in 10 minutes.");
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error adding MFA to account: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MfaVerifyBody {
|
||||||
|
#[serde(rename = "totpToken")]
|
||||||
|
pub totp_token: String,
|
||||||
|
pub code: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MFASuccess {
|
||||||
|
pub data: MFASuccessBody
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct MFASuccessBody {
|
||||||
|
#[serde(rename = "authToken")]
|
||||||
|
pub auth_token: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn finish_mfa_authenticator(token: String, code: String, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// load session token
|
||||||
|
let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
|
||||||
|
let session_token = fs::read_to_string(&token_store)?;
|
||||||
|
|
||||||
|
let res = client.post(server.join("/v1/verify-totp-authenticators")?).json(&MfaVerifyBody {totp_token: token, code }).bearer_auth(session_token).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let resp: MFASuccess = res.json().await?;
|
||||||
|
|
||||||
|
let token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
|
||||||
|
|
||||||
|
fs::write(&token_store, resp.data.auth_token)?;
|
||||||
|
|
||||||
|
println!("Auth token saved to {}", token_store.display());
|
||||||
|
println!("You are now fully logged-in. This 2fa token will expire in about 10 minutes, at which point you will need to authenticate again with 'tfcli account mfa'");
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error verifying MFA code: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct MfaAuthBody {
|
||||||
|
pub code: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn mfa_auth(code: String, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
// load session token
|
||||||
|
let token_store = dirs::config_dir().unwrap().join("tfcli-session.token");
|
||||||
|
let session_token = fs::read_to_string(&token_store)?;
|
||||||
|
|
||||||
|
let res = client.post(server.join("/v1/auth/totp")?).json(&MfaAuthBody { code }).bearer_auth(session_token).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let resp: MFASuccess = res.json().await?;
|
||||||
|
|
||||||
|
let token_store = dirs::config_dir().unwrap().join("tfcli-auth.token");
|
||||||
|
|
||||||
|
fs::write(&token_store, resp.data.auth_token)?;
|
||||||
|
|
||||||
|
println!("Auth token saved to {}", token_store.display());
|
||||||
|
println!("You are now fully logged-in. This 2fa token will expire in about 10 minutes, at which point you will need to authenticate again with 'tfcli account mfa'");
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error verifying MFA code: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct APIErrorResponse {
|
||||||
|
pub errors: Vec<APIError>
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct APIError {
|
||||||
|
pub code: String,
|
||||||
|
pub message: String,
|
||||||
|
pub path: Option<String>
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use ipnet::Ipv4Net;
|
||||||
|
use url::Url;
|
||||||
|
use crate::account::account_main;
|
||||||
|
use crate::network::network_main;
|
||||||
|
use crate::org::org_main;
|
||||||
|
|
||||||
|
pub mod account;
|
||||||
|
pub mod api;
|
||||||
|
pub mod network;
|
||||||
|
pub mod org;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
#[command(propagate_version = true)]
|
||||||
|
pub struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
#[clap(short, long, env = "TFCLI_SERVER")]
|
||||||
|
/// The base URL of your trifid-api instance. Defaults to the value in $XDG_CONFIG_HOME/tfcli-server-url.conf or the TFCLI_SERVER environment variable.
|
||||||
|
server: Option<Url>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum Commands {
|
||||||
|
/// Manage your trifid account
|
||||||
|
Account {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: AccountCommands
|
||||||
|
},
|
||||||
|
/// Manage the networks associated with your trifid account
|
||||||
|
Network {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: NetworkCommands
|
||||||
|
},
|
||||||
|
/// Manage the organization associated with your trifid account
|
||||||
|
Org {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: OrgCommands
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum AccountCommands {
|
||||||
|
/// Create a new trifid account on the designated server
|
||||||
|
Create {
|
||||||
|
#[clap(short, long)]
|
||||||
|
email: String
|
||||||
|
},
|
||||||
|
/// Log in to your account with a magic-link token acquired via email or the trifid-api logs.
|
||||||
|
MagicLink {
|
||||||
|
#[clap(short, long)]
|
||||||
|
magic_link_token: String
|
||||||
|
},
|
||||||
|
/// Create a new TOTP authenticator on this account to enable authorizing with 2fa and performing all management tasks.
|
||||||
|
MfaSetup {},
|
||||||
|
/// Finish creating a new TOTP authenticator by inputting the code shown on your authenticator app.
|
||||||
|
MfaSetupFinish {
|
||||||
|
#[clap(short, long)]
|
||||||
|
code: String,
|
||||||
|
#[clap(short, long)]
|
||||||
|
token: String
|
||||||
|
},
|
||||||
|
/// Create a new short-lived authentication token by inputting the code shown on your authenticator app.
|
||||||
|
Mfa {
|
||||||
|
#[clap(short, long)]
|
||||||
|
code: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum NetworkCommands {
|
||||||
|
/// List all networks associated with your trifid account.
|
||||||
|
List {},
|
||||||
|
/// Lookup a specific network by ID.
|
||||||
|
Lookup {
|
||||||
|
#[clap(short, long)]
|
||||||
|
id: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum OrgCommands {
|
||||||
|
/// Create an organization on your trifid-api server. NOTE: This command ONLY works on trifid-api servers. It will NOT work on original DN servers.
|
||||||
|
Create {
|
||||||
|
#[clap(short, long)]
|
||||||
|
cidr: Ipv4Net
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
match main2().await {
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("There was an error during execution: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn main2() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
// find server
|
||||||
|
let server;
|
||||||
|
|
||||||
|
if let Some(srv) = args.server {
|
||||||
|
server = srv; // Environment variable or CLI takes precedence over config file
|
||||||
|
} else {
|
||||||
|
let srv_url_file = dirs::config_dir().unwrap().join("tfcli-server-url.conf");
|
||||||
|
if !srv_url_file.exists() {
|
||||||
|
eprintln!("[error] no server URL available: '-s/--server' not provided, TFCLI_SERVER not set, and {} does not exist", srv_url_file.display());
|
||||||
|
eprintln!("[error] please set a server url via any of these mechanisms and try again");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
let url_s = fs::read_to_string(&srv_url_file)?;
|
||||||
|
let url = match Url::parse(&url_s) {
|
||||||
|
Ok(u) => u,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[error] unable to parse the URL in {}", srv_url_file.display());
|
||||||
|
eprintln!("[error] urlparse returned error '{}'", e);
|
||||||
|
eprintln!("[error] please correct the error and try again");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
server = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
match args.command {
|
||||||
|
Commands::Account { command } => account_main(command, server).await,
|
||||||
|
Commands::Network { command } => network_main(command, server).await,
|
||||||
|
Commands::Org { command } => org_main(command, server).await
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
use crate::api::APIErrorResponse;
|
||||||
|
use crate::NetworkCommands;
|
||||||
|
|
||||||
|
pub async fn network_main(command: NetworkCommands, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
match command {
|
||||||
|
NetworkCommands::List {} => list_networks(server).await,
|
||||||
|
NetworkCommands::Lookup {id} => get_network(id, server).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NetworkListResp {
|
||||||
|
pub data: Vec<Network>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Network {
|
||||||
|
pub id: String,
|
||||||
|
pub cidr: String,
|
||||||
|
#[serde(rename = "organizationID")]
|
||||||
|
pub organization_id: String,
|
||||||
|
#[serde(rename = "signingCAID")]
|
||||||
|
pub signing_ca_id: String,
|
||||||
|
#[serde(rename = "createdAt")]
|
||||||
|
pub created_at: String,
|
||||||
|
#[serde(rename = "lighthousesAsRelays")]
|
||||||
|
pub lighthouses_as_relays: bool,
|
||||||
|
pub name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_networks(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("/v1/networks")?).bearer_auth(token).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let resp: NetworkListResp = res.json().await?;
|
||||||
|
|
||||||
|
for network in &resp.data {
|
||||||
|
println!(" Network: {}", network.id);
|
||||||
|
println!(" CIDR: {}", network.cidr);
|
||||||
|
println!(" Organization: {}", network.organization_id);
|
||||||
|
println!(" Signing CA: {}", network.signing_ca_id);
|
||||||
|
println!("Dedicated Relays: {}", !network.lighthouses_as_relays);
|
||||||
|
println!(" Name: {}", network.name);
|
||||||
|
println!(" Created At: {}", network.created_at);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.data.is_empty() {
|
||||||
|
println!("No networks found");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error listing networks: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NetworkGetResponse {
|
||||||
|
pub data: Network
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_network(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/networks/{}", id))?).bearer_auth(token).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let network: Network = res.json::<NetworkGetResponse>().await?.data;
|
||||||
|
|
||||||
|
println!(" Network: {}", network.id);
|
||||||
|
println!(" CIDR: {}", network.cidr);
|
||||||
|
println!(" Organization: {}", network.organization_id);
|
||||||
|
println!(" Signing CA: {}", network.signing_ca_id);
|
||||||
|
println!("Dedicated Relays: {}", !network.lighthouses_as_relays);
|
||||||
|
println!(" Name: {}", network.name);
|
||||||
|
println!(" Created At: {}", network.created_at);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error listing networks: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use ipnet::Ipv4Net;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use url::Url;
|
||||||
|
use crate::OrgCommands;
|
||||||
|
use crate::api::APIErrorResponse;
|
||||||
|
|
||||||
|
pub async fn org_main(command: OrgCommands, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
match command {
|
||||||
|
OrgCommands::Create { cidr } => create_org(cidr, server).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct CreateOrgBody {
|
||||||
|
pub cidr: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct OrgCreateResponse {
|
||||||
|
pub organization: String,
|
||||||
|
pub ca: String,
|
||||||
|
pub network: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_org(cidr: Ipv4Net, server: Url) -> Result<(), Box<dyn Error>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
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.post(server.join("/v1/organization")?).json(&CreateOrgBody { cidr: cidr.to_string() }).bearer_auth(token).send().await?;
|
||||||
|
|
||||||
|
if res.status().is_success() {
|
||||||
|
let resp: OrgCreateResponse = res.json().await?;
|
||||||
|
println!("Created organization successfully.");
|
||||||
|
println!("Organization: {}", resp.organization);
|
||||||
|
println!(" Signing CA: {}", resp.ca);
|
||||||
|
println!(" Network: {}", resp.network);
|
||||||
|
} else {
|
||||||
|
let resp: APIErrorResponse = res.json().await?;
|
||||||
|
|
||||||
|
eprintln!("[error] Error creating org: {} {}", resp.errors[0].code, resp.errors[0].message);
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue