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
120 changes: 120 additions & 0 deletions docs/brand-manifest-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Brand Manifest Policy Configuration

## Overview

The brand manifest policy controls when a brand_manifest is required in `get_products` and `create_media_buy` requests. This is configured per-tenant in the database.

## Policy Options

### 1. `require_auth` (Default - Recommended)
- **Authentication**: Required
- **Brand Manifest**: Optional
- **Pricing**: Shown to authenticated users
- **Use Case**: Standard B2B model where advertisers must authenticate but don't need to provide brand context for every request
- **Testing**: Best for testing and development environments

### 2. `require_brand` (Strictest)
- **Authentication**: Required
- **Brand Manifest**: Required
- **Pricing**: Shown only when brand manifest provided
- **Use Case**: Publishers offering bespoke/custom products that require brand context
- **Testing**: More restrictive, harder for testing

### 3. `public` (Most Open)
- **Authentication**: Not required
- **Brand Manifest**: Optional
- **Pricing**: Hidden (only generic product info shown)
- **Use Case**: Public product catalog browsing
- **Testing**: Good for anonymous browsing, but pricing not shown

## Configuration

### Via Admin UI

1. Log into the admin UI at `https://admin.your-domain.com`
2. Navigate to **Tenant Settings** → **Policies & Workflows**
3. Find the **Brand Manifest Policy** dropdown
4. Select `require_auth` (recommended for testing)
5. Save changes

### Via Database (Direct SQL)

```sql
-- Update a specific tenant
UPDATE tenants
SET brand_manifest_policy = 'require_auth'
WHERE tenant_id = 'your-tenant-id';

-- Update all tenants
UPDATE tenants
SET brand_manifest_policy = 'require_auth';
```

### Via Python Script

Run the provided script to update all tenants:

```bash
# From project root
uv run python scripts/update_brand_manifest_policy.py
```

This will update all tenants to use `require_auth` policy.

### Via Fly.io (Production)

If deploying on Fly.io, you can run the script in production:

```bash
# SSH into production
fly ssh console --app adcp-sales-agent

# Run the update script
python scripts/update_brand_manifest_policy.py
```

Or update directly via database:

```bash
# Connect to production database
fly postgres connect --app adcp-sales-agent-db

# Run SQL update
UPDATE tenants SET brand_manifest_policy = 'require_auth';
```

## Testing

After updating the policy, test with:

```bash
# Test get_products without brand_manifest (should work with require_auth)
uv run pytest tests/unit/test_brand_manifest_optional.py -v

# Test full integration
uv run pytest tests/integration/ -k "get_products" -v
```

## Migration History

- **PR #663**: Added `brand_manifest_policy` system with three policy options
- **Migration 6f05f4179c33**: Added column with default `require_brand`
- **Migration 378299ad502f**: Changed default to `require_auth` for new tenants

## Implementation Details

The policy is enforced in `src/core/tools/products.py` in the `_get_products_impl()` function:

```python
# Policy enforcement
if policy == "require_brand" and not brand_manifest:
raise ToolError("Brand manifest required by tenant policy")
elif policy == "require_auth" and not principal_id:
raise ToolError("Authentication required by tenant policy")
# public policy allows all requests
```

Pricing visibility logic:
- `public`: No pricing shown
- `require_auth`: Pricing shown if authenticated
- `require_brand`: Pricing shown if authenticated AND brand_manifest provided
63 changes: 63 additions & 0 deletions scripts/update_brand_manifest_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Update brand_manifest_policy for all tenants to require_auth.

