From aeb6acf640f50c2676085b15bda9fd4e00298591 Mon Sep 17 00:00:00 2001 From: Akash Shanmugaraj <65720968+akashShanmugraj@users.noreply.github.com> Date: Sat, 27 Jan 2024 18:27:12 +0530 Subject: [PATCH] added BYOS (Bring Your Own SERVICE_ACCOUNT) for GCS authentication (#3262) --- doc/man/task-sync.5.in | 36 +++++++++++++++++++ src/Context.cpp | 1 + src/commands/CmdShow.cpp | 1 + src/commands/CmdSync.cpp | 3 +- src/tc/Server.cpp | 5 +-- src/tc/Server.h | 2 +- taskchampion/lib/src/server.rs | 16 ++++++++- taskchampion/lib/taskchampion.h | 1 + .../taskchampion/src/server/cloud/gcp.rs | 25 ++++++++++--- .../taskchampion/src/server/config.rs | 10 ++++-- 10 files changed, 88 insertions(+), 12 deletions(-) diff --git a/doc/man/task-sync.5.in b/doc/man/task-sync.5.in index 3483455b2..54129343d 100644 --- a/doc/man/task-sync.5.in +++ b/doc/man/task-sync.5.in @@ -88,6 +88,42 @@ Then configure Taskwarrior with: $ task config sync.gcp.bucket +However you can bring your own service account credentials if your +`application-default` is already being used by some other application + +To begin, navigate to the "IAM and Admin" section in the Navigation Menu, then select "Roles." + +On the top menu bar within the "Roles" section, click "CREATE ROLE." +Provide an appropriate name and description for the new role. + +Add permissions to your new role using the filter "Service:storage" (not the "Filter permissions by role" input box). +Select the following permissions: + + - storage.buckets.create + - storage.buckets.get + - storage.buckets.update + - storage.objects.create + - storage.objects.get + - storage.objects.list + - storage.objects.update + + Create your new role. + + On the left sidebar, navigate to "Service accounts." + + On the top menu bar within the "Service accounts" section, click "CREATE SERVICE ACCOUNT." + Provide an appropriate name and description for the new service account. + Select the role you just created and complete the service account creation process. + + Now, in the Service Account dashboard, click into the new service account and select "keys" on the top menu bar. + Click on "ADD KEY" to create and download a new key (a JSON key). + + +Then configure Taskwarrior with: + + $ task config sync.gcp.bucket + $ task config sync.gcp.credential_path + .SS Local Synchronization In order to take advantage of synchronization's side effect of saving disk diff --git a/src/Context.cpp b/src/Context.cpp index 0909ed12f..ddfe9fd19 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -283,6 +283,7 @@ std::string configurationDefaults = "#sync.server.client_id # Client ID for sync to a server\n" "#sync.server.origin # Origin of the sync server\n" "#sync.local.server_dir # Directory for local sync\n" + "#sync.gcp.credential_path # Path to JSON file containing credentials to authenticate GCP Sync\n" "#sync.gcp.bucket # Bucket for sync to GCP\n" "\n" "# Aliases - alternate names for commands\n" diff --git a/src/commands/CmdShow.cpp b/src/commands/CmdShow.cpp index 75d819dd8..27e0af744 100644 --- a/src/commands/CmdShow.cpp +++ b/src/commands/CmdShow.cpp @@ -193,6 +193,7 @@ int CmdShow::execute (std::string& output) " sugar" " summary.all.projects" " sync.local.server_dir" + " sync.gcp.credential_path" " sync.gcp.bucket" " sync.server.client_id" " sync.encryption_secret" diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index 69e853d25..2efa81131 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -64,6 +64,7 @@ int CmdSync::execute (std::string& output) // If no server is set up, quit. std::string origin = Context::getContext ().config.get ("sync.server.origin"); std::string server_dir = Context::getContext ().config.get ("sync.local.server_dir"); + std::string gcp_credential_path = Context::getContext ().config.get ("sync.gcp.credential_path"); std::string gcp_bucket = Context::getContext ().config.get ("sync.gcp.bucket"); std::string encryption_secret = Context::getContext ().config.get ("sync.encryption_secret"); if (server_dir != "") { @@ -73,7 +74,7 @@ int CmdSync::execute (std::string& output) if (encryption_secret == "") { throw std::string ("sync.encryption_secret is required"); } - server = tc::Server::new_gcp (gcp_bucket, encryption_secret); + server = tc::Server::new_gcp (gcp_bucket, gcp_credential_path, encryption_secret); std::ostringstream os; os << "GCP bucket " << gcp_bucket; server_ident = os.str(); diff --git a/src/tc/Server.cpp b/src/tc/Server.cpp index 0e2920cdb..7b35d925e 100644 --- a/src/tc/Server.cpp +++ b/src/tc/Server.cpp @@ -79,13 +79,14 @@ tc::Server::new_sync (const std::string &origin, const std::string &client_id, c //////////////////////////////////////////////////////////////////////////////// tc::Server -tc::Server::new_gcp (const std::string &bucket, const std::string &encryption_secret) +tc::Server::new_gcp (const std::string &bucket, const std::string &credential_path, const std::string &encryption_secret) { TCString tc_bucket = tc_string_borrow (bucket.c_str ()); TCString tc_encryption_secret = tc_string_borrow (encryption_secret.c_str ()); + TCString tc_credential_path = tc_string_borrow (credential_path.c_str ()); TCString error; - auto tcserver = tc_server_new_gcp (tc_bucket, tc_encryption_secret, &error); + auto tcserver = tc_server_new_gcp (tc_bucket, tc_credential_path, tc_encryption_secret, &error); if (!tcserver) { auto errmsg = format ("Could not configure connection to GCP bucket {1}: {2}", bucket, tc_string_content (&error)); diff --git a/src/tc/Server.h b/src/tc/Server.h index 07df206f2..08700c4bc 100644 --- a/src/tc/Server.h +++ b/src/tc/Server.h @@ -57,7 +57,7 @@ namespace tc { static Server new_sync (const std::string &origin, const std::string &client_id, const std::string &encryption_secret); // Construct a GCP server (tc_server_new_gcp). - static Server new_gcp (const std::string &bucket, const std::string &encryption_secret); + static Server new_gcp (const std::string &bucket, const std::string &credential_path, const std::string &encryption_secret); // This object "owns" inner, so copy is not allowed. Server (const Server &) = delete; diff --git a/taskchampion/lib/src/server.rs b/taskchampion/lib/src/server.rs index a043c1fcb..47b79debc 100644 --- a/taskchampion/lib/src/server.rs +++ b/taskchampion/lib/src/server.rs @@ -164,12 +164,14 @@ pub unsafe extern "C" fn tc_server_new_sync( /// /// ```c /// EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket, +/// struct TCString credential_path, /// struct TCString encryption_secret, /// struct TCString *error_out); /// ``` #[no_mangle] pub unsafe extern "C" fn tc_server_new_gcp( bucket: TCString, + credential_path_argument: TCString, encryption_secret: TCString, error_out: *mut TCString, ) -> *mut TCServer { @@ -180,15 +182,27 @@ pub unsafe extern "C" fn tc_server_new_gcp( // - bucket ownership is transferred to this function let bucket = unsafe { TCString::val_from_arg(bucket) }.into_string()?; + // SAFETY: + // - credential_path is valid (promised by caller) + // - credential_path ownership is transferred to this function + + let credential_path = + unsafe { TCString::val_from_arg(credential_path_argument) }.into_string()?; + let credential_path = if credential_path.is_empty() { + None + } else { + Some(credential_path) + }; + // SAFETY: // - encryption_secret is valid (promised by caller) // - encryption_secret ownership is transferred to this function let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) } .as_bytes() .to_vec(); - let server_config = ServerConfig::Gcp { bucket, + credential_path, encryption_secret, }; let server = server_config.into_server()?; diff --git a/taskchampion/lib/taskchampion.h b/taskchampion/lib/taskchampion.h index fd8dd657b..8e6de7969 100644 --- a/taskchampion/lib/taskchampion.h +++ b/taskchampion/lib/taskchampion.h @@ -446,6 +446,7 @@ EXTERN_C struct TCServer *tc_server_new_sync(struct TCString origin, // // The server must be freed after it is used - tc_replica_sync does not automatically free it. EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket, + struct TCString credential_path, struct TCString encryption_secret, struct TCString *error_out); diff --git a/taskchampion/taskchampion/src/server/cloud/gcp.rs b/taskchampion/taskchampion/src/server/cloud/gcp.rs index ccf3478a0..a53ad6e48 100644 --- a/taskchampion/taskchampion/src/server/cloud/gcp.rs +++ b/taskchampion/taskchampion/src/server/cloud/gcp.rs @@ -1,5 +1,6 @@ use super::service::{ObjectInfo, Service}; use crate::errors::Result; +use google_cloud_storage::client::google_cloud_auth::credentials::CredentialsFile; use google_cloud_storage::client::{Client, ClientConfig}; use google_cloud_storage::http::error::ErrorResponse; use google_cloud_storage::http::Error as GcsError; @@ -25,9 +26,17 @@ fn is_http_error(query: u16, res: &std::result::Result) -> bo } impl GcpService { - pub(in crate::server) fn new(bucket: String) -> Result { + pub(in crate::server) fn new(bucket: String, credential_path: Option) -> Result { let rt = Runtime::new()?; - let config = rt.block_on(ClientConfig::default().with_auth())?; + + let credentialpathstring = credential_path.clone().unwrap(); + let config: ClientConfig = if credential_path.unwrap() == "" { + rt.block_on(ClientConfig::default().with_auth())? + } else { + let credentials = rt.block_on(CredentialsFile::new_from_file(credentialpathstring))?; + rt.block_on(ClientConfig::default().with_credentials(credentials))? + }; + Ok(Self { client: Client::new(config), rt, @@ -244,10 +253,16 @@ mod tests { let Ok(bucket) = std::env::var("GCP_TEST_BUCKET") else { return None; }; + + let Ok(credential_path) = std::env::var("GCP_TEST_CREDENTIAL_PATH") else { + return None; + }; + let prefix = Uuid::new_v4(); - Some((GcpService::new(bucket).unwrap(), move |n: &_| { - format!("{}-{}", prefix.as_simple(), n).into_bytes() - })) + Some(( + GcpService::new(bucket, Some(credential_path)).unwrap(), + move |n: &_| format!("{}-{}", prefix.as_simple(), n).into_bytes(), + )) } #[test] diff --git a/taskchampion/taskchampion/src/server/config.rs b/taskchampion/taskchampion/src/server/config.rs index 9b9f5d698..20e0e6598 100644 --- a/taskchampion/taskchampion/src/server/config.rs +++ b/taskchampion/taskchampion/src/server/config.rs @@ -37,7 +37,12 @@ pub enum ServerConfig { /// Bucket in which to store the task data. This bucket must not be used for any other /// purpose. bucket: String, - + /// Path to a GCP credential file, in JSON format. This is required for GCP access incase + /// some other application already makes use of Application Default Credentials. + /// See https://cloud.google.com/docs/authentication#service-accounts for more details. + /// See https://cloud.google.com/iam/docs/keys-create-delete for instructions on how to + /// create a service account key. + credential_path: Option, /// Private encryption secret used to encrypt all data sent to the server. This can /// be any suitably un-guessable string of bytes. encryption_secret: Vec, @@ -58,9 +63,10 @@ impl ServerConfig { #[cfg(feature = "server-gcp")] ServerConfig::Gcp { bucket, + credential_path, encryption_secret, } => Box::new(CloudServer::new( - GcpService::new(bucket)?, + GcpService::new(bucket, credential_path)?, encryption_secret, )?), })