Headless Google Workspace MDM-security tenant enablement. A series of management API calls — packaged as Terraform + small TypeScript scripts — that turn on every Workspace function relevant to mobile device management (telemetry and response actions) and produce a verified handoff packet so a downstream technical person can connect their apparatus without touching admin.google.com.
What this is. The rail laying — APIs enabled, DWD authorizations registered, Pub/Sub topics provisioned, Workspace Events subscriptions stood up, IAM bindings granted to a separately-owned apparatus, scope verification probed live, handoff packet emitted.
What this is not. The apparatus itself. No collector, no forwarder, no downstream-SIEM wiring. The apparatus team consumes the handoff packet and connects from their side.
┌──────────────────────────────────────────────┐
│ Workspace tenant │
│ (Admin SDK / Cloud Identity / Alert Center │
│ / Workspace Events / Chrome Management) │
└──────────────────────┬───────────────────────┘
│
┌────────────────┴────────────────┐
│ THIS repo (Terraform + │
│ TS scripts) │
│ │
│ • Enables Cloud APIs │
│ • Registers DWD scope sets via │
│ DWD-API for apparatus client │
│ • Provisions Pub/Sub topics │
│ • Creates Workspace Events subs │
│ • Sets up audit log → Pub/Sub │
│ • Probes scopes live │
│ • Emits handoff.json + .md │
└────────────────┬────────────────┘
│ handoff packet
▼
┌─────────────────────────────────┐
│ Apparatus (apparatus-side │
│ GCP — NOT in this repo) │
│ │
│ • Reads Pub/Sub subscriptions │
│ • Polls Reports / Alert / etc │
│ • Calls action endpoints with │
│ writer DWD │
│ • Forwards to its SIEM │
└─────────────────────────────────┘
gcloudauthenticated as someone with Workspace super-admin + GCP org admin (one user, the MDR engineer)- A free GCP project ID to bootstrap, an org ID, and a billing account
- The apparatus team's OAuth 2.0 client ID for their reader SA (21-digit
unique_idfield). Optionally a second client ID for their writer SA, plus a subscriber SA email - Terraform ≥ 1.6
- Bun ≥ 1.1
The one OAuth consent screen you encounter (gcloud auth login) is Google's standard sign-in flow, NOT admin.google.com. See Residual UI.
# 1. Bootstrap GCP project (one-shot — cannot be Terraform)
PROJECT_ID=pai-gws-mdm-enablement \
ORG_ID=123456789012 \
BILLING_ID=000000-AAAAAA-BBBBBB \
./scripts/bootstrap-gcp.sh
# 2. Apply Terraform — enables APIs, provisions Pub/Sub topics + IAM,
# creates Workspace Events subscriptions (skipping unsupported types).
cd terraform
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # fill in customer ID, super-admin, apparatus client IDs
terraform init
terraform apply
cd ..
# 3. Register DWD scope sets against the apparatus's OAuth client ID(s).
# Idempotent — re-running adds any missing scopes without duplicating.
cp .env.example .env
$EDITOR .env # fill in the same values from terraform.tfvars
bun run register-dwd
# 4. Probe scopes live. Writes handoff/probe-report.json.
# REQUIRES test apparatus SA identity (see .env TEST_APPARATUS_*).
# In production: apparatus team runs this from their side.
bun run verify-scopes
# 5. Emit the handoff packet for the apparatus team.
bun run emit-handoff
# Or fold steps 3-5 into one:
bun run enable-end-to-endAfter step 5, ship handoff/handoff.json and handoff/handoff.md to the apparatus team. That is the entire handoff.
| Family | Topic | Carries | Source path |
|---|---|---|---|
mobile |
workspace-events-mobile |
Mobile device events (Android, iOS) | Workspace Events when covered; otherwise apparatus polls Reports API applicationName=mobile |
chrome |
workspace-events-chrome |
ChromeOS telemetry + events | Workspace Events when covered; otherwise apparatus polls Chrome Management telemetry/events |
alerts |
workspace-events-alerts |
Alert Center pre-correlated alerts | Apparatus polls Alert Center; topic carries any forwarded copies |
audit |
workspace-events-audit |
Workspace audit log export (login, token, admin, user_accounts, drive, …) | Cloud Logging sink (configured in this repo) → Pub/Sub |
| deadletter | workspace-events-deadletter |
Failed deliveries (any topic, 30 day retention) | Pub/Sub native |
The apparatus's subscriber SA, if provided in terraform.tfvars, already has roles/pubsub.subscriber on every family topic.
https://www.googleapis.com/auth/admin.directory.device.mobile.readonly
https://www.googleapis.com/auth/admin.directory.user.readonly
https://www.googleapis.com/auth/admin.directory.group.readonly
https://www.googleapis.com/auth/admin.directory.orgunit.readonly
https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly
https://www.googleapis.com/auth/admin.reports.audit.readonly
https://www.googleapis.com/auth/admin.reports.usage.readonly
https://www.googleapis.com/auth/apps.alerts
https://www.googleapis.com/auth/cloud-identity.devices.readonly
https://www.googleapis.com/auth/chrome.management.telemetry.readonly
https://www.googleapis.com/auth/chrome.management.policy.readonly
https://www.googleapis.com/auth/admin.directory.device.mobile.action
https://www.googleapis.com/auth/cloud-identity.devices
https://www.googleapis.com/auth/admin.directory.user.security
https://www.googleapis.com/auth/chrome.management.policy
| Endpoint | Method | Scope | Purpose |
|---|---|---|---|
admin.googleapis.com/admin/directory/v1/customer/{id}/devices/mobile |
GET | admin.directory.device.mobile.readonly |
Mobile inventory |
.../devices/mobile/{resourceId}/action |
POST | admin.directory.device.mobile.action |
block / wipe / approve / cancel |
cloudidentity.googleapis.com/v1/devices |
LIST | cloud-identity.devices.readonly |
Modern device inventory (work-profile, BYOD, ChromeOS) |
cloudidentity.googleapis.com/v1/devices/{name}:wipe |
POST | cloud-identity.devices |
Full wipe |
cloudidentity.googleapis.com/v1/devices/{name}/deviceUsers/{userId}:block |
POST | cloud-identity.devices |
Per-user-on-device block |
admin.googleapis.com/admin/reports/v1/activity/users/all/applications/{app} |
GET | admin.reports.audit.readonly |
Audit stream per app (mobile, login, token, admin, user_accounts, drive, chrome, context_aware_access, saml, groups_enterprise) |
alertcenter.googleapis.com/v1beta1/alerts |
LIST | apps.alerts |
Curated security alerts |
chromemanagement.googleapis.com/v1/customers/{id}/telemetry/devices |
LIST | chrome.management.telemetry.readonly |
ChromeOS device telemetry |
chromemanagement.googleapis.com/v1/customers/{id}/telemetry/events |
LIST | chrome.management.telemetry.readonly |
ChromeOS event stream |
chromepolicy.googleapis.com/v1/customers/{id}/policies/orgunits:batchModify |
POST | chrome.management.policy |
Chrome policy mutation |
admin.googleapis.com/admin/directory/v1/users/{userKey}/signOut |
POST | admin.directory.user.security |
Sign user out everywhere |
workspaceevents.googleapis.com/v1/subscriptions |
LIST | (apparatus reads via subscriber SA) | View / verify push subscriptions |
(Sample curl per endpoint generated into handoff/handoff.md by emit-handoff.)
Two files produced under handoff/:
handoff.json— machine-readable: customer ID, sub, scope sets, topic ARNs, endpoint catalog, probe-report statushandoff.md— human-readable: same content, plus a sample curl per endpoint pre-filled with the customer's tenant context
Ship both. The apparatus team needs nothing else from us.
| Scenario | admin.google.com clicks | Other UI |
|---|---|---|
| DWD-API available (happy path) | 0 | one gcloud auth login OAuth consent (Google sign-in, not admin.google.com) |
DWD-API returns 404 on your tenant (register-dwd fails fast and tells you) |
one ~2 min visit at setup time — Security → API controls → Domain-wide delegation → paste each client ID + scope CSV | same OAuth consent |
| Cloud Identity Premium not licensed | impossible to fix in API or admin console — license question (call Google) | — |
The script register-dwd checks DWD-API availability with its first request and prints the fallback URL inline if it 404s — no silent failure.
bun/bunxonly (nonpm/npxanywhere in scripts or package.json)- TypeScript only (no Python anywhere)
- Markdown only (no XML, no HTML for content)
- No web server runtime in this repo (no Hono, no Express)
- Nothing in this repo writes to admin.google.com; the only place that string appears is in this README's Residual UI section, where it must
gws-mdm-enablement/
├── package.json
├── bunfig.toml
├── tsconfig.json
├── .env.example
├── README.md # this file
├── handoff/ # gitignored — emit-handoff output lands here
├── scripts/
│ └── bootstrap-gcp.sh # one-shot project + billing setup
├── src/
│ ├── auth/
│ │ └── dwd.ts # DWD JWT mint + probeScopes
│ ├── lib/
│ │ ├── env.ts
│ │ └── logger.ts
│ └── scripts/
│ ├── register-dwd.ts # POST DWD-API to register scope sets
│ ├── verify-scopes.ts # mint + probe each scope live
│ └── emit-handoff.ts # write handoff/handoff.{json,md}
└── terraform/
├── versions.tf
├── main.tf # provider + locals
├── variables.tf
├── apis.tf # google_project_service for every API
├── pubsub.tf # topics + IAM + audit log sink
├── workspace_events.tf # subscriptions with apply-time probe
├── iam.tf
├── outputs.tf
├── probe-event-types.sh # apply-time eventTypes probe
└── terraform.tfvars.example
The Terraform module is parameterized. Adding a new customer is one new terraform.tfvars (different customer ID, super-admin, apparatus client IDs), one terraform init + apply in a fresh state, one bun run enable-end-to-end. No code changes.