diff --git a/docs/brand-manifest-policy.md b/docs/brand-manifest-policy.md new file mode 100644 index 000000000..7692ef0b6 --- /dev/null +++ b/docs/brand-manifest-policy.md @@ -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 diff --git a/scripts/update_brand_manifest_policy.py b/scripts/update_brand_manifest_policy.py new file mode 100755 index 000000000..e01fce161 --- /dev/null +++ b/scripts/update_brand_manifest_policy.py @@ -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() diff --git a/src/admin/blueprints/settings.py b/src/admin/blueprints/settings.py index 2fbffb0ef..6b17e70c6 100644 --- a/src/admin/blueprints/settings.py +++ b/src/admin/blueprints/settings.py @@ -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"] diff --git a/templates/tenant_settings.html b/templates/tenant_settings.html index 486c3383b..de2ddc5e2 100644 --- a/templates/tenant_settings.html +++ b/templates/tenant_settings.html @@ -975,6 +975,56 @@
+ Control when advertisers must provide brand information (brand_manifest) when discovering products or creating campaigns.
+
š Require Authentication (Recommended for most publishers)
+brand_manifest is optionalš Require Authentication + Brand Manifest (Strictest)
+brand_manifest is requiredš Public (Most open)
+brand_manifest optional