Skip to content

Commit f32077d

Browse files
committed
feat: restore domain-wide delegation via GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER
Re-apply DWD support on top of the workspace refactor (#613). Files moved from src/ to crates/google-workspace-cli/src/. - Add get_impersonated_user() helper and IMPERSONATED_USER_ENV constant - Pass impersonated user through get_token() -> get_token_inner() - Set builder.subject() for ServiceAccountAuthenticator when DWD is active - Show impersonated_user in auth status JSON output - Add help text and README documentation for the new env var - Add changeset for minor version bump
1 parent c7c6646 commit f32077d

5 files changed

Lines changed: 46 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@googleworkspace/cli": minor
3+
---
4+
5+
Restore domain-wide delegation support for service accounts via `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` env var

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,22 @@ export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/path/to/service-account.json
199199
gws drive files list
200200
```
201201
202+
#### Domain-Wide Delegation (DWD)
203+
204+
To access user data (Gmail, Calendar, etc.) via a service account with
205+
[domain-wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority),
206+
set the impersonated user:
207+
208+
```bash
209+
export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/path/to/service-account.json
210+
export GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER=user@example.com
211+
gws gmail users messages list --params '{"userId": "me"}'
212+
```
213+
214+
> **Note:** Without `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER`, service accounts
215+
> can only access their own resources. User-scoped APIs like Gmail and Calendar
216+
> require impersonation via DWD.
217+
202218
### Pre-obtained Access Token
203219
204220
Useful when another tool (e.g. `gcloud`) already mints tokens for your environment.
@@ -382,6 +398,7 @@ All variables are optional. See [`.env.example`](.env.example) for a copy-paste
382398
|---|---|
383399
| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority) |
384400
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (user or service account) |
401+
| `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` | Email to impersonate via domain-wide delegation (service accounts only) |
385402
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (alternative to `client_secret.json`) |
386403
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID`) |
387404
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) |

crates/google-workspace-cli/src/auth.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ const PROXY_ENV_VARS: &[&str] = &[
3434
"ALL_PROXY",
3535
];
3636

37+
const IMPERSONATED_USER_ENV: &str = "GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER";
38+
39+
/// Returns the impersonated user email for domain-wide delegation, if set.
40+
pub fn get_impersonated_user() -> Option<String> {
41+
std::env::var(IMPERSONATED_USER_ENV)
42+
.ok()
43+
.filter(|val| !val.trim().is_empty())
44+
}
45+
3746
/// Response from Google's token endpoint
3847
#[derive(Debug, Deserialize)]
3948
struct TokenResponse {
@@ -220,13 +229,14 @@ pub async fn get_token(scopes: &[&str]) -> anyhow::Result<String> {
220229
}
221230

222231
let creds_file = std::env::var("GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE").ok();
232+
let impersonated_user = get_impersonated_user();
223233
let config_dir = crate::auth_commands::config_dir();
224234
let enc_path = credential_store::encrypted_credentials_path();
225235
let default_path = config_dir.join("credentials.json");
226236
let token_cache = config_dir.join("token_cache.json");
227237

228238
let creds = load_credentials_inner(creds_file.as_deref(), &enc_path, &default_path).await?;
229-
get_token_inner(scopes, creds, &token_cache).await
239+
get_token_inner(scopes, creds, &token_cache, impersonated_user.as_deref()).await
230240
}
231241

232242
/// Check if HTTP proxy environment variables are set
@@ -244,6 +254,7 @@ async fn get_token_inner(
244254
scopes: &[&str],
245255
creds: Credential,
246256
token_cache_path: &std::path::Path,
257+
impersonated_user: Option<&str>,
247258
) -> anyhow::Result<String> {
248259
match creds {
249260
Credential::AuthorizedUser(ref secret) => {
@@ -279,10 +290,15 @@ async fn get_token_inner(
279290
.map(|f| f.to_string_lossy().to_string())
280291
.unwrap_or_else(|| "token_cache.json".to_string());
281292
let sa_cache = token_cache_path.with_file_name(format!("sa_{tc_filename}"));
282-
let builder = yup_oauth2::ServiceAccountAuthenticator::builder(key).with_storage(
293+
let mut builder = yup_oauth2::ServiceAccountAuthenticator::builder(key).with_storage(
283294
Box::new(crate::token_storage::EncryptedTokenStorage::new(sa_cache)),
284295
);
285296

297+
// Domain-wide delegation: set the impersonated user (sub claim) on the JWT
298+
if let Some(user) = impersonated_user {
299+
builder = builder.subject(user.to_string());
300+
}
301+
286302
let auth = builder
287303
.build()
288304
.await

crates/google-workspace-cli/src/auth_commands.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,11 @@ async fn handle_status() -> Result<(), GwsError> {
12331233
"token_cache_exists": has_token_cache,
12341234
});
12351235

1236+
// Show impersonated user if set (domain-wide delegation)
1237+
if let Some(user) = crate::auth::get_impersonated_user() {
1238+
output["impersonated_user"] = json!(user);
1239+
}
1240+
12361241
// Show client config (client_secret.json) status
12371242
let config_path = crate::oauth_config::client_config_path();
12381243
let has_config = config_path.exists();

crates/google-workspace-cli/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ fn print_usage() {
487487
println!(
488488
" GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND Keyring backend: keyring (default) or file"
489489
);
490+
println!(" GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER Email to impersonate via domain-wide delegation (SA only)");
490491
println!(" GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE Default Model Armor template");
491492
println!(
492493
" GOOGLE_WORKSPACE_CLI_SANITIZE_MODE Sanitization mode: warn (default) or block"

0 commit comments

Comments
 (0)