You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 ADCquota_project_id read from ~/.config/gcloud/application_default_credentials.json.
Two problems:
It escapes GOOGLE_WORKSPACE_CLI_CONFIG_DIR isolation. The whole point of pointing CONFIG_DIR at 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.json contains "quota_project_id": "<unrelated-project-from-adc>". This is the normal state on any machine where gcloud auth application-default login has been run.
Set up a gws binding in an isolated dir: GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/path/to/binding, with its own client_secret.json, and authenticate.
Remove the project_id key from /path/to/binding/client_secret.json (leave client_id / client_secret intact — OAuth itself does not need project_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:
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:
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-project for 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 by CONFIG_DIR silently 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.
Description
When
client_secret.jsonhas noproject_idfield, gws does not omit thex-goog-user-projectquota project — it silently falls back to the ambient gcloud ADCquota_project_idread from~/.config/gcloud/application_default_credentials.json.Two problems:
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.Environment
filekeyring backendquota_project_idset to an unrelated projectSteps to Reproduce
~/.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.GOOGLE_WORKSPACE_CLI_CONFIG_DIR=/path/to/binding, with its ownclient_secret.json, and authenticate.project_idkey from/path/to/binding/client_secret.json(leaveclient_id/client_secretintact — OAuth itself does not needproject_id).Expected Behavior
With no
project_idin the binding'sclient_secret.json, gws should either: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— 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_idflips the named project back to the client's own, confirming the fallback chain is:Additional Context
Honest scoping — two caveats so this is prioritised correctly:
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.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_FILEignored whencredentials.encexists) looks like the same genre — ambient/co-located state overriding an explicitly configured binding.