Skip to content

Commit bb5a4b6

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 6a45832 commit bb5a4b6

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
@@ -24,6 +24,15 @@ use anyhow::Context;
2424

2525
use crate::credential_store;
2626

27+
const IMPERSONATED_USER_ENV: &str = "GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER";
28+
29+
/// Returns the impersonated user email for domain-wide delegation, if set.
30+
pub fn get_impersonated_user() -> Option<String> {
31+
std::env::var(IMPERSONATED_USER_ENV)
32+
.ok()
33+
.filter(|val| !val.trim().is_empty())
34+
}
35+
2736
/// Returns the project ID to be used for quota and billing (sets the `x-goog-user-project` header).
2837
///
2938
/// Priority:
@@ -164,19 +173,21 @@ pub async fn get_token(scopes: &[&str]) -> anyhow::Result<String> {
164173
}
165174

166175
let creds_file = std::env::var("GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE").ok();
176+
let impersonated_user = get_impersonated_user();
167177
let config_dir = crate::auth_commands::config_dir();
168178
let enc_path = credential_store::encrypted_credentials_path();
169179
let default_path = config_dir.join("credentials.json");
170180
let token_cache = config_dir.join("token_cache.json");
171181

172182
let creds = load_credentials_inner(creds_file.as_deref(), &enc_path, &default_path).await?;
173-
get_token_inner(scopes, creds, &token_cache).await
183+
get_token_inner(scopes, creds, &token_cache, impersonated_user.as_deref()).await
174184
}
175185

176186
async fn get_token_inner(
177187
scopes: &[&str],
178188
creds: Credential,
179189
token_cache_path: &std::path::Path,
190+
impersonated_user: Option<&str>,
180191
) -> anyhow::Result<String> {
181192
match creds {
182193
Credential::AuthorizedUser(secret) => {
@@ -200,10 +211,15 @@ async fn get_token_inner(
200211
.map(|f| f.to_string_lossy().to_string())
201212
.unwrap_or_else(|| "token_cache.json".to_string());
202213
let sa_cache = token_cache_path.with_file_name(format!("sa_{tc_filename}"));
203-
let builder = yup_oauth2::ServiceAccountAuthenticator::builder(key).with_storage(
214+
let mut builder = yup_oauth2::ServiceAccountAuthenticator::builder(key).with_storage(
204215
Box::new(crate::token_storage::EncryptedTokenStorage::new(sa_cache)),
205216
);
206217

218+
// Domain-wide delegation: set the impersonated user (sub claim) on the JWT
219+
if let Some(user) = impersonated_user {
220+
builder = builder.subject(user.to_string());
221+
}
222+
207223
let auth = builder
208224
.build()
209225
.await

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

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

1076+
// Show impersonated user if set (domain-wide delegation)
1077+
if let Some(user) = crate::auth::get_impersonated_user() {
1078+
output["impersonated_user"] = json!(user);
1079+
}
1080+
10761081
// Show client config (client_secret.json) status
10771082
let config_path = crate::oauth_config::client_config_path();
10781083
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)