Skip to content

feat: Implement multi-account Configuration Profiles#358

Open
joeVenner wants to merge 34 commits intogoogleworkspace:mainfrom
joeVenner:main
Open

feat: Implement multi-account Configuration Profiles#358
joeVenner wants to merge 34 commits intogoogleworkspace:mainfrom
joeVenner:main

Conversation

@joeVenner
Copy link

Description

This pull request introduces a comprehensive Configuration Profiles feature, allowing users to seamlessly manage, sandbox, and switch between multiple Google Workspace accounts within gwcli.

In addition to the feature work, this PR includes a massive refactoring effort to migrate the authentication and credential management systems into a purely asynchronous tokio runtime environment, alongside several critical security and concurrency hardening patches.

🚀 Key Features

  • Profile-Aware Configuration Sandboxing: Configurations spanning credentials.json, token_cache.json, .encryption_key, and client_secret.json are now safely sandboxed inside ~/.config/gws/profiles/<name>/.
  • Credential Ring Segregation: Token tracking through the host's underlying macOS Keychain or Windows Credential Manager leverages the dynamically injected <profile> label explicitly (e.g., gws-cli-work), maintaining tight namespace isolation across multi-account deployments.
  • Standardized Global --profile Argument: Migrated the --profile flag resolution to clap natively via allow_external_subcommands, replacing fragile manual AST extraction loops and guaranteeing fallback support mapping through the GOOGLE_WORKSPACE_CLI_PROFILE environment variable.

🛡️ Security & Hardening

  • Mitigated TOCTOU Path Injection: Re-wrote custom GOOGLE_WORKSPACE_CLI_CONFIG_DIR handlers to forcefully instantiate directories prior to canonicalization. This securely seals a Time-of-Check to Time-of-Use window that could allow symlink substitution attacks on configuration directories.
  • Eliminated Custom Path Vulnerabilities: Abstracted and unified all restricted system-directory validations (preventing configs from resolving inside /etc, /usr, .ssh, etc.) into a central is_suspicious_path() helper to eliminate blind spots.
  • Cross-Platform OS Permissions: Hardened underlying keystore file permission assignments utilizing perms.set_mode(), which safely maps existing system metadata instead of destructively replacing behaviors on non-Unix platforms.

⚡ Async I/O & Concurrency Optimizations

  • Tokio Async Migrations: Fully transformed blocking file I/O operations (std::fs, Path::exists(), etc.) across the auth and credential stores into tokio::fs asynchronous executions. This explicitly prevents massive thread starvation intervals and deadlock chains from terminating the async runner during critical I/O handoffs (such as logout sweeps).
  • Idiomatic Async Concurrency: Abstracted concurrent KEY initialization tracking in credential_store.rs down to an idiomatic tokio::sync::OnceCell, cleanly substituting manual tokio::sync::RwLock blocking macros with native get_or_try_init() executors.
  • Data-Race Prevention: Mitigated multi-threaded environment data races triggered by std::env::set_var on explicitly invoked CLI --profile arguments by replacing the unsafe process mutation strategy with a global thread-safe std::sync::OnceLock<String>.

Validation

  • ✅ Safely executes legacy single-account pathways seamlessly.
  • ✅ Tested complete multi-account isolation and hot-swapping via gws auth switch.
  • ✅ Passing 100% of the regression test suite (cargo test --all), representing 426 tests including heavily isolated environment simulation checks.

