From a1a151c2692701e5099680cd69ffc04904a08bb2 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Mon, 12 Jan 2026 17:30:06 -0800 Subject: [PATCH 1/3] feat: add documentation and automation for GCP project recreation - Add docs/GCP-RECREATION.md with step-by-step setup guide - Add scripts/setup-gcp.sh to automate API enablement and infrastructure deployment - Refactor AuthManager to support configurable CLIENT_ID and CLOUD_FUNCTION_URL via environment variables - Update README.md with deployment section --- README.md | 4 + docs/GCP-RECREATION.md | 81 ++++++++++++++ scripts/setup-gcp.sh | 131 +++++++++++++++++++++++ workspace-server/src/auth/AuthManager.ts | 11 +- workspace-server/src/utils/config.ts | 31 ++++++ 5 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 docs/GCP-RECREATION.md create mode 100755 scripts/setup-gcp.sh create mode 100644 workspace-server/src/utils/config.ts diff --git a/README.md b/README.md index 8744edb..78a64c7 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ Shows your schedule for today or a specified date. Searches your Google Drive for files matching the given query. +## Deployment + +If you want to host your own version of this extension's infrastructure, see the [GCP Recreation Guide](docs/GCP-RECREATION.md). + ## Resources - [Documentation](docs/index.md): Detailed documentation on all the available tools. diff --git a/docs/GCP-RECREATION.md b/docs/GCP-RECREATION.md new file mode 100644 index 0000000..e24b958 --- /dev/null +++ b/docs/GCP-RECREATION.md @@ -0,0 +1,81 @@ +# Recreating the GCP Project + +This guide provides step-by-step instructions to recreate the Google Cloud Platform (GCP) project and infrastructure required for the Google Workspace Extension. + +## Overview + +The extension uses a "Hybrid" OAuth flow for security: +1. **Local Client**: Requests authorization from the user. +2. **Cloud Function**: Acts as a secure proxy to exchange the authorization code for tokens. It holds the `CLIENT_SECRET` securely in Secret Manager. +3. **Secret Manager**: Stores the OAuth Client Secret. + +## Prerequisites + +- A Google Cloud Project with billing enabled. +- [Google Cloud CLI (gcloud)](https://cloud.google.com/sdk/docs/install) installed and authenticated. +- Node.js and npm installed. + +## Step 1: Automated Infrastructure Setup + +We provide a script to automate most of the GCP setup, including enabling APIs, creating secrets, and deploying the Cloud Function. + +1. Set your project ID: + ```bash + gcloud config set project YOUR_PROJECT_ID + ``` +2. Run the setup script: + ```bash + ./scripts/setup-gcp.sh + ``` +3. Follow the prompts to enter your **OAuth Client ID** and **Client Secret**. (If you don't have them yet, see Step 2 below first). + +## Step 2: Manual OAuth Configuration + +Some steps must be performed manually in the Google Cloud Console for security and policy reasons. + +### 1. Configure OAuth Consent Screen +1. Go to [APIs & Services > OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent). +2. Select **Internal** (if you are a Google Workspace user) or **External**. +3. Fill in the required App information. +4. **Scopes**: Add the following scopes: + - `https://www.googleapis.com/auth/documents` + - `https://www.googleapis.com/auth/drive` + - `https://www.googleapis.com/auth/calendar` + - `https://www.googleapis.com/auth/chat.spaces` + - `https://www.googleapis.com/auth/chat.messages` + - `https://www.googleapis.com/auth/chat.memberships` + - `https://www.googleapis.com/auth/userinfo.profile` + - `https://www.googleapis.com/auth/gmail.modify` + - `https://www.googleapis.com/auth/directory.readonly` + - `https://www.googleapis.com/auth/presentations.readonly` + - `https://www.googleapis.com/auth/spreadsheets.readonly` + +### 2. Create OAuth 2.0 Client ID +1. Go to [APIs & Services > Credentials](https://console.cloud.google.com/apis/credentials). +2. Click **Create Credentials > OAuth client ID**. +3. Select **Web application** as the Application type. +4. Name it (e.g., "Workspace Extension"). +5. **Authorized redirect URIs**: Add the URL of your deployed Cloud Function (provided by the setup script). + - Format: `https://[REGION]-[PROJECT_ID].cloudfunctions.net/workspace-oauth-handler` +6. Click **Create**. +7. Copy the **Client ID** and **Client Secret**. + +## Step 3: Local Configuration + +After running the script and configuring the OAuth client, you need to tell the local extension where to find your infrastructure. + +Set the following environment variables in your shell (e.g., in `.zshrc` or `.bashrc`): + +```bash +export WORKSPACE_CLIENT_ID="your-client-id" +export WORKSPACE_CLOUD_FUNCTION_URL="https://your-cloud-function-url" +``` + +Alternatively, you can modify the `DEFAULT_CONFIG` in `workspace-server/src/utils/config.ts`. + +## Why a Cloud Function? + +The extension uses a Cloud Function to protect your `CLIENT_SECRET`. +- If the `CLIENT_SECRET` were included in the local extension code, anyone with access to the extension could steal it. +- By using a Cloud Function, the secret stays in your GCP project and is only used server-side during the token exchange. +- The local client only ever sees the resulting tokens, never the secret. diff --git a/scripts/setup-gcp.sh b/scripts/setup-gcp.sh new file mode 100755 index 0000000..fcc1959 --- /dev/null +++ b/scripts/setup-gcp.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +# GCP Setup Script for Google Workspace Extension +# This script enables necessary APIs and helps set up Secret Manager and Cloud Functions. + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}Starting Google Cloud Platform setup...${NC}" + +# Check if gcloud is installed +if ! command -v gcloud &> /dev/null; then + echo -e "${RED}Error: gcloud CLI is not installed. Please install it first.${NC}" + exit 1 +fi + +# Get current project ID +PROJECT_ID=$(gcloud config get-value project) +if [ -z "$PROJECT_ID" ]; then + echo -e "${RED}Error: No Google Cloud project is currently set.${NC}" + echo "Please run: gcloud config set project [PROJECT_ID]" + exit 1 +fi + +echo -e "Using project: ${GREEN}$PROJECT_ID${NC}" + +# 1. Enable Required APIs +echo -e "\n${YELLOW}Step 1: Enabling Required APIs...${NC}" +APIS=( + "drive.googleapis.com" + "docs.googleapis.com" + "calendar-json.googleapis.com" + "chat.googleapis.com" + "gmail.googleapis.com" + "people.googleapis.com" + "slides.googleapis.com" + "sheets.googleapis.com" + "admin.googleapis.com" + "secretmanager.googleapis.com" + "cloudfunctions.googleapis.com" + "cloudbuild.googleapis.com" +) + +for api in "${APIS[@]}"; do + echo "Enabling $api..." + gcloud services enable "$api" +done + +echo -e "${GREEN}APIs enabled successfully.${NC}" + +# 2. Setup Secret Manager +echo -e "\n${YELLOW}Step 2: Setting up Secret Manager...${NC}" +SECRET_ID="workspace-oauth-client-secret" + +if gcloud secrets describe "$SECRET_ID" &> /dev/null; then + echo "Secret $SECRET_ID already exists." +else + echo "Creating secret $SECRET_ID..." + gcloud secrets create "$SECRET_ID" --replication-policy=\"automatic\" +fi + +echo -e "${YELLOW}Please enter your OAuth 2.0 Client Secret (from Google Cloud Console):${NC}" +read -s CLIENT_SECRET +echo "$CLIENT_SECRET" | gcloud secrets versions add "$SECRET_ID" --data-file=- + +echo -e "${GREEN}Secret stored successfully.${NC}" + +# 3. Deploy Cloud Function +echo -e "\n${YELLOW}Step 3: Deploying Cloud Function...${NC}" +echo -e "${YELLOW}Please enter the OAuth 2.0 Client ID:${NC}" +read CLIENT_ID + +REGION="us-central1" # You can change this +FUNCTION_NAME="workspace-oauth-handler" + +# We need the function URL before deployment to set REDIRECT_URI, +# but we can also use a placeholder and update it after. +# Or better, construct it if we know the naming convention. +# For 2nd gen functions, it's https://[REGION]-[PROJECT_ID].cloudfunctions.net/[FUNCTION_NAME] +# But let's just deploy it and then update if needed. + +echo "Deploying Cloud Function..." +gcloud functions deploy "$FUNCTION_NAME" \ + --gen2 \ + --runtime=nodejs20 \ + --region="$REGION" \ + --source="./cloud_function" \ + --entry-point=oauthHandler \ + --trigger-http \ + --allow-unauthenticated \ + --set-env-vars "CLIENT_ID=$CLIENT_ID,SECRET_NAME=projects/$PROJECT_ID/secrets/$SECRET_ID/versions/latest" + +# Get the URL of the deployed function +FUNCTION_URL=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.uri)') + +echo -e "${GREEN}Cloud Function deployed at: $FUNCTION_URL${NC}" + +# Update REDIRECT_URI env var now that we have the URL +echo "Updating REDIRECT_URI environment variable..." +gcloud functions deploy "$FUNCTION_NAME" \ + --gen2 \ + --region="$REGION" \ + --update-env-vars "REDIRECT_URI=$FUNCTION_URL" + +# 4. Grant Permissions +echo -e "\n${YELLOW}Step 4: Granting Secret Manager Access to Cloud Function...${NC}" +# Get the service account used by the Cloud Function +SERVICE_ACCOUNT=$(gcloud functions describe "$FUNCTION_NAME" --region="$REGION" --format='value(serviceConfig.serviceAccountEmail)') + +gcloud secrets add-iam-policy-binding "$SECRET_ID" \ + --member="serviceAccount:$SERVICE_ACCOUNT" \ + --role="roles/secretmanager.secretAccessor" + +echo -e "${GREEN}Permissions granted successfully.${NC}" + +echo -e "\n${GREEN}GCP Setup Complete!${NC}" +echo -e "---------------------------------------------------" +echo -e "${YELLOW}Next Steps:${NC}" +echo "1. Go to Google Cloud Console > APIs & Services > Credentials." +echo "2. Edit your OAuth 2.0 Client ID." +echo "3. Add the following to 'Authorized redirect URIs':" +echo -e " ${GREEN}$FUNCTION_URL${NC}" +echo "4. Set the following environment variables in your local environment:" +echo -e " ${GREEN}export WORKSPACE_CLIENT_ID=\"$CLIENT_ID\"${NC}" +echo -e " ${GREEN}export WORKSPACE_CLOUD_FUNCTION_URL=\"$FUNCTION_URL\"${NC}" +echo -e "---------------------------------------------------" diff --git a/workspace-server/src/auth/AuthManager.ts b/workspace-server/src/auth/AuthManager.ts index acd7f76..b50f00e 100644 --- a/workspace-server/src/auth/AuthManager.ts +++ b/workspace-server/src/auth/AuthManager.ts @@ -13,10 +13,11 @@ import { logToFile } from '../utils/logger'; import open from '../utils/open-wrapper'; import { shouldLaunchBrowser } from '../utils/secure-browser-launcher'; import { OAuthCredentialStorage } from './token-storage/oauth-credential-storage'; +import { loadConfig } from '../utils/config'; -// The Client ID for the OAuth flow. -// The secret is handled by the cloud function, not in the client. -const CLIENT_ID = '338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com'; +const config = loadConfig(); +const CLIENT_ID = config.clientId; +const CLOUD_FUNCTION_URL = config.cloudFunctionUrl; const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minutes /** @@ -201,7 +202,7 @@ export class AuthManager { // Call the cloud function refresh endpoint // The cloud function has the client secret needed for token refresh - const response = await fetch('https://google-workspace-extension.geminicli.com/refreshToken', { + const response = await fetch(`${CLOUD_FUNCTION_URL}/refreshToken`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -287,7 +288,7 @@ export class AuthManager { const state = Buffer.from(JSON.stringify(statePayload)).toString('base64'); // The redirect URI for Google's auth server is the cloud function - const cloudFunctionRedirectUri = 'https://google-workspace-extension.geminicli.com'; + const cloudFunctionRedirectUri = CLOUD_FUNCTION_URL; const authUrl = client.generateAuthUrl({ redirect_uri: cloudFunctionRedirectUri, // Tell Google to go to the cloud function diff --git a/workspace-server/src/utils/config.ts b/workspace-server/src/utils/config.ts new file mode 100644 index 0000000..caae9e6 --- /dev/null +++ b/workspace-server/src/utils/config.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { logToFile } from './logger'; + +export interface WorkspaceConfig { + clientId: string; + cloudFunctionUrl: string; +} + +const DEFAULT_CONFIG: WorkspaceConfig = { + clientId: '338689075775-o75k922vn5fdl18qergr96rp8g63e4d7.apps.googleusercontent.com', + cloudFunctionUrl: 'https://google-workspace-extension.geminicli.com' +}; + +/** + * Loads the configuration. Currently uses defaults, but can be extended + * to read from environment variables or a configuration file. + */ +export function loadConfig(): WorkspaceConfig { + const config: WorkspaceConfig = { + clientId: process.env['WORKSPACE_CLIENT_ID'] || DEFAULT_CONFIG.clientId, + cloudFunctionUrl: process.env['WORKSPACE_CLOUD_FUNCTION_URL'] || DEFAULT_CONFIG.cloudFunctionUrl + }; + + logToFile(`Loaded config: clientId=${config.clientId}, cloudFunctionUrl=${config.cloudFunctionUrl}`); + return config; +} From 17c991ec7f253925fa9abfc7b75301d37132fe9e Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Tue, 20 Jan 2026 16:48:54 -0800 Subject: [PATCH 2/3] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- scripts/setup-gcp.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/scripts/setup-gcp.sh b/scripts/setup-gcp.sh index fcc1959..4fe66d6 100755 --- a/scripts/setup-gcp.sh +++ b/scripts/setup-gcp.sh @@ -77,12 +77,18 @@ read CLIENT_ID REGION="us-central1" # You can change this FUNCTION_NAME="workspace-oauth-handler" +FUNCTION_URL="https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}" -# We need the function URL before deployment to set REDIRECT_URI, -# but we can also use a placeholder and update it after. -# Or better, construct it if we know the naming convention. -# For 2nd gen functions, it's https://[REGION]-[PROJECT_ID].cloudfunctions.net/[FUNCTION_NAME] -# But let's just deploy it and then update if needed. +echo "Deploying Cloud Function..." +gcloud functions deploy "$FUNCTION_NAME" \ + --gen2 \ + --runtime=nodejs20 \ + --region="$REGION" \ + --source="./cloud_function" \ + --entry-point=oauthHandler \ + --trigger-http \ + --allow-unauthenticated \ + --set-env-vars "CLIENT_ID=$CLIENT_ID,SECRET_NAME=projects/$PROJECT_ID/secrets/$SECRET_ID/versions/latest,REDIRECT_URI=$FUNCTION_URL" echo "Deploying Cloud Function..." gcloud functions deploy "$FUNCTION_NAME" \ From fb60b6ef2278bfbf469b258acc0bf0b29fc374e4 Mon Sep 17 00:00:00 2001 From: Allen Hutchison Date: Tue, 20 Jan 2026 16:49:04 -0800 Subject: [PATCH 3/3] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- scripts/setup-gcp.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/setup-gcp.sh b/scripts/setup-gcp.sh index 4fe66d6..0df56bb 100755 --- a/scripts/setup-gcp.sh +++ b/scripts/setup-gcp.sh @@ -75,7 +75,12 @@ echo -e "\n${YELLOW}Step 3: Deploying Cloud Function...${NC}" echo -e "${YELLOW}Please enter the OAuth 2.0 Client ID:${NC}" read CLIENT_ID -REGION="us-central1" # You can change this +echo -e "${YELLOW}Please enter the GCP region for your Cloud Function (e.g., us-central1):${NC}" +read REGION +if [ -z "$REGION" ]; then + REGION="us-central1" + echo -e "${YELLOW}No region entered, defaulting to $REGION.${NC}" +fi FUNCTION_NAME="workspace-oauth-handler" FUNCTION_URL="https://${REGION}-${PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}"