fix(auth): enable native keyring backends on top of #359 (#373)

* fix(auth): enable native keyring backends

* test(auth): serialize config dir env test

* fix(auth): scope native keyring backends to desktop targets

* fix(ci): tolerate read-only gemini review tokens

* test(credential_store): cover race winner sync path
This commit is contained in:
Zsombor Szabo
2026-03-12 08:58:52 -07:00
committed by GitHub
parent 7e22a3d0e3
commit 087066f946
6 changed files with 88 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
Fix `gws auth login` encrypted credential persistence by enabling native keyring backends for the `keyring` crate on supported desktop platforms instead of silently falling back to the in-memory mock store.
+15 -6
View File
@@ -24,6 +24,7 @@ on:
permissions:
contents: write
issues: write
pull-requests: write
jobs:
@@ -125,9 +126,17 @@ jobs:
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['gemini: reviewed'],
});
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['gemini: reviewed'],
});
} catch (e) {
if (e.status === 403) {
console.log(`Token cannot add labels for this review event (${e.message}) — skipping`);
return;
}
throw e;
}
Generated
+35 -2
View File
@@ -203,6 +203,12 @@ version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -332,6 +338,16 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -1291,7 +1307,11 @@ version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [
"byteorder",
"log",
"security-framework 2.11.1",
"security-framework 3.7.0",
"windows-sys 0.60.2",
"zeroize",
]
@@ -2103,7 +2123,7 @@ dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
"security-framework 3.7.0",
]
[[package]]
@@ -2175,6 +2195,19 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.11.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework"
version = "3.7.0"
@@ -2182,7 +2215,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.0",
"core-foundation",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
+9 -1
View File
@@ -52,12 +52,20 @@ derive_builder = "0.20.2"
ratatui = "0.30.0"
crossterm = "0.29.0"
chrono = "0.4.44"
keyring = "3.6.3"
async-trait = "0.1.89"
serde_yaml = "0.9.34"
percent-encoding = "2.3.2"
zeroize = { version = "1.8.2", features = ["derive"] }
[target.'cfg(target_os = "macos")'.dependencies]
keyring = { version = "3.6.3", features = ["apple-native"] }
[target.'cfg(target_os = "windows")'.dependencies]
keyring = { version = "3.6.3", features = ["windows-native"] }
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
keyring = "3.6.3"
# The profile that 'cargo dist' will build with
[profile.dist]
+1
View File
@@ -1556,6 +1556,7 @@ mod tests {
}
#[test]
#[serial_test::serial]
fn config_dir_returns_gws_subdir() {
let path = config_dir();
assert!(path.ends_with("gws"));
+23 -8
View File
@@ -436,6 +436,7 @@ mod tests {
get_state: MockState,
set_succeeds: bool,
last_set: RefCell<Option<String>>,
on_set: RefCell<Option<Box<dyn FnMut(&str)>>>,
}
impl MockKeyring {
@@ -444,6 +445,7 @@ mod tests {
get_state: MockState::Ok(b64.to_string()),
set_succeeds: true,
last_set: RefCell::new(None),
on_set: RefCell::new(None),
}
}
@@ -452,6 +454,7 @@ mod tests {
get_state: MockState::NoEntry,
set_succeeds: true,
last_set: RefCell::new(None),
on_set: RefCell::new(None),
}
}
@@ -460,6 +463,7 @@ mod tests {
get_state: MockState::PlatformError,
set_succeeds: true,
last_set: RefCell::new(None),
on_set: RefCell::new(None),
}
}
@@ -467,6 +471,14 @@ mod tests {
self.set_succeeds = false;
self
}
fn with_on_set<F>(self, callback: F) -> Self
where
F: FnMut(&str) + 'static,
{
*self.on_set.borrow_mut() = Some(Box::new(callback));
self
}
}
impl KeyringProvider for MockKeyring {
@@ -482,6 +494,9 @@ mod tests {
fn set_password(&self, password: &str) -> Result<(), keyring::Error> {
*self.last_set.borrow_mut() = Some(password.to_string());
if let Some(callback) = self.on_set.borrow_mut().as_mut() {
callback(password);
}
if self.set_succeeds {
Ok(())
} else {
@@ -831,19 +846,19 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let key_file = dir.path().join(".encryption_key");
// Simulate: file was created by another process between our generate
// and our save_key_file_exclusive call. We pre-create the file so
// save_key_file_exclusive will fail with AlreadyExists.
let winner_key = [77u8; 32];
std::fs::write(&key_file, STANDARD.encode(winner_key)).unwrap();
let winner_b64 = STANDARD.encode(winner_key);
let race_key_file = key_file.clone();
let race_winner_b64 = winner_b64.clone();
// Use NoEntry so resolve_key goes into the generate path.
let mock = MockKeyring::no_entry();
let mock = MockKeyring::no_entry().with_on_set(move |_| {
if !race_key_file.exists() {
std::fs::write(&race_key_file, &race_winner_b64).unwrap();
}
});
let result = resolve_key(KeyringBackend::Keyring, &mock, &key_file).unwrap();
// Should return the winner's key, not the one we generated.
assert_eq!(result, winner_key);
// The keyring should have been synced with the winner's key.
let synced = mock.last_set.borrow().clone().unwrap();
assert_eq!(STANDARD.decode(&synced).unwrap(), winner_key);
}