This commit is contained in:
core 2023-06-15 20:49:06 -04:00
parent f52db518e7
commit a0db9a7777
Signed by: core
GPG Key ID: FDBF740DADDCEECF
9 changed files with 672 additions and 1 deletions

View File

@ -8,6 +8,7 @@
<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_migration/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tfcli/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />

97
Cargo.lock generated
View File

@ -781,6 +781,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "checked_int_cast"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919"
[[package]]
name = "chrono"
version = "0.4.24"
@ -993,6 +999,31 @@ dependencies = [
"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]]
name = "crypto-common"
version = "0.1.6"
@ -2347,6 +2378,25 @@ dependencies = [
"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]]
name = "quick-protobuf"
version = "0.8.1"
@ -2972,6 +3022,27 @@ dependencies = [
"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]]
name = "signal-hook-registry"
version = "1.4.1"
@ -3242,6 +3313,20 @@ version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "tfclient"
version = "0.1.8"
@ -3371,9 +3456,21 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"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]]
name = "tokio-native-tls"
version = "0.3.1"

View File

@ -5,5 +5,6 @@ members = [
"trifid-api/trifid_api_entities",
"tfclient",
"trifid-pki",
"dnapi-rs"
"dnapi-rs",
"tfcli"
]

16
tfcli/Cargo.toml Normal file
View File

@ -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"

239
tfcli/src/account.rs Normal file
View File

@ -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(())
}

12
tfcli/src/api.rs Normal file
View File

@ -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>
}

137
tfcli/src/main.rs Normal file
View File

@ -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
}
}

114
tfcli/src/network.rs Normal file
View File

@ -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(())
}

54
tfcli/src/org.rs Normal file
View File

@ -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(())
}