Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/document-env-vars.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---

Document all environment variables and enable GOOGLE_WORKSPACE_CLI_CONFIG_DIR in release builds
41 changes: 29 additions & 12 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
# OAuth Client Credentials
# Create these at https://console.cloud.google.com/apis/credentials
GOOGLE_WORKSPACE_CLI_CLIENT_ID=
GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=
# gws — Google Workspace CLI
# Copy this file to .env and uncomment the variables you need.
# All variables are optional. See README.md for details.

# Authentication
# Path to a service account JSON key file or user credentials
# ── Authentication ────────────────────────────────────────────────
# Pre-obtained OAuth2 access token (highest priority; bypasses all credential loading)
# GOOGLE_WORKSPACE_CLI_TOKEN=

# Path to OAuth credentials JSON (user or service account)
# GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=

# Impersonation (Domain-Wide Delegation)
# Email address of the user to impersonate when using a service account
# Default account email for multi-account usage (overridden by --account flag)
# GOOGLE_WORKSPACE_CLI_ACCOUNT=

# Email of user to impersonate via Domain-Wide Delegation (service accounts only)
# GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER=

# Model Armor Sanitization
# Default template resource name for --sanitize
# GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE=projects/my-project/locations/us-central1/templates/my-template
# Sanitization mode: 'warn' (default) or 'block'
# ── OAuth Client ──────────────────────────────────────────────────
# OAuth client ID and secret (alternative to saving client_secret.json)
# GOOGLE_WORKSPACE_CLI_CLIENT_ID=
# GOOGLE_WORKSPACE_CLI_CLIENT_SECRET=

# ── Configuration ─────────────────────────────────────────────────
# Override the config directory (default: ~/.config/gws)
# GOOGLE_WORKSPACE_CLI_CONFIG_DIR=

# ── Model Armor (response sanitization) ──────────────────────────
# Default Model Armor template (overridden by --sanitize flag)
# GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE=
# Sanitization mode: warn (default) or block
# GOOGLE_WORKSPACE_CLI_SANITIZE_MODE=warn

# ── Helpers ───────────────────────────────────────────────────────
# GCP project ID fallback for gmail watch and events subscribe (overridden by --project)
# GOOGLE_WORKSPACE_PROJECT_ID=
26 changes: 26 additions & 0 deletions .gemini/style_guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Code Review Style Guide

## Project Architecture

`gws` is a Rust CLI that dynamically generates commands from Google Discovery Documents at runtime. It does NOT use generated Rust crates (`google-drive3`, etc.) for API interaction. Do not suggest adding API-specific crates to `Cargo.toml`.

## Security: Trusted vs Untrusted Inputs

This CLI is frequently invoked by AI/LLM agents. CLI arguments may be adversarial.

- **CLI arguments (untrusted)** — Must validate paths against traversal (`../../`), reject control characters, percent-encode URL path segments, and use `reqwest .query()` for query parameters. Validators: `validate_safe_output_dir()`, `validate_safe_dir_path()`, `encode_path_segment()`, `validate_resource_name()`.
- **Environment variables (trusted)** — Set by the user in their shell profile, `.env` file, or deployment config. Do NOT flag missing path validation on environment variable values. This is consistent with `XDG_CONFIG_HOME`, `CARGO_HOME`, etc.

## Test Coverage

The `codecov/patch` check requires new/modified lines to be covered by tests. Prefer extracting testable helper functions over embedding logic in `main`/`run`. Tests should cover both happy paths and rejection paths (e.g., pass `../../.ssh` and assert `Err`).

## Changesets

Every PR must include a `.changeset/<name>.md` file. Use `patch` for fixes/chores, `minor` for features, `major` for breaking changes.

## Code Style

- Rust: `cargo clippy -- -D warnings` must pass. `cargo fmt` enforced via pre-commit hook.
- Node.js: Use `pnpm` not `npm`.
- OAuth scope strings in test code will trigger "restricted/sensitive scope" warnings — these are expected and should be ignored.
44 changes: 40 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ ASCII art title cards live in `art/`. The `scripts/show-art.sh` helper clears th
> [!IMPORTANT]
> This CLI is frequently invoked by AI/LLM agents. Always assume inputs can be adversarial — validate paths against traversal (`../../.ssh`), restrict format strings to allowlists, reject control characters, and encode user values before embedding them in URLs.
> [!NOTE]
> **Environment variables are trusted inputs.** The validation rules above apply to **CLI arguments** that may be passed by untrusted AI agents. Environment variables (e.g. `GOOGLE_WORKSPACE_CLI_CONFIG_DIR`) are set by the user themselves — in their shell profile, `.env` file, or deployment config — and are not subject to path traversal validation. This is consistent with standard conventions like `XDG_CONFIG_HOME`, `CARGO_HOME`, etc.
### Path Safety (`src/validate.rs`)

When adding new helpers or CLI flags that accept file paths, **always validate** using the shared helpers:
Expand Down Expand Up @@ -165,7 +168,40 @@ Use these labels to categorize pull requests and issues:

## Environment Variables