joeVenner and others added 30 commits March 7, 2026 05:03
Allows users to seamlessly switch between multiple Google Workspace accounts using the '--profile <name>' global flag or the 'gws auth switch <name>' command. This isolates configurations, token caches, and OS keyring storage per profile.
Resolves code review feedback:
- Extracted 'base_config_dir()' to a single location to prevent buggy fallback deduplication across 'auth_commands.rs' and 'credential_store.rs'.
- Fixed 'main.rs' argument filtering logic which incorrectly skipped the values of the '--api-version' flag.
Resolves code review feedback:
- Sanitized profile names during parsing and 'auth switch' to prevent path traversals using '../'.
- Added 'get_active_profile()' to deduplicate the logic of loading the active profile environment variable or fallback file.
- Optimized arguments iteration in 'main.rs'.
Resolves code review feedback:
- Correctly handles and surfacing errors when attempting to delete the active_profile file, while ignoring harmless 'NotFound' errors.
Resolves code review feedback:
- Extracted 'validate_profile_name' to a reusable function in 'auth_commands.rs' and applied it consistently.
- Updated 'credential_store.rs' to rely on the centralized 'get_active_profile' function instead of duplicating environmental checks.
Resolves code review feedback:
- Ensures profile names read from the local 'active_profile' file are validated against path traversal vectors (e.g. '../').
- Fallbacks to the default profile if the user-modified input on disk is flagged as invalid.
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Resolves code review feedback:
- Fixed a compilation error regarding a missing 'else' return branch in 'get_active_profile', and handled the error without using 'process::exit'.
- Simplified the argument iteration loop in 'main.rs' to strip redundant '--api-version' checks since it is handled later by the subcommand filter.
Resolves code review feedback:
- Adds a '.is_empty()' check to 'validate_profile_name()' to prevent silent fallbacks or writing to raw directories like 'profiles/'.
Resolves code review feedback:
- Fixed a bug in 'main.rs' where a dangling '--profile' flag before a subcommand like 'gws --profile drive files list' would swallow 'drive' as the profile name, breaking the command. It now correctly checks if the next argument is another flag or a command and behaves accordingly.
- Strengthened the profile name validation to restrict characters to alphanumerics, hyphens, and underscores, avoiding cross-platform filepath issues on Windows like ':' or '*' being disallowed in file names.
…nd system directories

Resolves critical security code review feedback:
- Validates 'GOOGLE_WORKSPACE_CLI_CONFIG_DIR' to prevent users from accidentally tricking the application into securely writing tokens or cache maps to protected root directories, e.g. '/', '/etc/', or '~/.ssh'. It now safely falls back to the default config directory if a suspicious location is detected.
- Validates 'GOOGLE_WORKSPACE_CLI_PROFILE' by using 'validate_profile_name', ensuring the profile's name cannot path-traverse via '../..' out of the 'profiles/' directory.
Resolves code review feedback:
- Improved the parsing logic for the '--profile' argument in 'main.rs'. Instead of unconditionally consuming the next non-flag argument as a profile name, the parser now checks if the argument matches any known services or built-in commands ('auth', 'schema', 'generate-skills'). This prevents subcommands from being incorrectly interpreted as profiles when a trailing '--profile' flag is omitted.
Resolves code review feedback:
- Removed the restriction that prevented profile names from matching service names (e.g., 'drive' or 'gmail').
- Profile names are now only validated against alphanumeric characters, dashes, and underscores, giving users more flexibility when naming their configuration profiles.
Resolves code review feedback:
- Corrected a regression where '--help' and '--version' mistakenly threw an invalid service parsing error. They are now whitelisted correctly as acceptable 'first_arg' parameters without requiring a subcommand.
- Migrated 'handle_switch' internal file system logic to use asynchronous 'tokio::fs' equivalents instead of 'std::fs', preventing the underlying async tokio thread from arbitrarily blocking during user profile activation.
- Tightened the path string security detection algorithm to ensure legitimate routes with '.ssh' inside the path string (e.g. '/home/user/project-ssh/config') aren't falsely flagged, only asserting when '.ssh' is definitively separated as an individual component folder.
Resolves code review feedback:
- Improved the 'GOOGLE_WORKSPACE_CLI_CONFIG_DIR' security validation in 'base_config_dir' to use rust's 'Path' methods instead of string representations, correctly securing Windows platforms against root directory writes.
- Enclosed UNIX specific path directory checks ('/etc', '/bin', etc) in a 'cfg!(unix)' target filter so they do not inadvertently conflict in non-POSIX environments.
Resolves code review feedback:
- Fixed an issue where the single-pass argument iterator combined '--profile' and generic argument extraction but skipped over the logic required to bypass the '--api-version' parameter and its subsequent value.
- Implemented a two-pass approach that cleanly extracts '--profile' values first into a filtered list, and then loops over the result to locate the 'first_arg' service string while correctly omitting any '--api-version' values.
Resolves code review feedback:
- Improved the 'GOOGLE_WORKSPACE_CLI_CONFIG_DIR' security check in 'base_config_dir' by resolving symlinks natively with 'std::fs::canonicalize' before inspecting the payload for restricted system origins. This prevents users from sneaking past the check using malicious directory shortcuts to system-critical routes like '/etc/'.
- Tightened 'validate_profile_name' to exclusively allow lowercase alphanumeric characters, dashes, and underscores. This mitigates critical path collisions on case-insensitive filesystems (such as default macOS or Windows) where 'MyProfile' and 'myprofile' resolve to identical folders but could conceptually conflict internally.
Resolves code review feedback:
- Fixed a Time-of-check to time-of-use (TOCTOU) vulnerability where the configuration directory was being checked successfully using the canonicalized symlink version, but erroneously returned the original, unchecked symlinked string allowing the symlink destination to be hot-swapped post-verification.
Resolves code review feedback:
- Fixed a path traversal security gap where if a 'GOOGLE_WORKSPACE_CLI_CONFIG_DIR' directory did not exist yet (meaning 'canonicalize' falls back to the raw path string), users could theoretically bypass restrictions using dot-dot operators, e.g., '/tmp/nonexistent/../../etc/'.
- Explicitly blocked any path containing '..' string components.
Resolves code review feedback:
- Enforced a rule inside 'validate_profile_name' that explicitly forbids profile names from starting with a hyphen ('-').
- This prevents CLI parsing ambiguity where a user passes a profile name like '-x', accidentally tricking the argument parser into interpreting it as a command-line flag rather than a valid configuration directory.
…n blocks, and consolidate keyring cache write-locks
@joeVenner joeVenner requested a review from jpoehnelt as a code owner March 10, 2026 01:45
@changeset-bot
Copy link

