diff --git a/api/src/backend/api/migrations/0049_lighthouseproviderconfiguration_and_more.py b/api/src/backend/api/migrations/0049_lighthouseproviderconfiguration_and_more.py index ddcacdae08..0f50d479f0 100644 --- a/api/src/backend/api/migrations/0049_lighthouseproviderconfiguration_and_more.py +++ b/api/src/backend/api/migrations/0049_lighthouseproviderconfiguration_and_more.py @@ -1,6 +1,5 @@ -# Generated by Django 5.1.12 on 2025-09-29 15:52 +# Generated by Django 5.1.12 on 2025-10-09 07:50 -import json import uuid import django.db.models.deletion @@ -10,6 +9,7 @@ import api.rls class Migration(migrations.Migration): + dependencies = [ ("api", "0048_api_key"), ] @@ -45,12 +45,6 @@ class Migration(migrations.Migration): ), ), ("is_active", models.BooleanField(default=True)), - ( - "tenant", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="api.tenant" - ), - ), ], options={ "db_table": "lighthouse_provider_configurations", @@ -73,20 +67,6 @@ class Migration(migrations.Migration): ("updated_at", models.DateTimeField(auto_now=True)), ("model_id", models.CharField(max_length=100)), ("default_parameters", models.JSONField(blank=True, default=dict)), - ( - "provider_configuration", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="available_models", - to="api.lighthouseproviderconfiguration", - ), - ), - ( - "tenant", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="api.tenant" - ), - ), ], options={ "db_table": "lighthouse_provider_models", @@ -109,19 +89,53 @@ class Migration(migrations.Migration): ("updated_at", models.DateTimeField(auto_now=True)), ("business_context", models.TextField(blank=True, default="")), ("default_provider", models.CharField(blank=True, max_length=50)), - ("default_models", models.JSONField(default=dict)), - ( - "tenant", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="api.tenant" - ), - ), + ("default_models", models.JSONField(blank=True, default=dict)), ], options={ "db_table": "lighthouse_tenant_config", "abstract": False, }, ), + migrations.RemoveConstraint( + model_name="tenantapikey", + name="statements_on_tenantapikey", + ), + migrations.AddConstraint( + model_name="tenantapikey", + constraint=api.rls.RowLevelSecurityConstraint( + "tenant_id", name="rls_on_tenantapikey" + ), + ), + migrations.AddField( + model_name="lighthouseproviderconfiguration", + name="tenant", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + migrations.AddField( + model_name="lighthouseprovidermodels", + name="provider_configuration", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="available_models", + to="api.lighthouseproviderconfiguration", + ), + ), + migrations.AddField( + model_name="lighthouseprovidermodels", + name="tenant", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), + migrations.AddField( + model_name="lighthousetenantconfiguration", + name="tenant", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="api.tenant" + ), + ), migrations.AddIndex( model_name="lighthouseproviderconfiguration", index=models.Index( @@ -131,9 +145,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="lighthouseproviderconfiguration", constraint=api.rls.RowLevelSecurityConstraint( - "tenant_id", - name="rls_on_lighthouseproviderconfiguration", - statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + "tenant_id", name="rls_on_lighthouseproviderconfiguration" ), ), migrations.AddConstraint( @@ -153,9 +165,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="lighthouseprovidermodels", constraint=api.rls.RowLevelSecurityConstraint( - "tenant_id", - name="rls_on_lighthouseprovidermodels", - statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + "tenant_id", name="rls_on_lighthouseprovidermodels" ), ), migrations.AddConstraint( @@ -168,9 +178,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="lighthousetenantconfiguration", constraint=api.rls.RowLevelSecurityConstraint( - "tenant_id", - name="rls_on_lighthousetenantconfiguration", - statements=["SELECT", "INSERT", "UPDATE", "DELETE"], + "tenant_id", name="rls_on_lighthousetenantconfiguration" ), ), migrations.AddConstraint( @@ -179,100 +187,4 @@ class Migration(migrations.Migration): fields=("tenant_id",), name="unique_tenant_lighthouse_config" ), ), - # Data backfill from the previous single-table lighthouse configuration to the new schema - migrations.RunPython( - code=lambda apps, schema_editor: _copy_lighthouse_config_to_new_models( - apps, schema_editor - ), - reverse_code=migrations.RunPython.noop, - ), ] - - -def _copy_lighthouse_config_to_new_models(apps, schema_editor): - """Idempotent backfill from old LighthouseConfiguration to new models. - - - No-op if old table doesn't exist or has no rows - - Per-tenant RLS context is set before inserts - - Only OpenAI provider is handled for now (future providers can extend) - """ - # Ensure the old table exists before attempting to read - introspection = schema_editor.connection.introspection - if "lighthouse_configurations" not in introspection.table_names(): - return - - # Historical models - OldConfig = apps.get_model("api", "LighthouseConfiguration") - ProviderCfg = apps.get_model("api", "LighthouseProviderConfiguration") - TenantCfg = apps.get_model("api", "LighthouseTenantConfiguration") - - from django.conf import settings - - try: - from cryptography.fernet import Fernet, InvalidToken - except Exception: - # If cryptography isn't available, skip credentials backfill (schema remains valid) - Fernet = None # type: ignore - InvalidToken = Exception # type: ignore - - fernet = None - if getattr(settings, "SECRETS_ENCRYPTION_KEY", None): - try: - fernet = ( - Fernet(settings.SECRETS_ENCRYPTION_KEY.encode()) if Fernet else None - ) - except Exception: - fernet = None - - SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);" - - # Iterate old configs (expected one per tenant) - with schema_editor.connection.cursor() as cursor: - for old in OldConfig.objects.all().only( - "tenant_id", - "business_context", - "model", - "api_key", - "is_active", - "temperature", - "max_tokens", - ): - # Set RLS context for tenant - cursor.execute(SET_CONFIG_QUERY, ["api.tenant_id", str(old.tenant_id)]) - - # Create or get tenant-level settings - if not TenantCfg.objects.filter(tenant_id=old.tenant_id).exists(): - TenantCfg.objects.create( - tenant_id=old.tenant_id, - business_context=old.business_context or "", - default_provider="openai", - default_models={"openai": old.model}, - ) - - # Create or get provider configuration for OpenAI - provider_cfg = ProviderCfg.objects.filter( - tenant_id=old.tenant_id, provider_type="openai" - ).first() - - if provider_cfg is None: - enc_credentials = None - # Best-effort decrypt old api_key and re-encrypt as JSON credentials - if fernet and getattr(old, "api_key", None): - try: - plaintext = fernet.decrypt(bytes(old.api_key)).decode() - creds_json = json.dumps({"api_key": plaintext}).encode() - enc_credentials = fernet.encrypt(creds_json) - except InvalidToken: - enc_credentials = None - except Exception: - enc_credentials = None - - # Only create provider configuration if we have credentials - if enc_credentials is not None: - provider_cfg = ProviderCfg.objects.create( - tenant_id=old.tenant_id, - provider_type="openai", - base_url=None, - credentials=enc_credentials, - is_active=bool(getattr(old, "is_active", True)), - )