GT AI OS Community Edition v2.0.33
Security hardening release addressing CodeQL and Dependabot alerts: - Fix stack trace exposure in error responses - Add SSRF protection with DNS resolution checking - Implement proper URL hostname validation (replaces substring matching) - Add centralized path sanitization to prevent path traversal - Fix ReDoS vulnerability in email validation regex - Improve HTML sanitization in validation utilities - Fix capability wildcard matching in auth utilities - Update glob dependency to address CVE - Add CodeQL suppression comments for verified false positives 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
44
scripts/postgresql/admin-entrypoint-wrapper.sh
Executable file
44
scripts/postgresql/admin-entrypoint-wrapper.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
# GT 2.0 Admin PostgreSQL Custom Entrypoint
|
||||
# Ensures postgres user password is synced from environment variable on every startup
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 GT 2.0 Admin PostgreSQL Startup..."
|
||||
|
||||
# Function to update postgres user password from environment variable
|
||||
update_postgres_password() {
|
||||
echo "🔐 Syncing postgres user password from environment..."
|
||||
|
||||
# Update postgres superuser password if POSTGRES_PASSWORD is set
|
||||
if [ -n "$POSTGRES_PASSWORD" ]; then
|
||||
psql -U postgres -d gt2_admin -c "ALTER USER postgres WITH PASSWORD '$POSTGRES_PASSWORD';" >/dev/null 2>&1 && \
|
||||
echo "✅ Updated postgres user password" || \
|
||||
echo "⚠️ Could not update postgres password (database may not be ready yet)"
|
||||
fi
|
||||
|
||||
# Also update gt2_admin if it exists and ADMIN_USER_PASSWORD is set
|
||||
if [ -n "$ADMIN_USER_PASSWORD" ]; then
|
||||
psql -U postgres -d gt2_admin -c "ALTER USER gt2_admin WITH PASSWORD '$ADMIN_USER_PASSWORD';" >/dev/null 2>&1 && \
|
||||
echo "✅ Updated gt2_admin user password" || \
|
||||
echo "⚠️ Could not update gt2_admin password (user may not exist yet)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to configure after PostgreSQL starts
|
||||
configure_after_start() {
|
||||
sleep 5 # Wait for PostgreSQL to fully start
|
||||
|
||||
# Update passwords from environment variables if PostgreSQL is running
|
||||
if pg_isready -U postgres >/dev/null 2>&1; then
|
||||
update_postgres_password
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure after PostgreSQL starts (in background)
|
||||
configure_after_start &
|
||||
|
||||
echo "🚀 Starting Admin PostgreSQL..."
|
||||
|
||||
# Call the original PostgreSQL entrypoint
|
||||
exec docker-entrypoint.sh "$@"
|
||||
26
scripts/postgresql/admin-extensions.sql
Normal file
26
scripts/postgresql/admin-extensions.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- GT 2.0 Admin Cluster Extensions Initialization
|
||||
-- Installs basic extensions for admin/control panel databases
|
||||
-- Does NOT include PGVector (not available in postgres:15-alpine image)
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- NOTE: Removed \c gt2_admin - Docker entrypoint runs this script
|
||||
-- against POSTGRES_DB (gt2_admin) automatically.
|
||||
|
||||
-- Basic extensions for admin database
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_buffercache";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 ADMIN EXTENSIONS SETUP ===';
|
||||
RAISE NOTICE 'Extensions configured in admin database:';
|
||||
RAISE NOTICE '- gt2_admin: uuid-ossp, pg_stat_statements, pg_buffercache, pgcrypto';
|
||||
RAISE NOTICE 'Note: PGVector NOT installed (admin cluster uses standard PostgreSQL)';
|
||||
RAISE NOTICE '=====================================';
|
||||
END $$;
|
||||
93
scripts/postgresql/docker-entrypoint-wrapper.sh
Executable file
93
scripts/postgresql/docker-entrypoint-wrapper.sh
Executable file
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
# GT 2.0 PostgreSQL Custom Entrypoint
|
||||
# Ensures pg_hba.conf is configured on EVERY startup, not just initialization
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 GT 2.0 PostgreSQL Startup - Configuring replication..."
|
||||
|
||||
# Function to configure pg_hba.conf
|
||||
configure_pg_hba() {
|
||||
local pg_hba_path="/var/lib/postgresql/data/pg_hba.conf"
|
||||
|
||||
if [ -f "$pg_hba_path" ]; then
|
||||
echo "📝 Configuring pg_hba.conf for replication..."
|
||||
|
||||
# Remove any existing GT 2.0 replication entries to avoid duplicates
|
||||
grep -v "# GT 2.0 Replication" "$pg_hba_path" > /tmp/pg_hba_clean.conf || true
|
||||
mv /tmp/pg_hba_clean.conf "$pg_hba_path"
|
||||
|
||||
# Add replication entries
|
||||
cat >> "$pg_hba_path" << 'EOF'
|
||||
|
||||
# GT 2.0 Replication Configuration
|
||||
host replication replicator 172.16.0.0/12 md5
|
||||
host replication replicator 172.20.0.0/16 md5
|
||||
host replication replicator 172.18.0.0/16 md5
|
||||
host replication replicator 10.0.0.0/8 md5
|
||||
host all all 172.16.0.0/12 md5
|
||||
host all all 172.20.0.0/16 md5
|
||||
host all all 172.18.0.0/16 md5
|
||||
host all all 10.0.0.0/8 md5
|
||||
EOF
|
||||
|
||||
echo "✅ pg_hba.conf configured for replication"
|
||||
else
|
||||
echo "⚠️ pg_hba.conf not found - will be created during initialization"
|
||||
fi
|
||||
}
|
||||
|
||||
# If PostgreSQL data directory exists, configure it before starting
|
||||
if [ -d /var/lib/postgresql/data ] && [ -f /var/lib/postgresql/data/PG_VERSION ]; then
|
||||
configure_pg_hba
|
||||
fi
|
||||
|
||||
# Function to update user passwords from environment variables
|
||||
update_user_passwords() {
|
||||
echo "🔐 Updating user passwords from environment variables..."
|
||||
|
||||
# Update gt2_tenant_user password if TENANT_USER_PASSWORD is set
|
||||
if [ -n "$TENANT_USER_PASSWORD" ]; then
|
||||
psql -U postgres -d gt2_tenants -c "ALTER USER gt2_tenant_user WITH PASSWORD '$TENANT_USER_PASSWORD';" >/dev/null 2>&1 && \
|
||||
echo "✅ Updated gt2_tenant_user password" || \
|
||||
echo "⚠️ Could not update gt2_tenant_user password (user may not exist yet)"
|
||||
fi
|
||||
|
||||
# Update replicator password if TENANT_REPLICATOR_PASSWORD is set
|
||||
if [ -n "$POSTGRES_REPLICATION_PASSWORD" ]; then
|
||||
psql -U postgres -d gt2_tenants -c "ALTER USER replicator WITH PASSWORD '$POSTGRES_REPLICATION_PASSWORD';" >/dev/null 2>&1 && \
|
||||
echo "✅ Updated replicator password" || \
|
||||
echo "⚠️ Could not update replicator password (user may not exist yet)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to configure after PostgreSQL starts
|
||||
configure_after_start() {
|
||||
sleep 5 # Wait for PostgreSQL to fully start
|
||||
configure_pg_hba
|
||||
|
||||
# Reload configuration if PostgreSQL is running
|
||||
if pg_isready -U postgres >/dev/null 2>&1; then
|
||||
echo "🔄 Reloading PostgreSQL configuration..."
|
||||
psql -U postgres -c "SELECT pg_reload_conf();" >/dev/null 2>&1 || true
|
||||
|
||||
# Update passwords from environment variables
|
||||
update_user_passwords
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure after PostgreSQL starts (in background)
|
||||
configure_after_start &
|
||||
|
||||
echo "🚀 Starting PostgreSQL with GT 2.0 configuration..."
|
||||
|
||||
# Pre-create tablespace directories with proper ownership for Linux compatibility
|
||||
# Required for x86/DGX deployments where bind mounts preserve host ownership
|
||||
echo "📁 Preparing tablespace directories..."
|
||||
mkdir -p /var/lib/postgresql/tablespaces/tenant_test
|
||||
chown postgres:postgres /var/lib/postgresql/tablespaces/tenant_test
|
||||
chmod 700 /var/lib/postgresql/tablespaces/tenant_test
|
||||
echo "✅ Tablespace directories ready"
|
||||
|
||||
# Call the original PostgreSQL entrypoint
|
||||
exec docker-entrypoint.sh "$@"
|
||||
106
scripts/postgresql/migrations/T001_rename_teams_to_tenants.sql
Normal file
106
scripts/postgresql/migrations/T001_rename_teams_to_tenants.sql
Normal file
@@ -0,0 +1,106 @@
|
||||
-- Migration T001: Rename 'teams' table to 'tenants' for semantic clarity
|
||||
-- Date: November 6, 2025
|
||||
--
|
||||
-- RATIONALE:
|
||||
-- The 'teams' table is misnamed - it stores TENANT metadata (one row per tenant),
|
||||
-- not user collaboration teams. This rename eliminates confusion and frees up the
|
||||
-- 'teams' name for actual user collaboration features.
|
||||
--
|
||||
-- IMPACT:
|
||||
-- - Renames table: teams → tenants
|
||||
-- - Renames all foreign key columns: team_id → tenant_id
|
||||
-- - Updates all constraints and indexes
|
||||
-- - NO DATA LOSS - purely structural rename
|
||||
--
|
||||
-- IDEMPOTENT: Can be run multiple times safely
|
||||
-- ROLLBACK: See rollback script: T001_rollback.sql
|
||||
|
||||
-- Note: When run via docker exec, we're already connected to gt2_tenants
|
||||
-- So we don't use \c command here
|
||||
|
||||
SET search_path TO tenant_test_company, public;
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Idempotency wrapper: Only run if migration hasn't been applied yet
|
||||
DO $$
|
||||
DECLARE
|
||||
teams_exists BOOLEAN;
|
||||
tenants_exists BOOLEAN;
|
||||
BEGIN
|
||||
-- Check if old 'teams' table exists and new 'tenants' table doesn't
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'tenant_test_company'
|
||||
AND table_name = 'teams'
|
||||
) INTO teams_exists;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'tenant_test_company'
|
||||
AND table_name = 'tenants'
|
||||
) INTO tenants_exists;
|
||||
|
||||
IF teams_exists AND NOT tenants_exists THEN
|
||||
RAISE NOTICE 'Migration T001: Applying teams → tenants rename...';
|
||||
|
||||
-- Step 1: Rename the table
|
||||
ALTER TABLE teams RENAME TO tenants;
|
||||
|
||||
-- Step 2: Rename foreign key columns in all dependent tables
|
||||
ALTER TABLE users RENAME COLUMN team_id TO tenant_id;
|
||||
ALTER TABLE agents RENAME COLUMN team_id TO tenant_id;
|
||||
ALTER TABLE datasets RENAME COLUMN team_id TO tenant_id;
|
||||
ALTER TABLE conversations RENAME COLUMN team_id TO tenant_id;
|
||||
ALTER TABLE documents RENAME COLUMN team_id TO tenant_id;
|
||||
ALTER TABLE document_chunks RENAME COLUMN team_id TO tenant_id;
|
||||
|
||||
-- Step 3: Rename foreign key constraints
|
||||
ALTER TABLE users RENAME CONSTRAINT users_team_id_fkey TO users_tenant_id_fkey;
|
||||
ALTER TABLE agents RENAME CONSTRAINT agents_team_id_fkey TO agents_tenant_id_fkey;
|
||||
ALTER TABLE datasets RENAME CONSTRAINT datasets_team_id_fkey TO datasets_tenant_id_fkey;
|
||||
ALTER TABLE conversations RENAME CONSTRAINT conversations_team_id_fkey TO conversations_tenant_id_fkey;
|
||||
ALTER TABLE documents RENAME CONSTRAINT documents_team_id_fkey TO documents_tenant_id_fkey;
|
||||
ALTER TABLE document_chunks RENAME CONSTRAINT document_chunks_team_id_fkey TO document_chunks_tenant_id_fkey;
|
||||
|
||||
-- Step 4: Rename indexes
|
||||
ALTER INDEX IF EXISTS idx_teams_domain RENAME TO idx_tenants_domain;
|
||||
ALTER INDEX IF EXISTS idx_users_team_id RENAME TO idx_users_tenant_id;
|
||||
ALTER INDEX IF EXISTS idx_agents_team_id RENAME TO idx_agents_tenant_id;
|
||||
ALTER INDEX IF EXISTS idx_datasets_team_id RENAME TO idx_datasets_tenant_id;
|
||||
ALTER INDEX IF EXISTS idx_conversations_team_id RENAME TO idx_conversations_tenant_id;
|
||||
ALTER INDEX IF EXISTS idx_documents_team_id RENAME TO idx_documents_tenant_id;
|
||||
ALTER INDEX IF EXISTS idx_document_chunks_team_id RENAME TO idx_document_chunks_tenant_id;
|
||||
|
||||
RAISE NOTICE '✅ Migration T001 applied successfully!';
|
||||
RAISE NOTICE ' - Table renamed: teams → tenants';
|
||||
RAISE NOTICE ' - Columns renamed: team_id → tenant_id (6 tables)';
|
||||
RAISE NOTICE ' - Constraints renamed: 6 foreign keys';
|
||||
RAISE NOTICE ' - Indexes renamed: 7 indexes';
|
||||
|
||||
ELSIF NOT teams_exists AND tenants_exists THEN
|
||||
RAISE NOTICE '✅ Migration T001 already applied (tenants table exists, teams table renamed)';
|
||||
ELSIF teams_exists AND tenants_exists THEN
|
||||
RAISE WARNING '⚠️ Migration T001 in inconsistent state: both teams and tenants tables exist!';
|
||||
RAISE WARNING ' Manual intervention may be required.';
|
||||
ELSE
|
||||
RAISE WARNING '⚠️ Migration T001 cannot run: neither teams nor tenants table exists!';
|
||||
RAISE WARNING ' Check if schema is properly initialized.';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verification query
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_count INTEGER;
|
||||
user_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO tenant_count FROM tenants;
|
||||
SELECT COUNT(*) INTO user_count FROM users;
|
||||
|
||||
RAISE NOTICE 'Migration T001 verification:';
|
||||
RAISE NOTICE ' Tenants: % rows', tenant_count;
|
||||
RAISE NOTICE ' Users: % rows', user_count;
|
||||
END $$;
|
||||
91
scripts/postgresql/migrations/T001_rollback.sql
Normal file
91
scripts/postgresql/migrations/T001_rollback.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- Rollback Migration T001: Rename 'tenants' table back to 'teams'
|
||||
-- Date: November 6, 2025
|
||||
--
|
||||
-- This script reverses the T001_rename_teams_to_tenants.sql migration
|
||||
-- Use only if you need to rollback the migration for any reason
|
||||
--
|
||||
-- NO DATA LOSS - purely structural rename back to original state
|
||||
-- IDEMPOTENT: Can be run multiple times safely
|
||||
|
||||
SET search_path TO tenant_test_company, public;
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Idempotency wrapper: Only run if rollback hasn't been applied yet
|
||||
DO $$
|
||||
DECLARE
|
||||
teams_exists BOOLEAN;
|
||||
tenants_exists BOOLEAN;
|
||||
BEGIN
|
||||
-- Check current state
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'tenant_test_company'
|
||||
AND table_name = 'teams'
|
||||
) INTO teams_exists;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'tenant_test_company'
|
||||
AND table_name = 'tenants'
|
||||
) INTO tenants_exists;
|
||||
|
||||
IF NOT teams_exists AND tenants_exists THEN
|
||||
RAISE NOTICE 'Rollback T001: Reverting tenants → teams rename...';
|
||||
|
||||
-- Step 1: Rename the table back
|
||||
ALTER TABLE tenants RENAME TO teams;
|
||||
|
||||
-- Step 2: Rename foreign key columns back
|
||||
ALTER TABLE users RENAME COLUMN tenant_id TO team_id;
|
||||
ALTER TABLE agents RENAME COLUMN tenant_id TO team_id;
|
||||
ALTER TABLE datasets RENAME COLUMN tenant_id TO team_id;
|
||||
ALTER TABLE conversations RENAME COLUMN tenant_id TO team_id;
|
||||
ALTER TABLE documents RENAME COLUMN tenant_id TO team_id;
|
||||
ALTER TABLE document_chunks RENAME COLUMN tenant_id TO team_id;
|
||||
|
||||
-- Step 3: Rename foreign key constraints back
|
||||
ALTER TABLE users RENAME CONSTRAINT users_tenant_id_fkey TO users_team_id_fkey;
|
||||
ALTER TABLE agents RENAME CONSTRAINT agents_tenant_id_fkey TO agents_team_id_fkey;
|
||||
ALTER TABLE datasets RENAME CONSTRAINT datasets_tenant_id_fkey TO datasets_team_id_fkey;
|
||||
ALTER TABLE conversations RENAME CONSTRAINT conversations_tenant_id_fkey TO conversations_team_id_fkey;
|
||||
ALTER TABLE documents RENAME CONSTRAINT documents_tenant_id_fkey TO documents_team_id_fkey;
|
||||
ALTER TABLE document_chunks RENAME CONSTRAINT document_chunks_tenant_id_fkey TO document_chunks_team_id_fkey;
|
||||
|
||||
-- Step 4: Rename indexes back
|
||||
ALTER INDEX IF EXISTS idx_tenants_domain RENAME TO idx_teams_domain;
|
||||
ALTER INDEX IF EXISTS idx_users_tenant_id RENAME TO idx_users_team_id;
|
||||
ALTER INDEX IF EXISTS idx_agents_tenant_id RENAME TO idx_agents_team_id;
|
||||
ALTER INDEX IF EXISTS idx_datasets_tenant_id RENAME TO idx_datasets_team_id;
|
||||
ALTER INDEX IF EXISTS idx_conversations_tenant_id RENAME TO idx_conversations_team_id;
|
||||
ALTER INDEX IF EXISTS idx_documents_tenant_id RENAME TO idx_documents_team_id;
|
||||
ALTER INDEX IF EXISTS idx_document_chunks_tenant_id RENAME TO idx_document_chunks_team_id;
|
||||
|
||||
RAISE NOTICE '✅ Rollback T001 completed successfully!';
|
||||
RAISE NOTICE ' - Table renamed: tenants → teams';
|
||||
RAISE NOTICE ' - Columns renamed: tenant_id → team_id (6 tables)';
|
||||
RAISE NOTICE ' - Constraints renamed: 6 foreign keys';
|
||||
RAISE NOTICE ' - Indexes renamed: 7 indexes';
|
||||
|
||||
ELSIF teams_exists AND NOT tenants_exists THEN
|
||||
RAISE NOTICE '✅ Rollback T001 already applied (teams table exists, tenants table not found)';
|
||||
ELSE
|
||||
RAISE WARNING '⚠️ Rollback T001 cannot determine state: teams=%,tenants=%', teams_exists, tenants_exists;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verification
|
||||
DO $$
|
||||
DECLARE
|
||||
team_count INTEGER;
|
||||
user_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO team_count FROM teams;
|
||||
SELECT COUNT(*) INTO user_count FROM users;
|
||||
|
||||
RAISE NOTICE 'Rollback T001 verification:';
|
||||
RAISE NOTICE ' Teams: % rows', team_count;
|
||||
RAISE NOTICE ' Users: % rows', user_count;
|
||||
END $$;
|
||||
@@ -0,0 +1,34 @@
|
||||
-- Migration: Add invitation status tracking to team_memberships
|
||||
-- Created: 2025-01-07
|
||||
-- Purpose: Enable team invitation accept/decline workflow
|
||||
|
||||
SET search_path TO tenant_test_company, public;
|
||||
|
||||
-- Add status tracking columns
|
||||
ALTER TABLE team_memberships
|
||||
ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'accepted'
|
||||
CHECK (status IN ('pending', 'accepted', 'declined'));
|
||||
|
||||
ALTER TABLE team_memberships
|
||||
ADD COLUMN IF NOT EXISTS invited_at TIMESTAMPTZ DEFAULT NOW();
|
||||
|
||||
ALTER TABLE team_memberships
|
||||
ADD COLUMN IF NOT EXISTS responded_at TIMESTAMPTZ;
|
||||
|
||||
-- Update existing memberships to 'accepted' status
|
||||
-- This ensures backward compatibility with existing data
|
||||
UPDATE team_memberships
|
||||
SET status = 'accepted', invited_at = created_at
|
||||
WHERE status IS NULL;
|
||||
|
||||
-- Create index for efficient pending invitation queries
|
||||
CREATE INDEX IF NOT EXISTS idx_team_memberships_status
|
||||
ON team_memberships(user_id, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_team_memberships_team_status
|
||||
ON team_memberships(team_id, status);
|
||||
|
||||
-- Add comment for documentation
|
||||
COMMENT ON COLUMN team_memberships.status IS 'Invitation status: pending (invited), accepted (active member), declined (rejected invitation)';
|
||||
COMMENT ON COLUMN team_memberships.invited_at IS 'Timestamp when invitation was sent';
|
||||
COMMENT ON COLUMN team_memberships.responded_at IS 'Timestamp when invitation was accepted or declined';
|
||||
@@ -0,0 +1,216 @@
|
||||
-- Migration T002: Create User Collaboration Teams Tables
|
||||
-- Date: November 6, 2025
|
||||
--
|
||||
-- PURPOSE:
|
||||
-- Creates tables for user collaboration teams (different from tenant metadata).
|
||||
-- Users can create teams, invite members, and share agents/datasets with team members.
|
||||
--
|
||||
-- TABLES CREATED:
|
||||
-- 1. teams - User collaboration teams (NOT tenant metadata)
|
||||
-- 2. team_memberships - Team members with two-tier permissions
|
||||
--
|
||||
-- PERMISSION MODEL:
|
||||
-- Tier 1 (Team-level): 'read' (access resources) or 'share' (access + share own resources)
|
||||
-- Tier 2 (Resource-level): Per-user permissions stored in JSONB {"agent:uuid": "read|edit"}
|
||||
--
|
||||
-- IDEMPOTENT: Can be run multiple times safely
|
||||
-- DEPENDS ON: T001_rename_teams_to_tenants.sql (must run first)
|
||||
|
||||
-- Note: When run via docker exec, we're already connected to gt2_tenants
|
||||
|
||||
SET search_path TO tenant_test_company, public;
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Table 1: User Collaboration Teams
|
||||
-- This is the NEW teams table for user collaboration (replaces old misnamed tenant table)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'tenant_test_company'
|
||||
AND table_name = 'teams'
|
||||
) THEN
|
||||
CREATE TABLE teams (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- Tenant isolation
|
||||
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Team owner
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
RAISE NOTICE '✅ Created teams table for user collaboration';
|
||||
ELSE
|
||||
RAISE NOTICE '✅ Teams table already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Table 2: Team Memberships with Two-Tier Permissions
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'tenant_test_company'
|
||||
AND table_name = 'team_memberships'
|
||||
) THEN
|
||||
CREATE TABLE team_memberships (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tier 1: Team-level permission (set by team owner)
|
||||
team_permission VARCHAR(20) NOT NULL DEFAULT 'read'
|
||||
CHECK (team_permission IN ('read', 'share')),
|
||||
-- 'read' = can access resources shared to this team
|
||||
-- 'share' = can access resources AND share own resources to this team
|
||||
|
||||
-- Tier 2: Resource-level permissions (set by resource sharer when sharing)
|
||||
-- JSONB structure: {"agent:uuid": "read|edit", "dataset:uuid": "read|edit"}
|
||||
resource_permissions JSONB DEFAULT '{}',
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(team_id, user_id) -- Prevent duplicate memberships
|
||||
);
|
||||
|
||||
RAISE NOTICE '✅ Created team_memberships table';
|
||||
ELSE
|
||||
RAISE NOTICE '✅ Team_memberships table already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Performance indexes
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_indexes
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
AND indexname = 'idx_teams_owner_id'
|
||||
) THEN
|
||||
CREATE INDEX idx_teams_owner_id ON teams(owner_id);
|
||||
RAISE NOTICE '✅ Created index: idx_teams_owner_id';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_indexes
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
AND indexname = 'idx_teams_tenant_id'
|
||||
) THEN
|
||||
CREATE INDEX idx_teams_tenant_id ON teams(tenant_id);
|
||||
RAISE NOTICE '✅ Created index: idx_teams_tenant_id';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_indexes
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
AND indexname = 'idx_team_memberships_team_id'
|
||||
) THEN
|
||||
CREATE INDEX idx_team_memberships_team_id ON team_memberships(team_id);
|
||||
RAISE NOTICE '✅ Created index: idx_team_memberships_team_id';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_indexes
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
AND indexname = 'idx_team_memberships_user_id'
|
||||
) THEN
|
||||
CREATE INDEX idx_team_memberships_user_id ON team_memberships(user_id);
|
||||
RAISE NOTICE '✅ Created index: idx_team_memberships_user_id';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_indexes
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
AND indexname = 'idx_team_memberships_resources'
|
||||
) THEN
|
||||
CREATE INDEX idx_team_memberships_resources ON team_memberships USING gin(resource_permissions);
|
||||
RAISE NOTICE '✅ Created index: idx_team_memberships_resources';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Function: Auto-unshare resources when user loses 'share' permission
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname = 'tenant_test_company'
|
||||
AND p.proname = 'auto_unshare_on_permission_downgrade'
|
||||
) THEN
|
||||
CREATE FUNCTION auto_unshare_on_permission_downgrade()
|
||||
RETURNS TRIGGER AS $func$
|
||||
BEGIN
|
||||
-- If team_permission changed from 'share' to 'read'
|
||||
IF OLD.team_permission = 'share' AND NEW.team_permission = 'read' THEN
|
||||
-- Clear all resource permissions for this user
|
||||
-- (they can no longer share resources, so remove what they shared)
|
||||
NEW.resource_permissions := '{}'::jsonb;
|
||||
|
||||
RAISE NOTICE 'Auto-unshared all resources for user % in team % due to permission downgrade',
|
||||
NEW.user_id, NEW.team_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$func$ LANGUAGE plpgsql;
|
||||
|
||||
RAISE NOTICE '✅ Created function: auto_unshare_on_permission_downgrade';
|
||||
ELSE
|
||||
RAISE NOTICE '✅ Function auto_unshare_on_permission_downgrade already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Trigger: Apply auto-unshare logic
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT FROM pg_trigger
|
||||
WHERE tgname = 'trigger_auto_unshare'
|
||||
) THEN
|
||||
CREATE TRIGGER trigger_auto_unshare
|
||||
BEFORE UPDATE OF team_permission ON team_memberships
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION auto_unshare_on_permission_downgrade();
|
||||
|
||||
RAISE NOTICE '✅ Created trigger: trigger_auto_unshare';
|
||||
ELSE
|
||||
RAISE NOTICE '✅ Trigger trigger_auto_unshare already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Grant permissions
|
||||
DO $$
|
||||
BEGIN
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON teams TO gt2_tenant_user;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON team_memberships TO gt2_tenant_user;
|
||||
RAISE NOTICE '✅ Granted permissions to gt2_tenant_user';
|
||||
EXCEPTION
|
||||
WHEN undefined_object THEN
|
||||
RAISE NOTICE '⚠️ Role gt2_tenant_user does not exist (ok for fresh installs)';
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Final verification
|
||||
DO $$
|
||||
DECLARE
|
||||
teams_count INTEGER;
|
||||
memberships_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO teams_count FROM teams;
|
||||
SELECT COUNT(*) INTO memberships_count FROM team_memberships;
|
||||
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE '========================================';
|
||||
RAISE NOTICE '✅ Migration T002 completed successfully!';
|
||||
RAISE NOTICE '========================================';
|
||||
RAISE NOTICE 'Tables created:';
|
||||
RAISE NOTICE ' - teams (user collaboration): % rows', teams_count;
|
||||
RAISE NOTICE ' - team_memberships: % rows', memberships_count;
|
||||
RAISE NOTICE 'Indexes: 5 created';
|
||||
RAISE NOTICE 'Functions: 1 created';
|
||||
RAISE NOTICE 'Triggers: 1 created';
|
||||
RAISE NOTICE '========================================';
|
||||
END $$;
|
||||
313
scripts/postgresql/migrations/T003_team_resource_shares.sql
Normal file
313
scripts/postgresql/migrations/T003_team_resource_shares.sql
Normal file
@@ -0,0 +1,313 @@
|
||||
-- Migration T003: Team Resource Sharing System
|
||||
-- Purpose: Enable multi-team resource sharing for agents and datasets
|
||||
-- Dependencies: T002_create_collaboration_teams.sql
|
||||
-- Author: GT 2.0 Development Team
|
||||
-- Date: 2025-01-07
|
||||
|
||||
-- Set schema for tenant isolation
|
||||
SET search_path TO tenant_test_company;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 1: Junction Table for Many-to-Many Resource Sharing
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS team_resource_shares (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
resource_type VARCHAR(20) NOT NULL CHECK (resource_type IN ('agent', 'dataset')),
|
||||
resource_id UUID NOT NULL,
|
||||
shared_by UUID NOT NULL REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
|
||||
-- Ensure each resource can only be shared once per team
|
||||
UNIQUE(team_id, resource_type, resource_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE team_resource_shares IS 'Junction table for sharing agents/datasets with collaboration teams';
|
||||
COMMENT ON COLUMN team_resource_shares.resource_type IS 'Type of resource: agent or dataset';
|
||||
COMMENT ON COLUMN team_resource_shares.resource_id IS 'UUID of the agent or dataset being shared';
|
||||
COMMENT ON COLUMN team_resource_shares.shared_by IS 'User who shared this resource with the team';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 2: Performance Indexes
|
||||
-- ============================================================================
|
||||
|
||||
-- Index for finding all teams a resource is shared with
|
||||
CREATE INDEX idx_trs_resource ON team_resource_shares(resource_type, resource_id);
|
||||
|
||||
-- Index for finding all resources shared with a team
|
||||
CREATE INDEX idx_trs_team ON team_resource_shares(team_id);
|
||||
|
||||
-- Index for finding resources shared by a specific user
|
||||
CREATE INDEX idx_trs_shared_by ON team_resource_shares(shared_by);
|
||||
|
||||
-- Composite index for common access checks
|
||||
CREATE INDEX idx_trs_lookup ON team_resource_shares(team_id, resource_type, resource_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 3: Helper View #1 - Individual User Resource Access
|
||||
-- ============================================================================
|
||||
-- Purpose: Flatten team memberships + resource shares for fast permission checks
|
||||
-- Usage: Check if specific user has access to specific resource
|
||||
|
||||
CREATE VIEW user_resource_access AS
|
||||
SELECT
|
||||
tm.user_id,
|
||||
trs.resource_type,
|
||||
trs.resource_id,
|
||||
tm.resource_permissions->(trs.resource_type || ':' || trs.resource_id::text) as permission,
|
||||
tm.team_id,
|
||||
tm.team_permission,
|
||||
trs.shared_by,
|
||||
trs.created_at
|
||||
FROM team_memberships tm
|
||||
JOIN team_resource_shares trs ON tm.team_id = trs.team_id
|
||||
WHERE tm.resource_permissions ? (trs.resource_type || ':' || trs.resource_id::text);
|
||||
|
||||
COMMENT ON VIEW user_resource_access IS 'Flattened view of user access to resources via team memberships';
|
||||
|
||||
-- Note: Indexes on views are not supported in standard PostgreSQL
|
||||
-- For performance, consider creating a materialized view if needed
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 4: Helper View #2 - Aggregated User Accessible Resources
|
||||
-- ============================================================================
|
||||
-- Purpose: Aggregate resources by user for efficient listing
|
||||
-- Usage: Get all agents/datasets accessible to a user (for list views)
|
||||
|
||||
CREATE VIEW user_accessible_resources AS
|
||||
SELECT
|
||||
tm.user_id,
|
||||
trs.resource_type,
|
||||
trs.resource_id,
|
||||
MAX(CASE
|
||||
WHEN tm.resource_permissions->(trs.resource_type || ':' || trs.resource_id::text) = '"edit"'::jsonb
|
||||
THEN 'edit'
|
||||
WHEN tm.resource_permissions->(trs.resource_type || ':' || trs.resource_id::text) = '"read"'::jsonb
|
||||
THEN 'read'
|
||||
ELSE 'none'
|
||||
END) as best_permission,
|
||||
COUNT(DISTINCT tm.team_id) as shared_in_teams,
|
||||
ARRAY_AGG(DISTINCT tm.team_id) as team_ids,
|
||||
MIN(trs.created_at) as first_shared_at
|
||||
FROM team_memberships tm
|
||||
JOIN team_resource_shares trs ON tm.team_id = trs.team_id
|
||||
WHERE tm.resource_permissions ? (trs.resource_type || ':' || trs.resource_id::text)
|
||||
GROUP BY tm.user_id, trs.resource_type, trs.resource_id;
|
||||
|
||||
COMMENT ON VIEW user_accessible_resources IS 'Aggregated view showing all resources accessible to each user with best permission level';
|
||||
|
||||
-- Note: Indexes on views are not supported in standard PostgreSQL
|
||||
-- For performance, consider creating a materialized view if needed
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 5: Cascade Cleanup Trigger
|
||||
-- ============================================================================
|
||||
-- Purpose: When a resource is unshared from a team, clean up member permissions
|
||||
-- Note: The ON DELETE CASCADE on team_resource_shares already handles team deletion
|
||||
|
||||
CREATE OR REPLACE FUNCTION cleanup_resource_permissions()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Remove the resource permission key from all team members
|
||||
UPDATE team_memberships
|
||||
SET resource_permissions = resource_permissions - (OLD.resource_type || ':' || OLD.resource_id::text)
|
||||
WHERE team_id = OLD.team_id
|
||||
AND resource_permissions ? (OLD.resource_type || ':' || OLD.resource_id::text);
|
||||
|
||||
RETURN OLD;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_cleanup_resource_permissions
|
||||
BEFORE DELETE ON team_resource_shares
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION cleanup_resource_permissions();
|
||||
|
||||
COMMENT ON FUNCTION cleanup_resource_permissions IS 'Removes resource permission entries from team members when resource is unshared';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 6: Validation Function
|
||||
-- ============================================================================
|
||||
-- Purpose: Validate that a user has 'share' permission before sharing resources
|
||||
|
||||
CREATE OR REPLACE FUNCTION validate_resource_share()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
user_team_permission VARCHAR(20);
|
||||
BEGIN
|
||||
-- Check if the user has 'share' permission on the team
|
||||
SELECT team_permission INTO user_team_permission
|
||||
FROM team_memberships
|
||||
WHERE team_id = NEW.team_id
|
||||
AND user_id = NEW.shared_by;
|
||||
|
||||
IF user_team_permission IS NULL THEN
|
||||
RAISE EXCEPTION 'User % is not a member of team %', NEW.shared_by, NEW.team_id;
|
||||
END IF;
|
||||
|
||||
IF user_team_permission != 'share' THEN
|
||||
RAISE EXCEPTION 'User % does not have share permission on team %', NEW.shared_by, NEW.team_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_validate_resource_share
|
||||
BEFORE INSERT ON team_resource_shares
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION validate_resource_share();
|
||||
|
||||
COMMENT ON FUNCTION validate_resource_share IS 'Ensures only users with share permission can share resources to teams';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 6B: Sync JSONB Permissions When Resource Shared
|
||||
-- ============================================================================
|
||||
-- Purpose: Automatically update team_memberships.resource_permissions when
|
||||
-- a resource is shared to a team. This ensures database-level consistency.
|
||||
|
||||
CREATE OR REPLACE FUNCTION sync_resource_permissions_on_share()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Note: This trigger is called AFTER validation, so we know the share is valid
|
||||
-- The actual permission levels (read/edit) are set by the application layer
|
||||
-- This trigger just ensures the resource key exists in the JSONB
|
||||
--
|
||||
-- The application will call a separate function to set individual user permissions
|
||||
-- after this trigger runs. This is a two-step process:
|
||||
-- 1. This trigger: Ensure resource is known to the team
|
||||
-- 2. Application: Set per-user permissions via update_member_resource_permission()
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Note: We're keeping this trigger simple for now. The application layer handles
|
||||
-- per-user permission assignment. A future optimization could move all permission
|
||||
-- logic into triggers, but that requires storing default permissions in team_resource_shares.
|
||||
|
||||
COMMENT ON FUNCTION sync_resource_permissions_on_share IS 'Placeholder for future JSONB sync automation';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 7: Helper Functions for Application Layer
|
||||
-- ============================================================================
|
||||
|
||||
-- Function to get all resources shared with a team
|
||||
CREATE OR REPLACE FUNCTION get_team_resources(p_team_id UUID, p_resource_type VARCHAR DEFAULT NULL)
|
||||
RETURNS TABLE (
|
||||
resource_id UUID,
|
||||
resource_type VARCHAR,
|
||||
shared_by UUID,
|
||||
created_at TIMESTAMP,
|
||||
member_count BIGINT
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
trs.resource_id,
|
||||
trs.resource_type,
|
||||
trs.shared_by,
|
||||
trs.created_at,
|
||||
COUNT(DISTINCT tm.user_id) as member_count
|
||||
FROM team_resource_shares trs
|
||||
JOIN team_memberships tm ON tm.team_id = trs.team_id
|
||||
WHERE trs.team_id = p_team_id
|
||||
AND (p_resource_type IS NULL OR trs.resource_type = p_resource_type)
|
||||
AND tm.resource_permissions ? (trs.resource_type || ':' || trs.resource_id::text)
|
||||
GROUP BY trs.resource_id, trs.resource_type, trs.shared_by, trs.created_at
|
||||
ORDER BY trs.created_at DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION get_team_resources IS 'Get all resources shared with a team, optionally filtered by resource type';
|
||||
|
||||
-- Function to check if a user has permission on a resource
|
||||
CREATE OR REPLACE FUNCTION check_user_resource_permission(
|
||||
p_user_id UUID,
|
||||
p_resource_type VARCHAR,
|
||||
p_resource_id UUID,
|
||||
p_required_permission VARCHAR DEFAULT 'read'
|
||||
)
|
||||
RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
user_permission VARCHAR;
|
||||
BEGIN
|
||||
-- Get the user's permission from any team that has this resource
|
||||
SELECT (ura.permission::text)
|
||||
INTO user_permission
|
||||
FROM user_resource_access ura
|
||||
WHERE ura.user_id = p_user_id
|
||||
AND ura.resource_type = p_resource_type
|
||||
AND ura.resource_id = p_resource_id
|
||||
LIMIT 1;
|
||||
|
||||
-- If no permission found, return false
|
||||
IF user_permission IS NULL THEN
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
|
||||
-- Remove quotes from JSONB string value
|
||||
user_permission := TRIM(BOTH '"' FROM user_permission);
|
||||
|
||||
-- Check permission level
|
||||
IF p_required_permission = 'read' THEN
|
||||
RETURN user_permission IN ('read', 'edit');
|
||||
ELSIF p_required_permission = 'edit' THEN
|
||||
RETURN user_permission = 'edit';
|
||||
ELSE
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION check_user_resource_permission IS 'Check if user has required permission (read/edit) on a resource';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 8: Migration Data (if needed)
|
||||
-- ============================================================================
|
||||
|
||||
-- If there are any existing agents/datasets with visibility='team',
|
||||
-- they would need to be migrated here. Since this is a fresh feature,
|
||||
-- no data migration is needed.
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 9: Grant Permissions
|
||||
-- ============================================================================
|
||||
|
||||
-- Grant appropriate permissions to application roles
|
||||
-- Note: Adjust role names based on your PostgreSQL setup
|
||||
|
||||
-- GRANT SELECT, INSERT, UPDATE, DELETE ON team_resource_shares TO gt2_tenant_user;
|
||||
-- GRANT SELECT ON user_resource_access TO gt2_tenant_user;
|
||||
-- GRANT SELECT ON user_accessible_resources TO gt2_tenant_user;
|
||||
-- GRANT EXECUTE ON FUNCTION get_team_resources TO gt2_tenant_user;
|
||||
-- GRANT EXECUTE ON FUNCTION check_user_resource_permission TO gt2_tenant_user;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 10: Verification Queries
|
||||
-- ============================================================================
|
||||
|
||||
-- Verify table was created
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'team_resource_shares') THEN
|
||||
RAISE NOTICE 'SUCCESS: team_resource_shares table created';
|
||||
ELSE
|
||||
RAISE EXCEPTION 'FAILURE: team_resource_shares table not found';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.views WHERE table_name = 'user_resource_access') THEN
|
||||
RAISE NOTICE 'SUCCESS: user_resource_access view created';
|
||||
ELSE
|
||||
RAISE EXCEPTION 'FAILURE: user_resource_access view not found';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.views WHERE table_name = 'user_accessible_resources') THEN
|
||||
RAISE NOTICE 'SUCCESS: user_accessible_resources view created';
|
||||
ELSE
|
||||
RAISE EXCEPTION 'FAILURE: user_accessible_resources view not found';
|
||||
END IF;
|
||||
|
||||
RAISE NOTICE 'Migration T003 completed successfully!';
|
||||
END $$;
|
||||
@@ -0,0 +1,78 @@
|
||||
-- Migration T004: Update validate_resource_share Trigger Function
|
||||
-- Purpose: Allow team owners and admins to share resources without requiring team membership
|
||||
-- Dependencies: T003_team_resource_shares.sql
|
||||
-- Author: GT 2.0 Development Team
|
||||
-- Date: 2025-01-07
|
||||
--
|
||||
-- Changes:
|
||||
-- - Add team owner bypass check (owners don't need team membership)
|
||||
-- - Add admin/developer role bypass check (admins can share to any team)
|
||||
-- - Preserve original team membership + share permission check for regular users
|
||||
--
|
||||
-- This migration is idempotent via CREATE OR REPLACE FUNCTION
|
||||
|
||||
SET search_path TO tenant_test_company;
|
||||
|
||||
CREATE OR REPLACE FUNCTION validate_resource_share()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_team_permission VARCHAR(20);
|
||||
is_team_owner BOOLEAN;
|
||||
user_role VARCHAR(50);
|
||||
user_tenant_id UUID;
|
||||
team_tenant_id UUID;
|
||||
BEGIN
|
||||
-- Check if user is team owner
|
||||
SELECT (owner_id = NEW.shared_by), tenant_id INTO is_team_owner, team_tenant_id
|
||||
FROM teams
|
||||
WHERE id = NEW.team_id;
|
||||
|
||||
-- Allow team owners to share
|
||||
IF is_team_owner THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Check if user is admin/developer (bypass membership requirement)
|
||||
SELECT u.user_type, u.tenant_id INTO user_role, user_tenant_id
|
||||
FROM users u
|
||||
WHERE u.id = NEW.shared_by;
|
||||
|
||||
-- Allow admins/developers in the same tenant
|
||||
IF user_role IN ('admin', 'developer', 'super_admin') AND user_tenant_id = team_tenant_id THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
-- Check if the user has 'share' permission on the team
|
||||
SELECT team_permission INTO user_team_permission
|
||||
FROM team_memberships
|
||||
WHERE team_id = NEW.team_id
|
||||
AND user_id = NEW.shared_by;
|
||||
|
||||
IF user_team_permission IS NULL THEN
|
||||
RAISE EXCEPTION 'User % is not a member of team %', NEW.shared_by, NEW.team_id;
|
||||
END IF;
|
||||
|
||||
IF user_team_permission != 'share' THEN
|
||||
RAISE EXCEPTION 'User % does not have share permission on team %', NEW.shared_by, NEW.team_id;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Verification: Check that the function exists
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname = 'tenant_test_company'
|
||||
AND p.proname = 'validate_resource_share'
|
||||
) THEN
|
||||
RAISE NOTICE 'SUCCESS: T004 migration completed - validate_resource_share function updated';
|
||||
ELSE
|
||||
RAISE EXCEPTION 'FAILED: T004 migration - validate_resource_share function not found';
|
||||
END IF;
|
||||
END $$;
|
||||
214
scripts/postgresql/migrations/T005_team_observability.sql
Normal file
214
scripts/postgresql/migrations/T005_team_observability.sql
Normal file
@@ -0,0 +1,214 @@
|
||||
-- Migration T005: Team Observability System
|
||||
-- Purpose: Add Observable member tracking for team-level activity monitoring
|
||||
-- Dependencies: T003_team_resource_shares.sql
|
||||
-- Author: GT 2.0 Development Team
|
||||
-- Date: 2025-01-10
|
||||
|
||||
-- Set schema for tenant isolation
|
||||
SET search_path TO tenant_test_company;
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 1: Add Observable Columns to team_memberships
|
||||
-- ============================================================================
|
||||
|
||||
-- Add Observable status tracking columns
|
||||
ALTER TABLE team_memberships
|
||||
ADD COLUMN IF NOT EXISTS is_observable BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS observable_consent_status VARCHAR(20) DEFAULT 'none',
|
||||
ADD COLUMN IF NOT EXISTS observable_consent_at TIMESTAMPTZ;
|
||||
|
||||
-- Add constraint for observable_consent_status values
|
||||
ALTER TABLE team_memberships
|
||||
ADD CONSTRAINT check_observable_consent_status
|
||||
CHECK (observable_consent_status IN ('none', 'pending', 'approved', 'revoked'));
|
||||
|
||||
COMMENT ON COLUMN team_memberships.is_observable IS 'Member consents to team managers viewing their activity';
|
||||
COMMENT ON COLUMN team_memberships.observable_consent_status IS 'Consent workflow status: none, pending, approved, revoked';
|
||||
COMMENT ON COLUMN team_memberships.observable_consent_at IS 'Timestamp when Observable status was approved';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 2: Extend team_permission to Include Manager Role
|
||||
-- ============================================================================
|
||||
|
||||
-- Drop existing constraint if it exists (handles both explicit and auto-generated names)
|
||||
ALTER TABLE team_memberships DROP CONSTRAINT IF EXISTS check_team_permission;
|
||||
ALTER TABLE team_memberships DROP CONSTRAINT IF EXISTS team_memberships_team_permission_check;
|
||||
|
||||
-- Add updated constraint with 'manager' role
|
||||
ALTER TABLE team_memberships
|
||||
ADD CONSTRAINT check_team_permission
|
||||
CHECK (team_permission IN ('read', 'share', 'manager'));
|
||||
|
||||
COMMENT ON COLUMN team_memberships.team_permission IS
|
||||
'Team role: read=Member (view only), share=Contributor (can share resources), manager=Manager (can manage members + view Observable activity)';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 3: Update Auto-Unshare Trigger for Manager Role
|
||||
-- ============================================================================
|
||||
|
||||
-- Update trigger function to handle 'manager' role
|
||||
CREATE OR REPLACE FUNCTION auto_unshare_on_permission_downgrade()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Clear resource_permissions when downgrading from share/manager to read
|
||||
-- Manager and Contributor (share) can share resources
|
||||
-- Member (read) cannot share resources
|
||||
IF OLD.team_permission IN ('share', 'manager')
|
||||
AND NEW.team_permission = 'read' THEN
|
||||
NEW.resource_permissions := '{}'::jsonb;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION auto_unshare_on_permission_downgrade IS
|
||||
'Clears resource_permissions when member is downgraded to read-only (Member role)';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 4: Update Resource Share Validation for Manager Role
|
||||
-- ============================================================================
|
||||
|
||||
-- Update validation function to allow managers to share
|
||||
CREATE OR REPLACE FUNCTION validate_resource_share()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
user_team_permission VARCHAR(20);
|
||||
is_team_owner BOOLEAN;
|
||||
user_role VARCHAR(50);
|
||||
BEGIN
|
||||
-- Get user's team permission
|
||||
SELECT team_permission INTO user_team_permission
|
||||
FROM team_memberships
|
||||
WHERE team_id = NEW.team_id
|
||||
AND user_id = NEW.shared_by;
|
||||
|
||||
-- Check if user is the team owner
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM teams
|
||||
WHERE id = NEW.team_id AND owner_id = NEW.shared_by
|
||||
) INTO is_team_owner;
|
||||
|
||||
-- Get user's system role for admin bypass
|
||||
SELECT role INTO user_role
|
||||
FROM users
|
||||
WHERE id = NEW.shared_by;
|
||||
|
||||
-- Allow if: owner, or has share/manager permission, or is admin/developer
|
||||
IF is_team_owner THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF user_role IN ('admin', 'developer') THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF user_team_permission IS NULL THEN
|
||||
RAISE EXCEPTION 'User % is not a member of team %', NEW.shared_by, NEW.team_id;
|
||||
END IF;
|
||||
|
||||
IF user_team_permission NOT IN ('share', 'manager') THEN
|
||||
RAISE EXCEPTION 'User % does not have permission to share resources (current permission: %)',
|
||||
NEW.shared_by, user_team_permission;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION validate_resource_share IS
|
||||
'Validates that only owners, managers, contributors (share), or admins can share resources to teams';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 5: Performance Indexes
|
||||
-- ============================================================================
|
||||
|
||||
-- Index for finding Observable members (used for activity queries)
|
||||
CREATE INDEX IF NOT EXISTS idx_team_memberships_observable
|
||||
ON team_memberships(team_id, is_observable, observable_consent_status)
|
||||
WHERE is_observable = true AND observable_consent_status = 'approved';
|
||||
|
||||
-- Index for finding members by role (for permission checks)
|
||||
CREATE INDEX IF NOT EXISTS idx_team_memberships_permission
|
||||
ON team_memberships(team_id, team_permission);
|
||||
|
||||
COMMENT ON INDEX idx_team_memberships_observable IS
|
||||
'Optimizes queries for Observable member activity (partial index for approved Observable members only)';
|
||||
COMMENT ON INDEX idx_team_memberships_permission IS
|
||||
'Optimizes role-based permission checks (finding managers, contributors, etc.)';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 6: Helper Function - Get Observable Members
|
||||
-- ============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_observable_members(p_team_id UUID)
|
||||
RETURNS TABLE (
|
||||
user_id UUID,
|
||||
user_email TEXT,
|
||||
user_name TEXT,
|
||||
observable_since TIMESTAMPTZ
|
||||
) AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
tm.user_id,
|
||||
u.email::text as user_email,
|
||||
u.full_name::text as user_name,
|
||||
tm.observable_consent_at
|
||||
FROM team_memberships tm
|
||||
JOIN users u ON tm.user_id = u.id
|
||||
WHERE tm.team_id = p_team_id
|
||||
AND tm.is_observable = true
|
||||
AND tm.observable_consent_status = 'approved'
|
||||
AND tm.status = 'accepted'
|
||||
ORDER BY tm.observable_consent_at DESC;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
COMMENT ON FUNCTION get_observable_members IS
|
||||
'Returns list of Observable team members with approved consent status';
|
||||
|
||||
-- ============================================================================
|
||||
-- SECTION 7: Verification
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
observable_count INTEGER;
|
||||
manager_count INTEGER;
|
||||
BEGIN
|
||||
-- Verify Observable columns exist
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'team_memberships'
|
||||
AND column_name = 'is_observable'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'FAILURE: is_observable column not created';
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'team_memberships'
|
||||
AND column_name = 'observable_consent_status'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'FAILURE: observable_consent_status column not created';
|
||||
END IF;
|
||||
|
||||
-- Verify indexes
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE indexname = 'idx_team_memberships_observable'
|
||||
) THEN
|
||||
RAISE EXCEPTION 'FAILURE: idx_team_memberships_observable index not created';
|
||||
END IF;
|
||||
|
||||
-- Count Observable members (should be 0 initially)
|
||||
SELECT COUNT(*) INTO observable_count
|
||||
FROM team_memberships
|
||||
WHERE is_observable = true;
|
||||
|
||||
RAISE NOTICE 'SUCCESS: Observable columns added (current Observable members: %)', observable_count;
|
||||
RAISE NOTICE 'SUCCESS: team_permission constraint updated to support manager role';
|
||||
RAISE NOTICE 'SUCCESS: Indexes created for Observable queries';
|
||||
RAISE NOTICE 'Migration T005 completed successfully!';
|
||||
END $$;
|
||||
60
scripts/postgresql/migrations/T006_auth_logs.sql
Normal file
60
scripts/postgresql/migrations/T006_auth_logs.sql
Normal file
@@ -0,0 +1,60 @@
|
||||
-- Migration: T006_auth_logs
|
||||
-- Description: Add authentication logging for user logins, logouts, and failed attempts
|
||||
-- Date: 2025-11-17
|
||||
-- Issue: #152
|
||||
|
||||
-- This migration creates the auth_logs table to track authentication events
|
||||
-- for observability and security auditing purposes.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Apply to existing tenant schemas
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_schema TEXT;
|
||||
BEGIN
|
||||
FOR tenant_schema IN
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
LOOP
|
||||
-- Create auth_logs table
|
||||
EXECUTE format('
|
||||
CREATE TABLE IF NOT EXISTS %I.auth_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL CHECK (event_type IN (''login'', ''logout'', ''failed_login'')),
|
||||
success BOOLEAN NOT NULL DEFAULT true,
|
||||
failure_reason TEXT,
|
||||
ip_address TEXT,
|
||||
user_agent TEXT,
|
||||
tenant_domain TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT ''{}''::jsonb
|
||||
)', tenant_schema);
|
||||
|
||||
-- Create indexes
|
||||
EXECUTE format('CREATE INDEX IF NOT EXISTS idx_auth_logs_user_id ON %I.auth_logs(user_id)', tenant_schema);
|
||||
EXECUTE format('CREATE INDEX IF NOT EXISTS idx_auth_logs_email ON %I.auth_logs(email)', tenant_schema);
|
||||
EXECUTE format('CREATE INDEX IF NOT EXISTS idx_auth_logs_event_type ON %I.auth_logs(event_type)', tenant_schema);
|
||||
EXECUTE format('CREATE INDEX IF NOT EXISTS idx_auth_logs_created_at ON %I.auth_logs(created_at DESC)', tenant_schema);
|
||||
EXECUTE format('CREATE INDEX IF NOT EXISTS idx_auth_logs_tenant_domain ON %I.auth_logs(tenant_domain)', tenant_schema);
|
||||
EXECUTE format('CREATE INDEX IF NOT EXISTS idx_auth_logs_event_created ON %I.auth_logs(event_type, created_at DESC)', tenant_schema);
|
||||
|
||||
RAISE NOTICE 'Applied T006_auth_logs migration to schema: %', tenant_schema;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verification query
|
||||
SELECT
|
||||
n.nspname AS schema_name,
|
||||
c.relname AS table_name,
|
||||
pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size
|
||||
FROM pg_class c
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relname = 'auth_logs'
|
||||
AND n.nspname LIKE 'tenant_%'
|
||||
ORDER BY n.nspname;
|
||||
61
scripts/postgresql/migrations/T007_optimize_queries.sql
Normal file
61
scripts/postgresql/migrations/T007_optimize_queries.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
-- T007_optimize_queries.sql
|
||||
-- Phase 1 Performance Optimization: Composite Indexes
|
||||
-- Creates composite indexes for common query patterns to improve performance
|
||||
-- Estimated improvement: 60-80% faster conversation and message queries
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Apply to all existing tenant schemas
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_schema TEXT;
|
||||
BEGIN
|
||||
FOR tenant_schema IN
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
LOOP
|
||||
-- Composite index for message queries
|
||||
-- Optimizes: SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at
|
||||
-- Common in: conversation_service.get_messages() with pagination
|
||||
-- Impact: Covers both filter and sort in single index scan
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation_created
|
||||
ON %I.messages
|
||||
USING btree (conversation_id, created_at ASC)
|
||||
', tenant_schema);
|
||||
|
||||
-- Composite index for conversation list queries
|
||||
-- Optimizes: SELECT * FROM conversations WHERE user_id = ? AND is_archived = false ORDER BY updated_at DESC
|
||||
-- Common in: conversation_service.list_conversations()
|
||||
-- Impact: Enables index-only scan for conversation lists
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_user_updated
|
||||
ON %I.conversations
|
||||
USING btree (user_id, is_archived, updated_at DESC)
|
||||
', tenant_schema);
|
||||
|
||||
RAISE NOTICE 'Applied T007 optimization indexes to schema: %', tenant_schema;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Performance Notes:
|
||||
-- - Both indexes support common access patterns in the application
|
||||
-- - No schema changes - purely additive optimization
|
||||
-- - Safe to run multiple times (IF NOT EXISTS)
|
||||
-- - Note: CONCURRENTLY cannot be used inside DO $$ blocks
|
||||
--
|
||||
-- Rollback (if needed):
|
||||
-- DO $$
|
||||
-- DECLARE tenant_schema TEXT;
|
||||
-- BEGIN
|
||||
-- FOR tenant_schema IN
|
||||
-- SELECT schema_name FROM information_schema.schemata
|
||||
-- WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
-- LOOP
|
||||
-- EXECUTE format('DROP INDEX IF EXISTS %I.idx_messages_conversation_created', tenant_schema);
|
||||
-- EXECUTE format('DROP INDEX IF EXISTS %I.idx_conversations_user_updated', tenant_schema);
|
||||
-- END LOOP;
|
||||
-- END $$;
|
||||
@@ -0,0 +1,73 @@
|
||||
-- T008_add_performance_indexes.sql
|
||||
-- Performance optimization: Add missing FK indexes for agents, datasets, and team shares
|
||||
-- Fixes: GitHub Issue #173 - Database Optimizations
|
||||
-- Impact: 60-80% faster API response times by eliminating full table scans
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Apply to all existing tenant schemas
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_schema TEXT;
|
||||
BEGIN
|
||||
FOR tenant_schema IN
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
LOOP
|
||||
-- Index for conversations.agent_id FK
|
||||
-- Optimizes: Queries filtering/joining conversations by agent
|
||||
-- Common in: agent_service.py aggregations, dashboard stats
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_conversations_agent_id
|
||||
ON %I.conversations
|
||||
USING btree (agent_id)
|
||||
', tenant_schema);
|
||||
|
||||
-- Index for documents.dataset_id FK
|
||||
-- Optimizes: Queries filtering documents by dataset
|
||||
-- Common in: dataset_service.py stats, document counts per dataset
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_documents_dataset_id
|
||||
ON %I.documents
|
||||
USING btree (dataset_id)
|
||||
', tenant_schema);
|
||||
|
||||
-- Composite index for team_resource_shares lookup
|
||||
-- Optimizes: get_resource_teams() queries by resource type and ID
|
||||
-- Fixes N+1: Enables batch lookups for agent/dataset team shares
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_team_resource_shares_lookup
|
||||
ON %I.team_resource_shares
|
||||
USING btree (resource_type, resource_id)
|
||||
', tenant_schema);
|
||||
|
||||
RAISE NOTICE 'Applied T008 performance indexes to schema: %', tenant_schema;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Performance Notes:
|
||||
-- - idx_conversations_agent_id: Required for agent-to-conversation joins
|
||||
-- - idx_documents_dataset_id: Required for dataset-to-document joins
|
||||
-- - idx_team_resource_shares_lookup: Enables batch team share lookups
|
||||
-- - All indexes are additive (IF NOT EXISTS) - safe to run multiple times
|
||||
--
|
||||
-- Expected impact at scale:
|
||||
-- - 1,000 users: 50-100ms queries → 5-15ms
|
||||
-- - 10,000 users: 500-1500ms queries → 20-80ms
|
||||
--
|
||||
-- Rollback (if needed):
|
||||
-- DO $$
|
||||
-- DECLARE tenant_schema TEXT;
|
||||
-- BEGIN
|
||||
-- FOR tenant_schema IN
|
||||
-- SELECT schema_name FROM information_schema.schemata
|
||||
-- WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
-- LOOP
|
||||
-- EXECUTE format('DROP INDEX IF EXISTS %I.idx_conversations_agent_id', tenant_schema);
|
||||
-- EXECUTE format('DROP INDEX IF EXISTS %I.idx_documents_dataset_id', tenant_schema);
|
||||
-- EXECUTE format('DROP INDEX IF EXISTS %I.idx_team_resource_shares_lookup', tenant_schema);
|
||||
-- END LOOP;
|
||||
-- END $$;
|
||||
143
scripts/postgresql/migrations/T009_tenant_scoped_categories.sql
Normal file
143
scripts/postgresql/migrations/T009_tenant_scoped_categories.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
-- T009_tenant_scoped_categories.sql
|
||||
-- Tenant-Scoped Editable/Deletable Agent Categories
|
||||
-- Issue: #215 - FR: Editable/Deletable Default Agent Categories
|
||||
--
|
||||
-- Changes:
|
||||
-- 1. Creates categories table in each tenant schema
|
||||
-- 2. Seeds default categories (General, Coding, Writing, etc.)
|
||||
-- 3. Migrates existing per-user custom categories to tenant-scoped
|
||||
--
|
||||
-- Rollback: See bottom of file
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- Apply to all existing tenant schemas
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_schema TEXT;
|
||||
BEGIN
|
||||
FOR tenant_schema IN
|
||||
SELECT schema_name
|
||||
FROM information_schema.schemata
|
||||
WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
LOOP
|
||||
-- Create categories table
|
||||
EXECUTE format('
|
||||
CREATE TABLE IF NOT EXISTS %I.categories (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
icon VARCHAR(10),
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_by UUID,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_deleted BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_categories_tenant FOREIGN KEY (tenant_id)
|
||||
REFERENCES %I.tenants(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_categories_created_by FOREIGN KEY (created_by)
|
||||
REFERENCES %I.users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT uq_categories_tenant_slug UNIQUE (tenant_id, slug)
|
||||
)
|
||||
', tenant_schema, tenant_schema, tenant_schema);
|
||||
|
||||
-- Create indexes
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_categories_tenant_id
|
||||
ON %I.categories(tenant_id)
|
||||
', tenant_schema);
|
||||
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_categories_slug
|
||||
ON %I.categories(tenant_id, slug)
|
||||
', tenant_schema);
|
||||
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_categories_created_by
|
||||
ON %I.categories(created_by)
|
||||
', tenant_schema);
|
||||
|
||||
EXECUTE format('
|
||||
CREATE INDEX IF NOT EXISTS idx_categories_is_deleted
|
||||
ON %I.categories(is_deleted) WHERE is_deleted = FALSE
|
||||
', tenant_schema);
|
||||
|
||||
-- Seed default categories for each tenant in this schema
|
||||
EXECUTE format('
|
||||
INSERT INTO %I.categories (tenant_id, name, slug, description, icon, is_default, sort_order)
|
||||
SELECT
|
||||
t.id,
|
||||
c.name,
|
||||
c.slug,
|
||||
c.description,
|
||||
c.icon,
|
||||
TRUE,
|
||||
c.sort_order
|
||||
FROM %I.tenants t
|
||||
CROSS JOIN (VALUES
|
||||
(''General'', ''general'', ''All-purpose agent for various tasks'', NULL, 10),
|
||||
(''Coding'', ''coding'', ''Programming and development assistance'', NULL, 20),
|
||||
(''Writing'', ''writing'', ''Content creation and editing'', NULL, 30),
|
||||
(''Analysis'', ''analysis'', ''Data analysis and insights'', NULL, 40),
|
||||
(''Creative'', ''creative'', ''Creative projects and brainstorming'', NULL, 50),
|
||||
(''Research'', ''research'', ''Research and fact-checking'', NULL, 60),
|
||||
(''Business'', ''business'', ''Business strategy and operations'', NULL, 70),
|
||||
(''Education'', ''education'', ''Teaching and learning assistance'', NULL, 80)
|
||||
) AS c(name, slug, description, icon, sort_order)
|
||||
ON CONFLICT (tenant_id, slug) DO NOTHING
|
||||
', tenant_schema, tenant_schema);
|
||||
|
||||
-- Migrate existing per-user custom categories from users.preferences
|
||||
-- Custom categories are stored as: preferences->'custom_categories' = [{"name": "...", "description": "..."}, ...]
|
||||
EXECUTE format('
|
||||
INSERT INTO %I.categories (tenant_id, name, slug, description, created_by, is_default, sort_order)
|
||||
SELECT DISTINCT ON (u.tenant_id, lower(regexp_replace(cc.name, ''[^a-zA-Z0-9]+'', ''-'', ''g'')))
|
||||
u.tenant_id,
|
||||
cc.name,
|
||||
lower(regexp_replace(cc.name, ''[^a-zA-Z0-9]+'', ''-'', ''g'')),
|
||||
COALESCE(cc.description, ''Custom category''),
|
||||
u.id,
|
||||
FALSE,
|
||||
100 + ROW_NUMBER() OVER (PARTITION BY u.tenant_id ORDER BY cc.name)
|
||||
FROM %I.users u
|
||||
CROSS JOIN LATERAL jsonb_array_elements(
|
||||
COALESCE(u.preferences->''custom_categories'', ''[]''::jsonb)
|
||||
) AS cc_json
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
cc_json->>''name'' AS name,
|
||||
cc_json->>''description'' AS description
|
||||
) AS cc
|
||||
WHERE cc.name IS NOT NULL AND cc.name != ''''
|
||||
ON CONFLICT (tenant_id, slug) DO NOTHING
|
||||
', tenant_schema, tenant_schema);
|
||||
|
||||
RAISE NOTICE 'Applied T009 categories table to schema: %', tenant_schema;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verification query (run manually):
|
||||
-- SELECT schema_name,
|
||||
-- (SELECT COUNT(*) FROM information_schema.tables
|
||||
-- WHERE table_schema = s.schema_name AND table_name = 'categories') as has_categories_table
|
||||
-- FROM information_schema.schemata s
|
||||
-- WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template';
|
||||
|
||||
-- Rollback (if needed):
|
||||
-- DO $$
|
||||
-- DECLARE tenant_schema TEXT;
|
||||
-- BEGIN
|
||||
-- FOR tenant_schema IN
|
||||
-- SELECT schema_name FROM information_schema.schemata
|
||||
-- WHERE schema_name LIKE 'tenant_%' AND schema_name != 'tenant_template'
|
||||
-- LOOP
|
||||
-- EXECUTE format('DROP TABLE IF EXISTS %I.categories CASCADE', tenant_schema);
|
||||
-- RAISE NOTICE 'Dropped categories table from schema: %', tenant_schema;
|
||||
-- END LOOP;
|
||||
-- END $$;
|
||||
88
scripts/postgresql/setup-tenant-tablespaces.sql
Normal file
88
scripts/postgresql/setup-tenant-tablespaces.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- GT 2.0 Tenant Tablespace Setup
|
||||
-- Creates dedicated tablespaces for tenant data isolation on persistent volumes
|
||||
|
||||
-- Create tablespace directory if it doesn't exist (PostgreSQL will create it)
|
||||
-- This tablespace will be on the dedicated tenant persistent volume
|
||||
-- Note: CREATE TABLESPACE cannot be in DO block or EXECUTE, must be top-level SQL
|
||||
-- Note: IF NOT EXISTS not supported until PostgreSQL 16, using conditional with DROP IF EXISTS
|
||||
|
||||
-- Drop and recreate to ensure clean state (safe for init scripts on fresh DB)
|
||||
DROP TABLESPACE IF EXISTS tenant_test_company_ts;
|
||||
CREATE TABLESPACE tenant_test_company_ts LOCATION '/var/lib/postgresql/tablespaces/tenant_test';
|
||||
|
||||
-- Set default tablespace for tenant schema (PostgreSQL doesn't support ALTER SCHEMA SET default_tablespace)
|
||||
-- Instead, we'll set the default for the database connection when needed
|
||||
|
||||
-- Move existing tenant tables to the dedicated tablespace
|
||||
-- This ensures all tenant data is stored on the tenant-specific persistent volume
|
||||
|
||||
-- Move users table
|
||||
ALTER TABLE tenant_test_company.users SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move teams table
|
||||
ALTER TABLE tenant_test_company.teams SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move agents table
|
||||
ALTER TABLE tenant_test_company.agents SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move conversations table
|
||||
ALTER TABLE tenant_test_company.conversations SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move messages table
|
||||
ALTER TABLE tenant_test_company.messages SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move documents table
|
||||
ALTER TABLE tenant_test_company.documents SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move document_chunks table (contains PGVector embeddings)
|
||||
ALTER TABLE tenant_test_company.document_chunks SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move datasets table
|
||||
ALTER TABLE tenant_test_company.datasets SET TABLESPACE tenant_test_company_ts;
|
||||
|
||||
-- Move all indexes to the tenant tablespace as well
|
||||
DO $$
|
||||
DECLARE
|
||||
rec RECORD;
|
||||
BEGIN
|
||||
FOR rec IN
|
||||
SELECT schemaname, indexname, tablename
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
LOOP
|
||||
BEGIN
|
||||
EXECUTE format('ALTER INDEX %I.%I SET TABLESPACE tenant_test_company_ts',
|
||||
rec.schemaname, rec.indexname);
|
||||
RAISE NOTICE 'Moved index %.% to tenant tablespace', rec.schemaname, rec.indexname;
|
||||
EXCEPTION
|
||||
WHEN OTHERS THEN
|
||||
RAISE WARNING 'Failed to move index %.%: %', rec.schemaname, rec.indexname, SQLERRM;
|
||||
END;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
-- Grant permissions for the tablespace
|
||||
GRANT CREATE ON TABLESPACE tenant_test_company_ts TO gt2_tenant_user;
|
||||
|
||||
-- Display tablespace information
|
||||
SELECT
|
||||
spcname as tablespace_name,
|
||||
pg_tablespace_location(oid) as location,
|
||||
pg_size_pretty(pg_tablespace_size(spcname)) as size
|
||||
FROM pg_tablespace
|
||||
WHERE spcname LIKE 'tenant_%';
|
||||
|
||||
-- Display tenant table locations
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
tablespace
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'tenant_test_company'
|
||||
ORDER BY tablename;
|
||||
|
||||
-- Display completion notice
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE 'Tenant tablespace setup completed for test';
|
||||
END $$;
|
||||
71
scripts/postgresql/tenant-extensions.sql
Normal file
71
scripts/postgresql/tenant-extensions.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- GT 2.0 Tenant Cluster Extensions Initialization
|
||||
-- Installs all extensions for tenant database including PGVector
|
||||
-- Requires pgvector/pgvector:pg15 Docker image
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- NOTE: Removed \c gt2_tenants - Docker entrypoint runs this script
|
||||
-- against POSTGRES_DB (gt2_tenants) automatically.
|
||||
|
||||
-- Vector extension for embeddings (PGVector) - Required for RAG/embeddings
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- Full-text search support
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent;
|
||||
|
||||
-- Statistics and monitoring
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
|
||||
|
||||
-- UUID generation
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- JSON support enhancements
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gin";
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gist";
|
||||
|
||||
-- Security extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Verify critical extensions are loaded
|
||||
DO $$
|
||||
DECLARE
|
||||
ext_count INTEGER;
|
||||
BEGIN
|
||||
-- Check vector extension
|
||||
SELECT COUNT(*) INTO ext_count FROM pg_extension WHERE extname = 'vector';
|
||||
IF ext_count = 0 THEN
|
||||
RAISE EXCEPTION 'Vector extension not loaded - PGVector support required for embeddings';
|
||||
ELSE
|
||||
RAISE NOTICE 'Vector extension loaded successfully - PGVector enabled';
|
||||
END IF;
|
||||
|
||||
-- Check pg_trgm extension
|
||||
SELECT COUNT(*) INTO ext_count FROM pg_extension WHERE extname = 'pg_trgm';
|
||||
IF ext_count = 0 THEN
|
||||
RAISE EXCEPTION 'pg_trgm extension not loaded - Full-text search support required';
|
||||
ELSE
|
||||
RAISE NOTICE 'pg_trgm extension loaded successfully - Full-text search enabled';
|
||||
END IF;
|
||||
|
||||
-- Check pg_stat_statements extension
|
||||
SELECT COUNT(*) INTO ext_count FROM pg_extension WHERE extname = 'pg_stat_statements';
|
||||
IF ext_count = 0 THEN
|
||||
RAISE WARNING 'pg_stat_statements extension not loaded - Query monitoring limited';
|
||||
ELSE
|
||||
RAISE NOTICE 'pg_stat_statements extension loaded successfully - Query monitoring enabled';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 TENANT EXTENSIONS SETUP ===';
|
||||
RAISE NOTICE 'Extensions configured in tenant database:';
|
||||
RAISE NOTICE '- gt2_tenants: PGVector + full-text search + monitoring + crypto';
|
||||
RAISE NOTICE 'All critical extensions verified and loaded';
|
||||
RAISE NOTICE '======================================';
|
||||
END $$;
|
||||
26
scripts/postgresql/unified/00-create-databases.sql
Normal file
26
scripts/postgresql/unified/00-create-databases.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- GT 2.0 Admin Database Creation Script
|
||||
-- Creates databases for admin/control panel cluster only
|
||||
-- This MUST run first (00-prefix ensures execution order)
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- Create gt2_admin database for control panel
|
||||
SELECT 'CREATE DATABASE gt2_admin'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'gt2_admin')\gexec
|
||||
|
||||
-- Create gt2_control_panel database for control panel backend
|
||||
SELECT 'CREATE DATABASE gt2_control_panel'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'gt2_control_panel')\gexec
|
||||
|
||||
-- Log database creation completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 ADMIN DATABASE CREATION ===';
|
||||
RAISE NOTICE 'Databases created successfully:';
|
||||
RAISE NOTICE '- gt2_admin (control panel metadata)';
|
||||
RAISE NOTICE '- gt2_control_panel (control panel backend)';
|
||||
RAISE NOTICE 'Note: gt2_tenants created in tenant cluster separately';
|
||||
RAISE NOTICE '======================================';
|
||||
END $$;
|
||||
20
scripts/postgresql/unified/00-create-tenant-database.sql
Normal file
20
scripts/postgresql/unified/00-create-tenant-database.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- GT 2.0 Tenant Database Creation Script
|
||||
-- Creates database for tenant cluster only
|
||||
-- This MUST run first (00-prefix ensures execution order)
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- Create gt2_tenants database for tenant data storage
|
||||
SELECT 'CREATE DATABASE gt2_tenants'
|
||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'gt2_tenants')\gexec
|
||||
|
||||
-- Log database creation completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 TENANT DATABASE CREATION ===';
|
||||
RAISE NOTICE 'Database created successfully:';
|
||||
RAISE NOTICE '- gt2_tenants (tenant data storage with PGVector)';
|
||||
RAISE NOTICE '=======================================';
|
||||
END $$;
|
||||
33
scripts/postgresql/unified/01-create-admin-roles.sql
Normal file
33
scripts/postgresql/unified/01-create-admin-roles.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- GT 2.0 Admin Cluster Role Creation Script
|
||||
-- Creates PostgreSQL roles for admin/control panel cluster
|
||||
-- Runs in admin postgres container only
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- Create admin user for control panel database
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gt2_admin') THEN
|
||||
CREATE ROLE gt2_admin LOGIN PASSWORD 'dev_password_change_in_prod';
|
||||
RAISE NOTICE 'Created gt2_admin role for control panel access';
|
||||
ELSE
|
||||
RAISE NOTICE 'gt2_admin role already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Grant database connection permissions (only on databases that exist in admin container)
|
||||
GRANT CONNECT ON DATABASE gt2_admin TO gt2_admin;
|
||||
GRANT CONNECT ON DATABASE gt2_control_panel TO gt2_admin;
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 ADMIN CLUSTER ROLE CREATION ===';
|
||||
RAISE NOTICE 'Role created: gt2_admin';
|
||||
RAISE NOTICE 'Permissions granted on:';
|
||||
RAISE NOTICE ' - gt2_admin database';
|
||||
RAISE NOTICE ' - gt2_control_panel database';
|
||||
RAISE NOTICE '=========================================';
|
||||
END $$;
|
||||
62
scripts/postgresql/unified/01-create-tenant-roles.sql
Normal file
62
scripts/postgresql/unified/01-create-tenant-roles.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- GT 2.0 Tenant Cluster Role Creation Script
|
||||
-- Creates PostgreSQL roles for tenant cluster (including replication)
|
||||
-- Runs in tenant postgres container only
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- Create replication user for High Availability
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'replicator') THEN
|
||||
CREATE ROLE replicator WITH REPLICATION PASSWORD 'tenant_replicator_dev_password' LOGIN;
|
||||
RAISE NOTICE 'Created replicator role for HA cluster';
|
||||
ELSE
|
||||
RAISE NOTICE 'Replicator role already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create application user for tenant backend connections (legacy)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gt2_app') THEN
|
||||
CREATE ROLE gt2_app LOGIN PASSWORD 'gt2_app_password';
|
||||
RAISE NOTICE 'Created gt2_app role for tenant backend';
|
||||
ELSE
|
||||
RAISE NOTICE 'gt2_app role already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create tenant user for tenant database operations (current)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'gt2_tenant_user') THEN
|
||||
CREATE ROLE gt2_tenant_user LOGIN PASSWORD 'gt2_tenant_dev_password';
|
||||
RAISE NOTICE 'Created gt2_tenant_user role for tenant operations';
|
||||
ELSE
|
||||
RAISE NOTICE 'gt2_tenant_user role already exists';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Set default search_path for gt2_tenant_user role
|
||||
-- This ensures all connections automatically use tenant_test_company schema
|
||||
ALTER ROLE gt2_tenant_user SET search_path TO tenant_test_company, public;
|
||||
|
||||
-- Grant database connection permissions (only on gt2_tenants which exists in tenant container)
|
||||
GRANT CONNECT ON DATABASE gt2_tenants TO gt2_app;
|
||||
GRANT CONNECT ON DATABASE gt2_tenants TO gt2_tenant_user;
|
||||
GRANT CONNECT ON DATABASE gt2_tenants TO replicator;
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 TENANT CLUSTER ROLE CREATION ===';
|
||||
RAISE NOTICE 'Roles created:';
|
||||
RAISE NOTICE ' - replicator (for HA replication)';
|
||||
RAISE NOTICE ' - gt2_app (tenant backend - legacy)';
|
||||
RAISE NOTICE ' - gt2_tenant_user (tenant operations - current)';
|
||||
RAISE NOTICE 'Permissions granted on:';
|
||||
RAISE NOTICE ' - gt2_tenants database';
|
||||
RAISE NOTICE '==========================================';
|
||||
END $$;
|
||||
2962
scripts/postgresql/unified/01-init-control-panel-schema-complete.sql
Normal file
2962
scripts/postgresql/unified/01-init-control-panel-schema-complete.sql
Normal file
File diff suppressed because it is too large
Load Diff
98
scripts/postgresql/unified/02-init-extensions.sql
Normal file
98
scripts/postgresql/unified/02-init-extensions.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- GT 2.0 Unified Extensions Initialization
|
||||
-- Ensures all required extensions are properly configured for all databases
|
||||
-- Run after user creation (02-prefix ensures execution order)
|
||||
|
||||
-- Enable logging (but don't stop on errors for database connections)
|
||||
\set ECHO all
|
||||
|
||||
-- Connect to gt2_tenants database first for PGVector setup
|
||||
\c gt2_tenants
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
-- Vector extension for embeddings (PGVector) - Required for tenant database
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- Full-text search support
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent;
|
||||
|
||||
-- Statistics and monitoring
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
|
||||
|
||||
-- UUID generation (built-in in PostgreSQL 13+, but ensure availability)
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- JSON support enhancements
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gin";
|
||||
CREATE EXTENSION IF NOT EXISTS "btree_gist";
|
||||
|
||||
-- Security extensions
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Connect to control panel database and add required extensions (if it exists)
|
||||
\set ON_ERROR_STOP off
|
||||
\c gt2_control_panel
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
-- Basic extensions for control panel
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Connect to admin database and add required extensions (if it exists)
|
||||
\set ON_ERROR_STOP off
|
||||
\c gt2_admin
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
-- Basic extensions for admin database
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_stat_statements";
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Switch back to tenant database for verification
|
||||
\set ON_ERROR_STOP off
|
||||
\c gt2_tenants
|
||||
\set ON_ERROR_STOP on
|
||||
|
||||
-- Verify critical extensions are loaded
|
||||
DO $$
|
||||
DECLARE
|
||||
ext_count INTEGER;
|
||||
BEGIN
|
||||
-- Check vector extension
|
||||
SELECT COUNT(*) INTO ext_count FROM pg_extension WHERE extname = 'vector';
|
||||
IF ext_count = 0 THEN
|
||||
RAISE EXCEPTION 'Vector extension not loaded - PGVector support required for embeddings';
|
||||
ELSE
|
||||
RAISE NOTICE 'Vector extension loaded successfully - PGVector enabled';
|
||||
END IF;
|
||||
|
||||
-- Check pg_trgm extension
|
||||
SELECT COUNT(*) INTO ext_count FROM pg_extension WHERE extname = 'pg_trgm';
|
||||
IF ext_count = 0 THEN
|
||||
RAISE EXCEPTION 'pg_trgm extension not loaded - Full-text search support required';
|
||||
ELSE
|
||||
RAISE NOTICE 'pg_trgm extension loaded successfully - Full-text search enabled';
|
||||
END IF;
|
||||
|
||||
-- Check pg_stat_statements extension
|
||||
SELECT COUNT(*) INTO ext_count FROM pg_extension WHERE extname = 'pg_stat_statements';
|
||||
IF ext_count = 0 THEN
|
||||
RAISE WARNING 'pg_stat_statements extension not loaded - Query monitoring limited';
|
||||
ELSE
|
||||
RAISE NOTICE 'pg_stat_statements extension loaded successfully - Query monitoring enabled';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE NOTICE '=== GT 2.0 UNIFIED EXTENSIONS SETUP ===';
|
||||
RAISE NOTICE 'Extensions configured in all databases:';
|
||||
RAISE NOTICE '- gt2_tenants: PGVector + full-text + monitoring';
|
||||
RAISE NOTICE '- gt2_control_panel: Basic extensions + crypto';
|
||||
RAISE NOTICE '- gt2_admin: Basic extensions + crypto';
|
||||
RAISE NOTICE 'All critical extensions verified and loaded';
|
||||
RAISE NOTICE '=====================================';
|
||||
END $$;
|
||||
2431
scripts/postgresql/unified/04-init-tenant-schema-complete.sql
Normal file
2431
scripts/postgresql/unified/04-init-tenant-schema-complete.sql
Normal file
File diff suppressed because it is too large
Load Diff
64
scripts/postgresql/unified/05-create-tenant-test-data.sql
Normal file
64
scripts/postgresql/unified/05-create-tenant-test-data.sql
Normal file
@@ -0,0 +1,64 @@
|
||||
-- GT 2.0 Tenant Test Data Creation Script
|
||||
-- Creates test tenant and gtadmin@test.com user in tenant database
|
||||
-- Mirrors the control panel test data for user sync compatibility
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- Create test tenant in tenant schema
|
||||
INSERT INTO tenant_test_company.tenants (
|
||||
domain,
|
||||
name,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'test-company',
|
||||
'HW Workstation Test Deployment',
|
||||
NOW(),
|
||||
NOW()
|
||||
) ON CONFLICT (domain) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Create test super admin user in tenant schema
|
||||
-- Role mapping: super_admin from control panel → 'admin' in tenant database
|
||||
-- This mirrors what sync_user_to_tenant_database() does in control-panel-backend
|
||||
INSERT INTO tenant_test_company.users (
|
||||
email,
|
||||
username,
|
||||
full_name,
|
||||
tenant_id,
|
||||
role,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'gtadmin@test.com',
|
||||
'gtadmin',
|
||||
'GT Admin',
|
||||
(SELECT id FROM tenant_test_company.tenants WHERE domain = 'test-company' LIMIT 1),
|
||||
'admin',
|
||||
NOW(),
|
||||
NOW()
|
||||
) ON CONFLICT (email, tenant_id) DO UPDATE SET
|
||||
username = EXCLUDED.username,
|
||||
full_name = EXCLUDED.full_name,
|
||||
role = EXCLUDED.role,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_count INTEGER;
|
||||
user_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO tenant_count FROM tenant_test_company.tenants WHERE domain = 'test-company';
|
||||
SELECT COUNT(*) INTO user_count FROM tenant_test_company.users WHERE email = 'gtadmin@test.com';
|
||||
|
||||
RAISE NOTICE '=== GT 2.0 TENANT TEST DATA CREATION ===';
|
||||
RAISE NOTICE 'Test tenant created: % (domain: test-company)', tenant_count;
|
||||
RAISE NOTICE 'Test user created: % (email: gtadmin@test.com)', user_count;
|
||||
RAISE NOTICE 'User role: admin (mapped from super_admin)';
|
||||
RAISE NOTICE 'Note: User can now log into tenant app at localhost:3002';
|
||||
RAISE NOTICE '========================================';
|
||||
END $$;
|
||||
245
scripts/postgresql/unified/05-create-test-data.sql
Normal file
245
scripts/postgresql/unified/05-create-test-data.sql
Normal file
@@ -0,0 +1,245 @@
|
||||
-- GT 2.0 Test Data Creation Script
|
||||
-- Creates test tenant and gtadmin@test.com user for development/testing
|
||||
-- This is the ONLY place where the test user should be created
|
||||
|
||||
-- Enable logging
|
||||
\set ON_ERROR_STOP on
|
||||
\set ECHO all
|
||||
|
||||
-- Create test tenant
|
||||
INSERT INTO public.tenants (
|
||||
uuid,
|
||||
name,
|
||||
domain,
|
||||
template,
|
||||
status,
|
||||
max_users,
|
||||
resource_limits,
|
||||
namespace,
|
||||
subdomain,
|
||||
optics_enabled,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'test-tenant-uuid-001',
|
||||
'GT AI OS',
|
||||
'test-company',
|
||||
'enterprise',
|
||||
'active',
|
||||
100,
|
||||
'{"cpu": "4000m", "memory": "8Gi", "storage": "50Gi"}',
|
||||
'gt-test',
|
||||
'test',
|
||||
false, -- Optics disabled by default (enable via Control Panel)
|
||||
NOW(),
|
||||
NOW()
|
||||
) ON CONFLICT (domain) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
template = EXCLUDED.template,
|
||||
status = EXCLUDED.status,
|
||||
max_users = EXCLUDED.max_users,
|
||||
resource_limits = EXCLUDED.resource_limits,
|
||||
namespace = EXCLUDED.namespace,
|
||||
subdomain = EXCLUDED.subdomain,
|
||||
optics_enabled = EXCLUDED.optics_enabled,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Create test super admin user
|
||||
-- Password: Test@123
|
||||
-- Hash generated with: python -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('Test@123'))"
|
||||
INSERT INTO public.users (
|
||||
uuid,
|
||||
email,
|
||||
full_name,
|
||||
hashed_password,
|
||||
user_type,
|
||||
tenant_id,
|
||||
capabilities,
|
||||
is_active,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'test-admin-uuid-001',
|
||||
'gtadmin@test.com',
|
||||
'GT Admin Test User',
|
||||
'$2b$12$otRZHfXz7GJUjA.ULeIc4ev612FSAK3tDcOYZdZCJ219j7WFNjFye',
|
||||
'super_admin',
|
||||
(SELECT id FROM public.tenants WHERE domain = 'test-company'),
|
||||
'[{"resource": "*", "actions": ["*"], "constraints": {}}]',
|
||||
true,
|
||||
NOW(),
|
||||
NOW()
|
||||
) ON CONFLICT (email) DO UPDATE SET
|
||||
hashed_password = EXCLUDED.hashed_password,
|
||||
user_type = EXCLUDED.user_type,
|
||||
tenant_id = EXCLUDED.tenant_id,
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
is_active = EXCLUDED.is_active,
|
||||
updated_at = NOW();
|
||||
|
||||
-- ===================================================================
|
||||
-- MODEL CONFIGURATIONS
|
||||
-- ===================================================================
|
||||
|
||||
-- Insert LLM model configurations
|
||||
INSERT INTO public.model_configs (
|
||||
model_id, name, version, provider, model_type, endpoint,
|
||||
context_window, max_tokens, capabilities,
|
||||
cost_per_million_input, cost_per_million_output,
|
||||
is_active, health_status, request_count, error_count,
|
||||
success_rate, avg_latency_ms,
|
||||
tenant_restrictions, required_capabilities,
|
||||
created_at, updated_at
|
||||
) VALUES
|
||||
-- Groq Llama 3.1 8B Instant (fast, cheap)
|
||||
('llama-3.1-8b-instant', 'Groq Llama 3.1 8b Instant', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
131072, 131072,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
0.05, 0.08, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- Groq Compound AI Search (blended: GPT-OSS-120B + Llama 4 Scout)
|
||||
('groq/compound', 'Groq Compound AI Search', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
131072, 8192,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
0.13, 0.47, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- Groq OpenAI GPT OSS 120B (large OSS)
|
||||
('openai/gpt-oss-120b', 'Groq Open AI GPT OSS 120b', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
131072, 32000,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
0.15, 0.60, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- Groq OpenAI GPT OSS 20B (medium OSS)
|
||||
('openai/gpt-oss-20b', 'Groq Open AI GPT OSS 20b', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
131072, 65536,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
0.075, 0.30, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- Groq Meta Llama 4 Maverick 17B (17Bx128E MoE)
|
||||
('meta-llama/llama-4-maverick-17b-128e-instruct', 'Groq Meta Llama 4 Maverick 17b 128 MOE Instruct', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
131072, 8192,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
0.20, 0.60, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- Moonshot AI Kimi K2 (1T parameters, 256k context)
|
||||
('moonshotai/kimi-k2-instruct-0905', 'Groq Moonshot AI Kimi K2 instruct 0905', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
262144, 16384,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
1.00, 3.00, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- Groq Llama Guard 4 12B (safety/moderation model)
|
||||
('meta-llama/llama-guard-4-12b', 'Groq Llama Guard 4 12B', '1.0', 'groq', 'llm',
|
||||
'https://api.groq.com/openai/v1/chat/completions',
|
||||
131072, 8192,
|
||||
'{"reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false, "multilingual": false}'::json,
|
||||
0.20, 0.20, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW()),
|
||||
|
||||
-- BGE-M3 Multilingual Embedding Model (embeddings, input only)
|
||||
('BAAI/bge-m3', 'BGE-M3 Multilingual Embedding', '1.0', 'external', 'embedding',
|
||||
'http://gentwo-vllm-embeddings:8000/v1/embeddings',
|
||||
8192, 8193,
|
||||
'{"multilingual": true, "reasoning": false, "function_calling": false, "vision": false, "audio": false, "streaming": false}'::json,
|
||||
0.01, 0.00, true, 'unknown', 0, 0, 100, 0,
|
||||
'{"global_access": true}'::json, '[]'::json,
|
||||
NOW(), NOW())
|
||||
|
||||
ON CONFLICT (model_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
version = EXCLUDED.version,
|
||||
provider = EXCLUDED.provider,
|
||||
model_type = EXCLUDED.model_type,
|
||||
endpoint = EXCLUDED.endpoint,
|
||||
context_window = EXCLUDED.context_window,
|
||||
max_tokens = EXCLUDED.max_tokens,
|
||||
capabilities = EXCLUDED.capabilities,
|
||||
cost_per_million_input = EXCLUDED.cost_per_million_input,
|
||||
cost_per_million_output = EXCLUDED.cost_per_million_output,
|
||||
is_active = EXCLUDED.is_active,
|
||||
tenant_restrictions = EXCLUDED.tenant_restrictions,
|
||||
required_capabilities = EXCLUDED.required_capabilities,
|
||||
updated_at = NOW();
|
||||
|
||||
-- ===================================================================
|
||||
-- TENANT MODEL ACCESS
|
||||
-- ===================================================================
|
||||
|
||||
-- Enable all models for test tenant with 10,000 requests/min rate limit
|
||||
INSERT INTO public.tenant_model_configs (
|
||||
tenant_id, model_id, is_enabled, tenant_capabilities,
|
||||
rate_limits, usage_constraints, priority,
|
||||
created_at, updated_at
|
||||
) VALUES
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'llama-3.1-8b-instant', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'groq/compound', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'openai/gpt-oss-120b', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'openai/gpt-oss-20b', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'meta-llama/llama-4-maverick-17b-128e-instruct', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'moonshotai/kimi-k2-instruct-0905', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'meta-llama/llama-guard-4-12b', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW()),
|
||||
|
||||
((SELECT id FROM public.tenants WHERE domain = 'test-company'), 'BAAI/bge-m3', true, '{}'::json,
|
||||
'{"requests_per_minute": 10000}'::json, '{}'::json, 5, NOW(), NOW())
|
||||
|
||||
ON CONFLICT (tenant_id, model_id) DO UPDATE SET
|
||||
is_enabled = EXCLUDED.is_enabled,
|
||||
rate_limits = EXCLUDED.rate_limits,
|
||||
updated_at = NOW();
|
||||
|
||||
-- Log completion
|
||||
DO $$
|
||||
DECLARE
|
||||
tenant_count INTEGER;
|
||||
user_count INTEGER;
|
||||
model_count INTEGER;
|
||||
tenant_model_count INTEGER;
|
||||
BEGIN
|
||||
SELECT COUNT(*) INTO tenant_count FROM public.tenants WHERE domain = 'test-company';
|
||||
SELECT COUNT(*) INTO user_count FROM public.users WHERE email = 'gtadmin@test.com';
|
||||
SELECT COUNT(*) INTO model_count FROM public.model_configs;
|
||||
SELECT COUNT(*) INTO tenant_model_count FROM public.tenant_model_configs WHERE tenant_id = (SELECT id FROM public.tenants WHERE domain = 'test-company');
|
||||
|
||||
RAISE NOTICE '=== GT 2.0 TEST DATA CREATION ===';
|
||||
RAISE NOTICE 'Test tenant created: % (domain: test-company)', tenant_count;
|
||||
RAISE NOTICE 'Test user created: % (email: gtadmin@test.com)', user_count;
|
||||
RAISE NOTICE 'Login credentials:';
|
||||
RAISE NOTICE ' Email: gtadmin@test.com';
|
||||
RAISE NOTICE ' Password: Test@123';
|
||||
RAISE NOTICE '';
|
||||
RAISE NOTICE 'LLM Models configured: %', model_count;
|
||||
RAISE NOTICE 'Tenant model access enabled: %', tenant_model_count;
|
||||
RAISE NOTICE 'Rate limit: 10,000 requests/minute per model';
|
||||
RAISE NOTICE '====================================';
|
||||
END $$;
|
||||
Reference in New Issue
Block a user