changeset-bot bot commented Mar 10, 2026

⚠️ No Changeset found

Latest commit: 7784355

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@googleworkspace-bot googleworkspace-bot added area: auth area: discovery area: core Core CLI parsing, commands, error handling, utilities labels Mar 10, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust multi-account configuration profiles feature, enabling seamless management and isolation of Google Workspace accounts within gwcli. Concurrently, it undertakes a significant refactoring to migrate authentication and credential management to a fully asynchronous tokio runtime, drastically improving I/O performance and concurrency. The changes also include critical security enhancements, such as mitigating TOCTOU vulnerabilities, enforcing strict path validations, and hardening cross-platform file permissions, ensuring a more secure and stable user experience.

Highlights

  • Multi-account Configuration Profiles: Introduced a new feature allowing users to manage and switch between isolated configuration profiles for different Google Workspace accounts. This includes sandboxing credentials.json, token_cache.json, .encryption_key, and client_secret.json within ~/.config/gws/profiles/<name>/.
  • Asynchronous I/O Migration (Tokio): Migrated all blocking file I/O operations across authentication and credential management to tokio::fs asynchronous executions, preventing thread starvation and deadlocks. This includes std::fs::read, write, exists, create_dir_all, remove_file, and set_permissions.
  • Enhanced Security Hardening: Rewrote config directory handlers to create directories before canonicalization, closing a Time-of-Check to Time-of-Use vulnerability for symlink attacks. Implemented is_suspicious_path() to prevent configuration files from resolving in restricted system directories (e.g., /etc, /usr, .ssh). Hardened keystore file permission assignments using perms.set_mode() for safer, cross-platform metadata mapping.
  • Credential Ring Segregation: Token tracking in OS keyrings (macOS Keychain, Windows Credential Manager) now explicitly leverages the active profile name (e.g., gws-cli-work) to ensure tight namespace isolation for multi-account deployments.
  • Robust CLI Argument Handling: Migrated --profile flag resolution to clap's native allow_external_subcommands and std::sync::OnceLock, eliminating fragile manual parsing and preventing data races from std::env::set_var in multi-threaded environments.
