Repository navigation
client_secret.json without project_id: silent fallback to ambient gcloud ADC quota_project_id escapes CONFIG_DIR isolation #878
Description
Description
When client_secret.json has no project_id field, gws does not omit the x-goog-user-project quota project — it silently falls back to the ambient gcloud ADC quota_project_id read from ~/.config/gcloud/application_default_credentials.json.
Two problems:
- It escapes
GOOGLE_WORKSPACE_CLI_CONFIG_DIRisolation. The whole point of pointingCONFIG_DIRat a self-contained directory is that the binding is defined by that directory. Instead gws reaches outside it into machine-global gcloud state that the user never associated with this binding. - It fails confusingly. The resulting 403 names a project the user has no relationship with, giving no hint that the value came from ADC. This appears to be the same symptom reported in 403 permission error referencing unknown project ID (not user's project) #291 ("403 permission error referencing unknown project ID (not user's project)", closed) but via a different trigger, so it may be worth a regression check there.
Environment
| Detail | Value |
|---|---|
| gws | 0.22.5 |
| OS | macOS (Darwin 25.5.0, arm64) |
| Auth | OAuth desktop-app flow, file keyring backend |
| gcloud ADC | present, with quota_project_id set to an unrelated project |
Steps to Reproduce
- Have gcloud ADC present with a quota project set — i.e.
~/.config/gcloud/application_default_credentials.jsoncontains"quota_project_id": "<unrelated-project-from-adc>". This is the normal state on any machine wheregcloud auth application-default loginhas been run. - Set up a gws binding in an isolated dir:
GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/path/to/binding, with its ownclient_secret.json, and authenticate. - Remove the
project_idkey from/path/to/binding/client_secret.json(leaveclient_id/client_secretintact — OAuth itself does not needproject_id). - Make any raw Discovery call, e.g.:
GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/path/to/binding \ gws gmail users messages get --params '{"userId":"me","id":"<MSG_ID>","format":"minimal"}'
Expected Behavior
With no project_id in the binding's client_secret.json, gws should either:
- omit
x-goog-user-projectentirely (which per Drive API returns 403 for non-project-member users due to unnecessary x-goog-user-project header #729 is arguably correct for Workspace APIs — quota attributes to the OAuth client automatically); or - fail loudly with a clear message that no quota project is configured for this binding
— but not silently adopt a project from machine-global gcloud state that lives outside the configured CONFIG_DIR.
Actual Behavior
The quota project silently becomes the ADC quota_project_id, and the call 403s against a project the binding never referenced:
{
"error": {
"code": 403,
"message": "Caller does not have required permission to use project <unrelated-project-from-adc>. Grant the caller the roles/serviceusage.serviceUsageConsumer role, or a custom role with the serviceusage.services.use permission, by visiting https://console.developers.google.com/iam-admin/iam?project=<unrelated-project-from-adc> and then retry.",
"reason": "forbidden"
}
}Restoring project_id flips the named project back to the client's own, confirming the fallback chain is:
client_secret.json project_id → (if absent) ADC quota_project_id → (no "none" option)
Additional Context
Honest scoping — two caveats so this is prioritised correctly:
- The trigger is narrow. A
client_secret.jsondownloaded from the Cloud Console always includesproject_id; I reached this state by removing it deliberately, while probing whether the quota stamp of Drive API returns 403 for non-project-member users due to unnecessary x-goog-user-project header #729 could be suppressed. Users are most likely to hit it via a hand-edited or hand-assembled client file. - If Drive API returns 403 for non-project-member users due to unnecessary x-goog-user-project header #729 is fixed by not sending
x-goog-user-projectfor Workspace APIs, this fallback likely becomes moot. I'm filing it separately because the isolation escape is a defect in its own right — a binding scoped byCONFIG_DIRsilently consuming machine-global ADC state is surprising regardless of which project ends up in the header — and because it's a second data point that the quota-project resolution logic is doing more than it needs to.
Possibly related: #857 (GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE ignored when credentials.enc exists) looks like the same genre — ambient/co-located state overriding an explicitly configured binding.
Activity
bhaskoro-muthohar commentedon Jul 19, 2026
Hit the same issue — gws 0.22.x on macOS (Darwin 25.5.0, arm64). Had
gcloud auth application-default loginconfigured for a work project, and gws was silently picking up itsquota_project_idinstead of usingproject_idfrom myclient_secret.json.Removing
quota_project_idfrom~/.config/gcloud/application_default_credentials.jsonfixed it immediately. Thanks for documenting the fallback chain — saved a lot of debugging time.fix(auth): warn on silent ADC quota project fallback (upstream google…
fix(auth): warn on silent ADC quota project fallback (upstream google…
fix(auth): don't take the quota project from global ADC