This makes brand_manifest optional for all tenants, which is useful for testing
and matches the B2B model where advertisers need auth but not necessarily a
brand manifest for every request.
"""

import sys
from pathlib import Path

# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))

from sqlalchemy import select

from src.core.database.database_session import get_db_session
from src.core.database.models import Tenant


def update_all_tenants_to_require_auth():
"""Update all tenants to use require_auth policy."""
with get_db_session() as session:
# Get all tenants
stmt = select(Tenant)
tenants = session.scalars(stmt).all()

print(f"Found {len(tenants)} tenants")

updated_count = 0
for tenant in tenants:
old_policy = tenant.brand_manifest_policy
if old_policy != "require_auth":
tenant.brand_manifest_policy = "require_auth"
print(f" Updated tenant '{tenant.name}' ({tenant.tenant_id}): {old_policy} -> require_auth")
updated_count += 1
else:
print(f" Tenant '{tenant.name}' ({tenant.tenant_id}): already set to require_auth")

if updated_count > 0:
session.commit()
print(f"\n✅ Successfully updated {updated_count} tenant(s) to require_auth policy")
else:
print("\n✅ All tenants already using require_auth policy")

return updated_count


def main():
"""Main entry point."""
print("Updating brand_manifest_policy for all tenants...\n")
try:
update_all_tenants_to_require_auth()
except Exception as e:
print(f"\n❌ Error updating tenants: {e}")
import traceback

traceback.print_exc()
sys.exit(1)


if __name__ == "__main__":
main()
14 changes: 14 additions & 0 deletions src/admin/blueprints/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,20 @@ def update_business_rules(tenant_id):
flash("At least one measurement provider is required.", "error")
return redirect(url_for("tenants.tenant_settings", tenant_id=tenant_id, section="business-rules"))

# Update brand manifest policy
if "brand_manifest_policy" in data:
policy_value = data.get("brand_manifest_policy", "").strip()
if policy_value in ["public", "require_auth", "require_brand"]:
tenant.brand_manifest_policy = policy_value
else:
if request.is_json:
return (
jsonify({"success": False, "error": f"Invalid brand_manifest_policy: {policy_value}"}),
400,
)
flash(f"Invalid brand manifest policy: {policy_value}", "error")
return redirect(url_for("tenants.tenant_settings", tenant_id=tenant_id, section="business-rules"))

# Update approval workflow
if "human_review_required" in data:
manual_approval_value = data.get("human_review_required") in [True, "true", "on", 1, "1"]
Expand Down
50 changes: 50 additions & 0 deletions templates/tenant_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,56 @@ <h3>Budget Controls</h3>
</div>
</div>

<!-- Brand Manifest Policy -->
<div class="form-section">
<h3>Brand Manifest Policy</h3>
<p style="color: #6b7280; font-size: 0.875rem; margin-bottom: 1rem;">
Control when advertisers must provide brand information (<code>brand_manifest</code>) when discovering products or creating campaigns.
</p>
<div class="form-group">
<label for="brand_manifest_policy">Brand Context Requirement</label>
<select id="brand_manifest_policy" name="brand_manifest_policy" class="form-control">
<option value="require_auth" {% if tenant.brand_manifest_policy == 'require_auth' or not tenant.brand_manifest_policy %}selected{% endif %}>
🔐 Require Authentication (brand_manifest optional)
</option>
<option value="require_brand" {% if tenant.brand_manifest_policy == 'require_brand' %}selected{% endif %}>
🔒 Require Authentication + Brand Manifest (strictest)
</option>
<option value="public" {% if tenant.brand_manifest_policy == 'public' %}selected{% endif %}>
🌐 Public (no authentication, no pricing shown)
</option>
</select>
<details style="margin-top: 0.5rem;">
<summary style="cursor: pointer; color: #666; font-size: 0.875rem;">About these options</summary>
<div style="margin-top: 0.5rem; padding: 0.75rem; background: #f8f9fa; border-radius: 4px; font-size: 0.875rem;">
<p style="margin: 0 0 0.75rem;"><strong>🔐 Require Authentication</strong> (Recommended for most publishers)</p>
<ul style="margin: 0 0 1rem 1.5rem;">
<li>Advertisers must authenticate to see products</li>
<li><code>brand_manifest</code> is <strong>optional</strong></li>
<li>Pricing shown to authenticated users</li>
<li>Best for: Standard B2B sales, testing environments</li>
</ul>

<p style="margin: 0 0 0.75rem;"><strong>🔒 Require Authentication + Brand Manifest</strong> (Strictest)</p>
<ul style="margin: 0 0 1rem 1.5rem;">
<li>Advertisers must authenticate AND provide brand context</li>
<li><code>brand_manifest</code> is <strong>required</strong></li>
<li>Pricing shown only with brand context</li>
<li>Best for: Bespoke products requiring brand-specific customization</li>
</ul>

<p style="margin: 0 0 0.75rem;"><strong>🌐 Public</strong> (Most open)</p>
<ul style="margin: 0 0 0; 1.5rem;">
<li>No authentication required</li>
<li><code>brand_manifest</code> optional</li>
<li>Pricing <strong>hidden</strong> (generic catalog only)</li>
<li>Best for: Public product browsing, lead generation</li>
</ul>
</div>
</details>
</div>
</div>

<!-- Naming Conventions -->
<div class="form-section">
<h3>Naming Conventions</h3>
Expand Down