Changelog
  • src/auth.rs
    • Updated config_dir and encrypted_credentials_path calls to be asynchronous
    • Replaced PathBuf::exists() with tokio::fs::metadata().await.is_ok() for async file existence checks
    • Converted credential_store::encrypt and decrypt calls to their asynchronous versions
    • Migrated std::fs operations to tokio::fs for asynchronous file handling
    • Modified resolve_client_credentials and handle_logout functions to be asynchronous
    • Adapted test functions to use #[tokio::test] and async
  • src/auth_commands.rs
    • Introduced base_config_dir as an asynchronous function for base configuration directory resolution, including TOCTOU mitigation and suspicious path validation
    • Implemented is_suspicious_path to check for restricted directory usage
    • Added OVERRIDE_PROFILE using std::sync::OnceLock for thread-safe global profile management
    • Created get_active_profile to determine the current profile from various sources
    • Defined validate_profile_name for enforcing valid profile naming conventions
    • Updated config_dir, plain_credentials_path, and token_cache_path to be asynchronous and profile-aware
    • Added switch subcommand to handle_auth_command for profile switching
    • Converted handle_logout and resolve_client_credentials to asynchronous functions
    • Updated test functions to use #[tokio::test] and async
  • src/commands.rs
    • Added a global --profile argument to the CLI command builder
  • src/credential_store.rs
    • Replaced std::sync::OnceLock with tokio::sync::OnceCell for the encryption key, making get_or_create_key asynchronous
    • Modified keyring integration to use profile-specific service names for segregation
    • Converted all file system operations (read_to_string, create_dir_all, set_permissions, write) to their tokio::fs asynchronous counterparts
    • Updated encrypt, decrypt, encrypted_credentials_path, save_encrypted, load_encrypted_from_path, and load_encrypted functions to be asynchronous
    • Ensured cross-platform permission setting for key files using tokio::fs::metadata and set_permissions
    • Adapted test functions to use #[tokio::test] and async
  • src/discovery.rs
    • Updated cache_dir to use the asynchronous config_dir
    • Migrated file system operations for cache management (create_dir_all, metadata, read_to_string, write) to tokio::fs
  • src/main.rs
    • Refactored argument parsing to leverage clap's allow_external_subcommands for global flags, including --profile
    • Integrated logic to process the --profile argument and set the global OVERRIDE_PROFILE
    • Adjusted command dispatch for schema, generate-skills, and auth to use clap's subcommand parsing
    • Updated print_usage to reflect the new --profile flag and GOOGLE_WORKSPACE_CLI_PROFILE environment variable
  • src/oauth_config.rs
    • Converted client_config_path, save_client_config, and load_client_config functions to be asynchronous
    • Migrated file system operations (create_dir_all, set_permissions, read_to_string) to tokio::fs
    • Replaced crate::fs_util::atomic_write with crate::fs_util::atomic_write_async
    • Adapted test functions to use #[tokio::test] and async
  • src/setup.rs
    • Modified manual_oauth_instructions to be asynchronous and use the asynchronous client_config_path
    • Updated stage_configure_oauth to await asynchronous credential loading and client config saving
    • Converted run_setup to await the asynchronous client_config_path
  • src/token_storage.rs
    • Updated decrypt and encrypt calls to their asynchronous versions
    • Migrated file system permission setting for token storage directories to tokio::fs
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Generative AI Prohibited Use Policy, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is an impressive and comprehensive pull request that successfully introduces the multi-account configuration profiles feature. The massive refactoring to a purely asynchronous tokio runtime is well-executed and significantly hardens the application against TOCTOU vulnerabilities, data races, and I/O blocking. The migration from manual argument parsing to clap's subcommand handling is also a major improvement for robustness.

My review has identified a critical issue where some synchronous file system calls remain in an async function, which could block the tokio runtime. Addressing this will ensure the full benefits of the async migration are realized.

Note: Security Review did not run due to the size of the PR.

let token_cache = token_cache_path();
let plain_path = plain_credentials_path().await;
let enc_path = credential_store::encrypted_credentials_path().await;
let token_cache = token_cache_path().await;
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

While the path resolutions are now correctly async, the subsequent checks for file existence on lines 1059-1061 use the synchronous Path::exists(). These blocking I/O calls can starve the tokio runtime, which undermines a primary goal of this refactoring.

Please replace these with the non-blocking equivalent:

    let has_encrypted = tokio::fs::metadata(&enc_path).await.is_ok();
    let has_plain = tokio::fs::metadata(&plain_path).await.is_ok();
    let has_token_cache = tokio::fs::metadata(&token_cache).await.is_ok();


// Show client config (client_secret.json) status
let config_path = crate::oauth_config::client_config_path();
let config_path = crate::oauth_config::client_config_path().await;
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

Similar to the previous comment, the check on the next line (config_path.exists()) is a synchronous, blocking I/O call within an async function. This should be updated to use the non-blocking tokio::fs::metadata to avoid starving the executor.

Please change line 1089 to:

let has_config = tokio::fs::metadata(&config_path).await.is_ok();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth area: core Core CLI parsing, commands, error handling, utilities area: discovery

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants