Skip to content

goetchstone/gamgui

Repository files navigation

GamGUI

A free, local, open-source macOS GUI for GAM7 — administer Google Workspace (users, groups, signatures, delegates, vacation responders, reports, and more) without memorizing CLI commands, with your credentials kept in the macOS Keychain.

GAM exposes far more of Google Workspace than the Admin Console surfaces (Gmail signatures/delegates/forwarding, advanced group settings, bulk operations, reporting). GamGUI puts a safe, native front end on top of it.

Status

Actively developed and used against live Google Workspace tenants. Working today:

  • Setup wizard — first-run GAM project / OAuth / domain-wide-delegation flow.
  • Users — fast list/search/detail (cached + paginated), profile editing (title/department/location) with a bulk "assign store" tool, mailbox delegates, vacation responders, and a guarded suspend.
  • Gmail signatures — a scoped designer with variables, a live preview, and bulk apply.
  • Groups — membership management, including a drag-and-drop board.
  • Calendars — find any shared calendar by name (instant, from a local index that scales to large tenants), see who has access, search a calendar's events, and remove a stray event or an entire orphaned secondary calendar.
  • Lifecycle — a guided offboarding routine (reset password → delegate → auto-responder → transfer Drive & calendars → remove from everyone's calendars → reminder on the manager), with a live preview of the generated auto-reply.
  • Reports — 2SV gaps, inactive accounts, admins, missing recovery, and directory completeness.

You build and run it yourself; it is not yet notarized for distribution to other Macs.

Destructive actions are guarded — but verify before trusting them on production. Suspend, account delete, calendar/event delete, data transfer, the offboarding routine, and bulk operations all run behind a preview → typed confirmation → audit-logged path. A few of the newer ones haven't yet been exercised against a live tenant, so run them once on a throwaway test user/event before relying on them. Account deletion is reversible only within Google's ~20-day window. GamGUI is provided as-is under the MIT License, with no warranty — use at your own risk; you are responsible for what you run against your own tenant.

Design goals

  • Local & native — a single bundled .app; no server, nothing leaves your machine.
  • Secure — secrets live in the macOS Keychain; GAM's plaintext credential files are materialized into a locked-down temporary directory only for the duration of each gam invocation, then wiped. (details)
  • Easy but powerful — form/table UI for the common painful tasks, full GAM power underneath.
  • Connector-ready — built around a connector protocol, so the Google Workspace connector is cleanly isolated and other systems could be added later without touching the UI.

Architecture

HTMX views → FastAPI routes → Services → Connector protocol → GAMConnector
                                              → GAMRunner (subprocess)
                                              → SecretsVault (Keychain) + EphemeralConfig (temp GAMCFGDIR)

Wrapped in a pywebview native window (WKWebView). See docs/the plan for the full design.

Security model

GAM stores credentials as plaintext files (client_secrets.json, oauth2.txt, oauth2service.json) in its config dir. oauth2service.json can impersonate any user in the domain and oauth2.txt is effectively an admin password, so GamGUI:

  1. keeps the canonical copies in the Keychain (keyring, device-bound, not synced);
  2. materializes them into a chmod 700 temp dir (files chmod 600) set as GAMCFGDIR only for each gam call;
  3. wipes that dir on completion (success or failure);
  4. writes refreshed OAuth tokens back to the Keychain.

Build from source

Requirements: Python 3.9+ and macOS (to run the native window; the test suite itself runs on Linux too). No Google credentials are needed to build or test.

git clone <repo-url> && cd gamgui
make setup     # create .venv, install dev + native-window deps
make gam       # vendor the pinned GAM7 binary into gamgui/resources/gam7 (needs network)
make test      # offline test suite — uses a mock gam, no binary/credentials required
make run       # launch the app (native window; prints a browser URL if pywebview is absent)

make help lists all targets. Prefer raw commands? pip install -e ".[dev,desktop]", then scripts/fetch_gam.sh, pytest, python -m gamgui.app. For an exact pinned install instead of the flexible one, use pip install -r requirements.txt.

The GAM7 binary is not committed (platform-specific, large) — make gam / scripts/fetch_gam.sh fetches the pinned version (v7.46.02) from the official releases and records its checksum.

Build a standalone .app (macOS)

make app       # PyInstaller -> dist/GamGUI.app (bundles Python + the GAM7 binary)

For distribution to other Macs you must codesign + notarize the bundle (including the embedded gam binary); running it yourself needs no signing.

Stop the Keychain prompts

By default the app is ad-hoc signed, so macOS treats each rebuild as a new identity and re-prompts for the Keychain on every launch — and "Always Allow" never sticks. The fix (no Apple Developer account needed — that's only for shipping the app to other people's Macs) is a stable self-signed code-signing cert named GamGUI Local. Once it exists in your login keychain, scripts/build_app.sh signs with it automatically, so your one-time Always Allow persists across launches and rebuilds.

Create it once, either way:

  • GUI: Keychain Access → Certificate Assistant → Create a Certificate… → name it GamGUI Local, Identity Type Self-Signed Root, Certificate Type Code Signing.
  • CLI:
    D=$(mktemp -d)
    printf '[req]\ndistinguished_name=dn\nx509_extensions=v3\nprompt=no\n[dn]\nCN=GamGUI Local\n[v3]\nbasicConstraints=critical,CA:false\nkeyUsage=critical,digitalSignature\nextendedKeyUsage=critical,codeSigning\n' > "$D/c.cnf"
    openssl req -x509 -newkey rsa:2048 -keyout "$D/k.pem" -out "$D/c.pem" -days 3650 -nodes -config "$D/c.cnf"
    openssl pkcs12 -export -inkey "$D/k.pem" -in "$D/c.pem" -out "$D/id.p12" -passout pass:gamgui-local -name "GamGUI Local"
    security import "$D/id.p12" -P gamgui-local -T /usr/bin/codesign && rm -rf "$D"

Then rebuild (make app). The first launch still asks once per credential — click Always Allow on each — and you won't be prompted again, even after future rebuilds. (Override the cert name with CODESIGN_IDENTITY=…. The cert is local and not trusted for distribution by design — it only quiets your own Keychain.)

The app also caches the three secrets in-process for a sliding window (default 5 min) so a burst of actions doesn't re-prompt; tune with GAMGUI_SECRET_CACHE_TTL (seconds; 0 disables).

Staying current with GAM (and not breaking on updates)

GamGUI pins a tested GAM7 version — EXPECTED_GAM_VERSION in gamgui/core/gam/commands.py, matched by scripts/fetch_gam.sh. It never auto-upgrades; you bump deliberately, and three guards keep that safe:

  • Release-watch (.github/workflows/gam-watch.yml, weekly) opens an issue when GAM ships a version newer than the pin — awareness, not auto-upgrade.
  • Compat check (gam-compat CI job + tests/test_command_contract.py) asserts every GAM sub-command our builders use still exists in the vendored command reference, so a renamed/removed command fails CI rather than your tenant. A non-blocking gam-latest-preview job runs the same check against the newest GAM as an early warning.
  • Runtime self-check — if the running gam differs from the tested version (e.g. a GAMGUI_GAM_BINARY override), the setup screen shows a soft warning. It never blocks.

To bump GAM:

  1. make gam --tag vX.Y.Z — re-vendor the binary, command reference, and checksum.
  2. Update EXPECTED_GAM_VERSION (gamgui/core/gam/commands.py) and TAG (scripts/fetch_gam.sh).
  3. make test — the command-contract test flags any sub-command that changed.
  4. Skim gamgui/resources/gam7/GamUpdate.txt for breaking changes.
  5. .venv/bin/python scripts/acceptance.py against a tenant — read-only; the true output-shape check.
  6. Commit.

Tests & CI

pytest is fully offline (mock gam + in-memory Keychain). CI runs it on Ubuntu and macOS across Python 3.9 and 3.12 — see .github/workflows/ci.yml.

Email signatures

The Signatures screen designs one HTML signature with variables, previews it rendered for a real person, and applies it in bulk — scoped to a single user (for testing), a group, an org unit, a department, a location, or the whole company. Each user's current signature is also shown rendered on their detail page.

Template variables (filled per user from the directory): {name} {first} {last} {email} {title} ({role} is an alias) {phone} {department} {location} {ou}. Wrap a fragment in [[ … ]] to drop it when a variable inside is empty — e.g. [[{title} · ]] vanishes for people with no title, so one template can roll out before every profile is filled in.

Hosting signature images (logo, social icons)

Gmail does not allow inline/base64 images or Google Drive links in signatures — every image must be a file at a public HTTPS URL. GamGUI is a local app and doesn't host images itself; you point the template's <img src="…"> at wherever you host them. Whatever host you choose, the URL must be:

  • HTTPS and anonymously reachable — Gmail fetches images through its own proxy (no cookies/referer) and caches them. Test a URL in a private/incognito window; if it loads there, Gmail can fetch it.
  • served with the correct Content-Type (image/png, …) and no hotlink/referer protection — referer-based protection is the usual cause of "the logo shows for me but not for recipients."
  • versioned by filename when an image changes (logo-2026.png) — Gmail caches by URL, so overwriting the same name can keep serving the old one.

Size icons ~2× their display size and set explicit width/height on each <img>.

Where to host — pick one:

  • A web host you already have (simplest). Drop the files in a public folder, e.g. https://yourdomain.com/email/logo.png. Done.
  • Google Cloud Storage (Google-native; reuse the GCP project GAM created). Requires a billing account linked to the project — but small signature assets fall under the Always-Free tier, so the bill rounds to $0:
    1. Cloud Console → Billing → link a billing account to the project (if not already).
    2. Cloud Storage → Create bucket — globally-unique name, a US region, Standard class, Uniform bucket-level access.
    3. Make objects public: bucket Permissions → Grant access → principal allUsers → role Storage Object Viewer. (If your org enforces Public access prevention, allow it on this bucket.)
    4. Upload the images.
    5. Reference them at https://storage.googleapis.com/<bucket>/<path>/logo.png. (Pricing changes — confirm the current free-tier limits, but for a handful of small PNGs it is effectively free.)
  • GitHub + jsDelivr (free, no billing). Commit the images to a public repo and serve them via the jsDelivr CDN: https://cdn.jsdelivr.net/gh/<user>/<repo>@<branch>/path/logo.png. CDN-fast, no card.
  • Cloudflare R2 / Amazon S3 — or any public-object store — also work.

License

MIT — see LICENSE.

About

Free, open-source macOS GUI for GAM7 — administer Google Workspace from a native app: users, groups, signatures, delegates, vacation, and reports, with credentials kept in your macOS Keychain.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors