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
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,16 @@ DATABASE_URL=postgresql://adcp_user:secure_password_change_me@postgres:5432/adcp
# ============================================
# Only needed if using real ad server adapters

# Google Ad Manager
# Google Ad Manager OAuth (for GAM Admin UI configuration)
GAM_OAUTH_CLIENT_ID=your-gam-oauth-client-id.apps.googleusercontent.com
GAM_OAUTH_CLIENT_SECRET=your-gam-oauth-client-secret

# Google Cloud Platform (for auto-provisioning service accounts)
# Required if you want to use the "Create Service Account" feature in Admin UI
# This is the GCP project ID where service accounts will be created
# GCP_PROJECT_ID=your-gcp-project-id

# Legacy: Manual GAM configuration (not needed if using Admin UI)
# GAM_NETWORK_CODE=123456789
# GAM_SERVICE_ACCOUNT_JSON='{"type":"service_account"...}'

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ gam_ad_units.json
client_secret*.json
credentials*.json

# GCP Service Account Keys
*-key.json
adcp-manager-key.json

# Build artifacts
*.egg-info/
*.egg-info
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""add_gam_service_account_email_to_adapter_config

Revision ID: 661c474053fa
Revises: 1a7693edad5d
Create Date: 2025-10-19 04:41:52.439532

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "661c474053fa"
down_revision: str | Sequence[str] | None = "1a7693edad5d"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# Only add service account email - GCP project ID is environment config, not per-tenant
op.add_column("adapter_config", sa.Column("gam_service_account_email", sa.String(length=255), nullable=True))


def downgrade() -> None:
"""Downgrade schema."""
op.drop_column("adapter_config", "gam_service_account_email")
73 changes: 72 additions & 1 deletion docs/gam-service-account-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ The AdCP Sales Agent supports two authentication methods for Google Ad Manager:
1. **OAuth (Refresh Token)** - User-based authentication requiring manual token refresh
2. **Service Account** - Automated authentication using Google Cloud service accounts (recommended for production)

## 🚀 New: Automatic Service Account Provisioning (Recommended for Partners)

**For partner integrations, we now create and manage service accounts for you automatically!**

Instead of partners sending us their service account JSON credentials, we:
1. Create a service account in our GCP project
2. Provide you with the service account email
3. You add this email as a user in your GAM with Trafficker role
4. We handle all credential management securely

## Service Account Benefits

- ✅ **No manual refresh**: Service accounts don't expire like OAuth tokens
Expand All @@ -16,6 +26,23 @@ The AdCP Sales Agent supports two authentication methods for Google Ad Manager:
- ✅ **Multi-tenant**: Each tenant can have their own service account
- ✅ **Cloud-native**: Credentials stored encrypted in database, no file management

## Security Model

Service account authentication uses **two-factor control**:

1. **Private Key** (we control): Stored encrypted in our database, used to cryptographically sign API requests
2. **GAM User List** (partner controls): Partner must explicitly add the service account email to their GAM

**Both are required for access:**
- Just knowing the email is NOT enough - API calls must be signed with the private key
- Just having the private key is NOT enough - partner must grant permissions in their GAM

**Partner maintains control:**
- Can revoke access anytime by removing email from GAM
- Controls what permissions to grant (Trafficker, Salesperson, etc.)
- Can restrict to specific advertisers via GAM teams
- All activity appears in their GAM audit logs

## Required Service Account Roles

### Recommended: "Trafficker" Role
Expand Down Expand Up @@ -78,7 +105,51 @@ Network:
- Read (to get timezone and network info)
```

## Setup Instructions
## Prerequisites for Automatic Service Account Creation

The automatic service account creation feature requires:

1. **GCP Project Configuration**: The `GCP_PROJECT_ID` environment variable must be set to your Google Cloud Project ID
2. **Service Account Permissions**: The application's default credentials must have permissions to create service accounts and keys in that project (requires `roles/iam.serviceAccountAdmin` or `roles/iam.serviceAccountKeyAdmin`)

If you haven't set these up yet, see your system administrator or cloud platform documentation.

## Setup Instructions (New Automatic Flow - Recommended)

### Step 1: Request Service Account Creation

1. Log into the Admin UI (http://localhost:8001 or your production URL)
2. Navigate to **Tenant Settings** → **Ad Server**
3. Select **Google Ad Manager** as your adapter
4. Scroll to the **Service Account Integration** section
5. Click **🔑 Create Service Account**
6. Wait a few seconds while we create the service account in our GCP project
7. Copy the service account email that appears

### Step 2: Add Service Account to Your GAM

1. Log into your [Google Ad Manager](https://admanager.google.com/) account
2. Navigate to **Admin** → **Access & authorization** → **Users**
3. Click **New user**
4. Paste the service account email from Step 1
5. Assign role: **Trafficker** (recommended)
6. Under **Teams**, select specific advertisers (optional but recommended for security)
7. Click **Save**

### Step 3: Test Connection

1. Return to the Admin UI settings page
2. Click **Test Connection** button
3. If successful, you're done! If not, verify:
- Service account email was added correctly in GAM
- Trafficker role was assigned
- You clicked Save in GAM

---

## Alternative: Manual Service Account Setup (Legacy)

**Note**: The automatic flow above is recommended. Use this only if you need to create your own service account.

### Step 1: Create Service Account in Google Cloud Console

Expand Down
244 changes: 244 additions & 0 deletions docs/gcp-service-account-provisioning-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# GCP Service Account Provisioning - Deployment Setup

## Overview

This guide explains how to set up the automatic service account provisioning feature for production deployment on Fly.io (or any cloud platform).

## Architecture

```
Your Sales Agent (Fly.io)
Uses "Management Service Account"
Creates "Partner Service Accounts" in your GCP project
Partners add these to their GAM
```

## Prerequisites

1. A Google Cloud Platform (GCP) project
2. Access to create service accounts in that project
3. Fly.io CLI installed and authenticated

## Step-by-Step Setup

### Step 1: Create a GCP Project (if needed)

```bash
# Create a new GCP project (or use existing)
gcloud projects create adcp-sales-agent-prod --name="AdCP Sales Agent Production"

# Set as default project
gcloud config set project adcp-sales-agent-prod
```

### Step 2: Create the "Management" Service Account

This is the service account that your application will run as to create other service accounts:

```bash
# Create the management service account
gcloud iam service-accounts create adcp-manager \
--display-name="AdCP Service Account Manager" \
--description="Service account used by AdCP Sales Agent to create partner service accounts"

# Get the email
export SA_EMAIL="adcp-manager@adcp-sales-agent-prod.iam.gserviceaccount.com"
echo "Management Service Account: $SA_EMAIL"
```

### Step 3: Grant IAM Permissions

The management service account needs permission to create other service accounts:

```bash
# Grant Service Account Admin role (to create service accounts)
gcloud projects add-iam-policy-binding adcp-sales-agent-prod \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/iam.serviceAccountAdmin"

# Grant Service Account Key Admin role (to create service account keys)
gcloud projects add-iam-policy-binding adcp-sales-agent-prod \
--member="serviceAccount:$SA_EMAIL" \
--role="roles/iam.serviceAccountKeyAdmin"
```

**Why these roles?**
- `roles/iam.serviceAccountAdmin` - Allows creating and managing service accounts
- `roles/iam.serviceAccountKeyAdmin` - Allows creating service account keys

### Step 4: Generate Service Account Key

```bash
# Create a JSON key for the management service account
gcloud iam service-accounts keys create ~/adcp-manager-key.json \
--iam-account=$SA_EMAIL

# The key is saved to ~/adcp-manager-key.json
```

**⚠️ IMPORTANT:** Keep this key secure! It has permission to create service accounts in your project.

### Step 5: Configure Fly.io

#### Set the GCP Project ID (in fly.toml)

Edit `fly.toml` and uncomment/set:

```toml
[env]
# Other env vars...
GCP_PROJECT_ID = "adcp-sales-agent-prod" # Your actual project ID
```

Commit and push this change.

#### Set the Service Account Key (as Fly secret)

```bash
# Read the key file and set as Fly secret
fly secrets set GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat ~/adcp-manager-key.json)" \
--app adcp-sales-agent

# Verify it was set (you won't see the value, just the name)
fly secrets list --app adcp-sales-agent
```

### Step 6: Deploy

```bash
# Deploy the application with the new configuration
fly deploy --app adcp-sales-agent
```

### Step 7: Verify Setup

Check the application logs to ensure credentials are loading:

```bash
fly logs --app adcp-sales-agent
```

Look for:
```
GCP credentials loaded from GOOGLE_APPLICATION_CREDENTIALS_JSON
```

### Step 8: Test the Feature

1. Log into Admin UI: https://sales-agent.scope3.com/
2. Navigate to **Tenant Settings** → **Ad Server**
3. Select **Google Ad Manager**
4. Scroll to **Service Account Integration**
5. Click **🔑 Create Service Account**
6. You should see a service account email created!

## Verification Checklist

- [ ] GCP project created/identified
- [ ] Management service account created
- [ ] IAM roles granted (serviceAccountAdmin + serviceAccountKeyAdmin)
- [ ] Service account key generated
- [ ] `GCP_PROJECT_ID` set in fly.toml
- [ ] `GOOGLE_APPLICATION_CREDENTIALS_JSON` set as Fly secret
- [ ] Application deployed
- [ ] Logs show credentials loaded
- [ ] Test service account creation works

## Troubleshooting

### Error: "GCP_PROJECT_ID not configured"
**Cause:** Environment variable not set in fly.toml

**Fix:**
```toml
[env]
GCP_PROJECT_ID = "your-project-id"
```

### Error: "Permission denied" or "IAM API not enabled"
**Cause:** Missing IAM permissions or API not enabled

**Fix:**
```bash
# Enable IAM API
gcloud services enable iam.googleapis.com --project=adcp-sales-agent-prod

# Re-grant permissions
gcloud projects add-iam-policy-binding adcp-sales-agent-prod \
--member="serviceAccount:adcp-manager@adcp-sales-agent-prod.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountAdmin"
```

### Error: "No explicit GCP credentials provided"
**Cause:** GOOGLE_APPLICATION_CREDENTIALS_JSON secret not set

**Fix:**
```bash
fly secrets set GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat ~/adcp-manager-key.json)" \
--app adcp-sales-agent
```

### Verify What Service Account Is Being Used

```bash
# In the application, log the credentials
# The service account email will appear in logs when IAMClient is initialized

fly logs --app adcp-sales-agent | grep "service_account"
```

## Security Best Practices

1. **Rotate Keys Regularly**: Create new keys every 90 days
```bash
# Create new key
gcloud iam service-accounts keys create ~/new-key.json --iam-account=$SA_EMAIL

# Update Fly secret
fly secrets set GOOGLE_APPLICATION_CREDENTIALS_JSON="$(cat ~/new-key.json)"

# Delete old key (get key ID from console)
gcloud iam service-accounts keys delete KEY_ID --iam-account=$SA_EMAIL
```

2. **Least Privilege**: Only grant the minimum required roles

3. **Monitor Usage**: Check GCP IAM audit logs for service account creation activity

4. **Separate Projects**: Consider using a dedicated GCP project for service account creation

## Cost Considerations

- Service account creation is **free**
- Service account keys are **free**
- IAM API calls are **free** (within quota)
- No ongoing costs for this feature

## Alternative: Using Workload Identity (Advanced)

If you're running on GCP (not Fly.io), you can use Workload Identity instead of service account keys:

```bash
# This is more secure but only works on GCP environments
# Not applicable for Fly.io deployments
```

## Support

If you encounter issues:
1. Check Fly.io logs: `fly logs --app adcp-sales-agent`
2. Verify IAM permissions in GCP Console
3. Ensure IAM API is enabled in your project
4. Check that the service account key JSON is valid

## Summary

Once configured, the flow is:
1. Your app runs as the "management" service account (credentials in Fly secret)
2. When a partner clicks "Create Service Account" in Admin UI
3. Your app creates a new service account: `adcp-sales-tenant123@project.iam.gserviceaccount.com`
4. Partner adds that email to their GAM
5. Done! No credential sharing needed.
Loading