- `GOOGLE_WORKSPACE_CLI_TOKEN` — Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading)
- `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` — Path to OAuth credentials JSON (no default; if unset, falls back to credentials secured by the OS Keyring and encrypted in `~/.config/gws/`)
- `GOOGLE_WORKSPACE_CLI_ACCOUNT` — Default account email for multi-account usage (overridden by `--account` flag)
- Supports `.env` files via `dotenvy`
### Authentication

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading) |
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (no default; if unset, falls back to credentials secured by the OS Keyring and encrypted in `~/.config/gws/`) |
| `GOOGLE_WORKSPACE_CLI_ACCOUNT` | Default account email for multi-account usage (overridden by `--account` flag) |
| `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` | Email of user to impersonate with Domain-Wide Delegation (service accounts only) |
| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google ADC path; used as fallback when no gws-specific credentials are configured |

### Configuration

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override the config directory (default: `~/.config/gws`) |

### OAuth Client

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (for `gws auth login` when no `client_secret.json` is saved) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID` above) |

### Sanitization (Model Armor)

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template (overridden by `--sanitize` flag) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |

### Helpers

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID fallback for `gmail watch` and `events subscribe` helpers (overridden by `--project` flag) |

All variables can also live in a `.env` file (loaded via `dotenvy`).
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ npm install -g @googleworkspace/cli
- [AI Agent Skills](#ai-agent-skills)
- [MCP Server](#mcp-server)
- [Advanced Usage](#advanced-usage)
- [Environment Variables](#environment-variables)
- [Architecture](#architecture)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
Expand Down Expand Up @@ -356,6 +357,25 @@ gws gmail users messages get --params '...' \
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |

## Environment Variables

All variables are optional. See [`.env.example`](.env.example) for a copy-paste template.

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority) |
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (user or service account) |
| `GOOGLE_WORKSPACE_CLI_ACCOUNT` | Default account email (overridden by `--account` flag) |
| `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` | Email for Domain-Wide Delegation (service accounts) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (alternative to `client_secret.json`) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID`) |
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID fallback for helper commands |

Environment variables can also be set in a `.env` file (loaded via [dotenvy](https://crates.io/crates/dotenvy)).

## Architecture

`gws` uses a **two-phase parsing** strategy:
Expand Down
5 changes: 2 additions & 3 deletions src/auth_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ const READONLY_SCOPES: &[&str] = &[
];

pub fn config_dir() -> PathBuf {
#[cfg(test)]
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
return PathBuf::from(dir);
}
Comment on lines 95 to 97
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

While the AGENTS.md file was updated to state that environment variables are trusted inputs, using the GOOGLE_WORKSPACE_CLI_CONFIG_DIR variable without validation introduces a significant security risk. An attacker who can control the environment variables of the process running gws could set this variable to an arbitrary path. This could lead to:

  1. Credential Exfiltration: Pointing the config directory to a location the attacker can read (e.g., /tmp/gws-config) to steal credentials and tokens.
  2. Impersonation/Privilege Escalation: Pointing to a directory containing attacker-controlled credentials.

Although other environment variables like LD_PRELOAD can be more dangerous, defense-in-depth is crucial, especially for a security-sensitive tool that handles credentials. The project already has path validation logic in src/validate.rs (as mentioned in AGENTS.md). This logic should be applied to the path provided by GOOGLE_WORKSPACE_CLI_CONFIG_DIR to prevent path traversal attacks (CWE-22).

Suggested change
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
return PathBuf::from(dir);
}
if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") {
if crate::validate::validate_safe_dir_path(&dir).is_ok() {
return PathBuf::from(dir);
} else {
eprintln!("Warning: GOOGLE_WORKSPACE_CLI_CONFIG_DIR value ('{}') is invalid or points to a restricted path. Falling back to default config directory.", dir);
}
}

Expand Down Expand Up @@ -266,7 +265,7 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
}

// Determine scopes: explicit flags > interactive TUI > defaults
let mut scopes = resolve_scopes(
let scopes = resolve_scopes(
&filtered_args,
project_id.as_deref(),
services_filter.as_ref(),
Expand All @@ -277,7 +276,7 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> {
// gmail.metadata blocks query parameters like `q`, and is redundant
// when broader scopes (gmail.modify, gmail.readonly, mail.google.com)
// are already included.
let scopes = filter_redundant_restrictive_scopes(scopes);
let mut scopes = filter_redundant_restrictive_scopes(scopes);

let secret = yup_oauth2::ApplicationSecret {
client_id: client_id.clone(),
Expand Down
13 changes: 13 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,19 @@ fn print_usage() {
println!(
" GOOGLE_WORKSPACE_CLI_ACCOUNT Default account email for multi-account"
);
println!(
" GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER Email for Domain-Wide Delegation (service accounts)"
);
println!(
" GOOGLE_WORKSPACE_CLI_CONFIG_DIR Override config directory (default: ~/.config/gws)"
);
println!(" GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE Default Model Armor template");
println!(
" GOOGLE_WORKSPACE_CLI_SANITIZE_MODE Sanitization mode: warn (default) or block"
);
println!(
" GOOGLE_WORKSPACE_PROJECT_ID GCP project ID fallback for helper commands"
);
println!();
println!("COMMUNITY:");
println!(" Star the repo: https://github.com/googleworkspace/cli");
Expand Down
Loading