Skip to content

Commit

Permalink
feat: Everything needed to create secrets
Browse files Browse the repository at this point in the history
  • Loading branch information
bradhe committed Nov 8, 2024
1 parent aa4d64a commit 4e3686f
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 15 deletions.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ clap = { version = "4.5", features = ["derive"] }
cli-table = "0.4"
colored = "2"
config = { path = "crates/config" }
crypto = { path = "crates/crpyto" }
crypto = { path = "crates/crypto" }
dirs = "5"
glob = "0.3"
log = "0.4"
pem = "3"
promptly = "0.3"
rand = "0.8"
reqwest = { version = "0.12", features = ["json", "native-tls-vendored"] }
Expand Down
2 changes: 2 additions & 0 deletions crates/tower-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ edition = "2021"
chrono = { workspace = true }
config = { workspace = true }
log = { workspace = true }
pem = { workspace = true }
reqwest = { workspace = true }
rsa = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
Expand Down
33 changes: 30 additions & 3 deletions crates/tower-api/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize};
use reqwest::Error;

#[derive(Serialize, Deserialize)]
pub struct DetailedString {
Expand Down Expand Up @@ -31,8 +30,8 @@ pub struct TowerError {
pub formatted: DetailedString,
}

impl From<Error> for TowerError {
fn from(err: Error) -> Self {
impl From<reqwest::Error> for TowerError {
fn from(err: reqwest::Error) -> Self {
if err.is_redirect() {
Self {
code: "tower_api_client_redirect_error".to_string(),
Expand Down Expand Up @@ -100,3 +99,31 @@ impl From<Error> for TowerError {
}
}
}


impl From<pem::PemError> for TowerError {
fn from(err: pem::PemError) -> Self {
log::debug!("Error decoding PEM: {:?}", err);

Self {
code: "tower_api_client_error".to_string(),
domain: "tower_api_client".to_string(),
description: DetailedString::from_string("An unexpected or unknown error occured!"),
formatted: DetailedString::from_string("An unexpected or unknown error occured!"),
}
}
}


impl From<rsa::pkcs1::Error> for TowerError {
fn from(err: rsa::pkcs1::Error) -> Self {
log::debug!("Error parsing RSA public key: {:?}", err);

Self {
code: "tower_api_client_error".to_string(),
domain: "tower_api_client".to_string(),
description: DetailedString::from_string("An unexpected or unknown error occured!"),
formatted: DetailedString::from_string("An unexpected or unknown error occured!"),
}
}
}
41 changes: 41 additions & 0 deletions crates/tower-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use reqwest::{
use serde_json::Value;
use std::convert::From;
use config::Config;
use rsa::{
RsaPublicKey,
pkcs1::DecodeRsaPublicKey,
};

mod types;
mod error;
Expand Down Expand Up @@ -61,6 +65,24 @@ struct DeleteSecretResponse {
secret: Secret,
}

#[derive(Serialize, Deserialize)]
struct SecretsKeyResponse {
/// public_key is a PEM-encoded RSA public key that can be used to encrypt secrets.
public_key: String,
}

#[derive(Serialize, Deserialize)]
struct CreateSecretRequest {
name: String,
encrypted_value: String,
preview: String,
}

#[derive(Serialize, Deserialize)]
struct CreateSecretResponse {
secret: Secret,
}

pub type Result<T> = std::result::Result<T, TowerError>;

pub struct Client {
Expand Down Expand Up @@ -147,6 +169,25 @@ impl Client {
Ok(res.secret)
}

pub async fn secrets_key(&self) -> Result<RsaPublicKey> {
let res = self.request::<SecretsKeyResponse>(Method::GET, "/api/secrets/key", None).await?;
let decoded = pem::parse(res.public_key)?;
let public_key = RsaPublicKey::from_pkcs1_der(&decoded.contents())?;
Ok(public_key)
}

pub async fn create_secret(&self, name: &str, encrypted_value: &str, preview: &str) -> Result<Secret> {
let data = CreateSecretRequest {
name: String::from(name),
encrypted_value: String::from(encrypted_value),
preview: String::from(preview),
};

let body = serde_json::to_value(data).unwrap();
let res = self.request::<CreateSecretResponse>(Method::POST, "/api/secrets", Some(body)).await?;
Ok(res.secret)
}

async fn request<T>(&self, method: Method, path: &str, body: Option<Value>) -> Result<T>
where
T: for<'de> Deserialize<'de>,
Expand Down
1 change: 1 addition & 0 deletions crates/tower-cmd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ clap = { workspace = true }
cli-table = { workspace = true }
colored = { workspace = true }
config = { workspace = true }
crypto = { workspace = true }
log = { workspace = true }
promptly = { workspace = true }
rpassword = { workspace = true }
Expand Down
2 changes: 0 additions & 2 deletions crates/tower-cmd/src/apps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ pub async fn do_list_apps(_config: Config, client: Client) {
pub async fn do_create_app(_config: Config, client: Client, args: &ArgMatches) {
let name = args.get_one::<String>("name").unwrap_or_else(|| {
output::die("App name (--name) is required");
std::process::exit(1);
});

let description = args.get_one::<String>("description").unwrap();
Expand All @@ -89,7 +88,6 @@ pub async fn do_create_app(_config: Config, client: Client, args: &ArgMatches) {
pub async fn do_delete_app(_config: Config, client: Client, cmd: Option<(&str, &ArgMatches)>) {
let opts = cmd.unwrap_or_else(|| {
output::die("App name (e.g. tower apps delete <name>) is required");
std::process::exit(1);
});

let spinner = output::spinner("Deleting app...");
Expand Down
2 changes: 1 addition & 1 deletion crates/tower-cmd/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub fn newline() {
io::stdout().write_all("\n".as_bytes()).unwrap();
}

pub fn die(msg: &str) {
pub fn die(msg: &str) -> ! {
let line = format!("{} {}\n", "Error:".red(), msg);
io::stdout().write_all(line.as_bytes()).unwrap();
std::process::exit(1);
Expand Down
60 changes: 52 additions & 8 deletions crates/tower-cmd/src/secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use colored::Colorize;
use clap::{value_parser, Arg, ArgMatches, Command};
use config::Config;
use tower_api::Client;
use crypto::encrypt;

use crate::output;

Expand All @@ -11,7 +12,7 @@ pub fn secrets_cmd() -> Command {
.arg_required_else_help(true)
.subcommand(
Command::new("list")
.about("List all of your apps`")
.about("List all of your secrets")
)
.subcommand(
Command::new("create")
Expand All @@ -22,18 +23,17 @@ pub fn secrets_cmd() -> Command {
.action(clap::ArgAction::Set)
)
.arg(
Arg::new("description")
.long("description")
Arg::new("value")
.long("value")
.value_parser(value_parser!(String))
.default_value("")
.action(clap::ArgAction::Set)
)
.about("Create a new app in Tower")
.about("Create a new secret in your Tower account")
)
.subcommand(
Command::new("delete")
.allow_external_subcommands(true)
.about("Delete an app in Tower")
.about("Delete a secret in Tower")
)
}

Expand Down Expand Up @@ -63,13 +63,44 @@ pub async fn do_list_secrets(_config: Config, client: Client) {
}

pub async fn do_create_secret(_config: Config, client: Client, args: &ArgMatches) {
todo!()
let name = args.get_one::<String>("name").unwrap_or_else(|| {
output::die("Secret name (--name) is required");
});

let value = args.get_one::<String>("value").unwrap_or_else(|| {
output::die("Secret value (--value) is required");
});

let spinner = output::spinner("Creating secret...");

match client.secrets_key().await {
Ok(public_key) => {
let encrypted_value = encrypt(public_key, value.to_string());
let preview = create_preview(value);

match client.create_secret(&name, &encrypted_value, &preview).await {
Ok(secret) => {
spinner.success();

let line = format!("Secret \"{}\" was created", secret.name);
output::success(&line);
},
Err(err) => {
spinner.failure();
output::tower_error(err);
}
}
},
Err(err) => {
spinner.failure();
output::tower_error(err);
}
}
}

pub async fn do_delete_secret(_config: Config, client: Client, cmd: Option<(&str, &ArgMatches)>) {
let opts = cmd.unwrap_or_else(|| {
output::die("Secret name (e.g. tower secrets delete <name>) is required");
std::process::exit(1);
});

let spinner = output::spinner("Deleting secret...");
Expand All @@ -88,3 +119,16 @@ pub async fn do_delete_secret(_config: Config, client: Client, cmd: Option<(&str
}
}
}

fn create_preview(value: &str) -> String {
let len = value.len();
let preview_len = 10;
let suffix_length = 4;

if len <= preview_len {
"XXXXXXXXXX".to_string()
} else {
let suffix = &value[value.char_indices().rev().nth(suffix_length - 1).unwrap().0..];
format!("XXXXXX{}", suffix)
}
}

0 comments on commit 4e3686f

Please sign in to comment.