mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat: add lighthouse migration file
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
# Generated by Django 5.1.12 on 2025-09-29 15:52
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("api", "0047_remove_integration_unique_configuration_per_tenant"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LighthouseProviderConfiguration",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"provider_type",
|
||||
models.CharField(
|
||||
choices=[("openai", "OpenAI")],
|
||||
help_text="LLM provider name",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
("base_url", models.URLField(blank=True, null=True)),
|
||||
(
|
||||
"credentials",
|
||||
models.BinaryField(
|
||||
help_text="Encrypted JSON credentials for the provider"
|
||||
),
|
||||
),
|
||||
("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",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LighthouseProviderModels",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("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",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="LighthouseTenantConfiguration",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("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"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "lighthouse_tenant_config",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_type"], name="lh_pc_tenant_type_idx"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id", name="rls_on_lighthouseproviderconfiguration"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseproviderconfiguration",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider_type"),
|
||||
name="unique_provider_config_per_tenant",
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="lighthouseprovidermodels",
|
||||
index=models.Index(
|
||||
fields=["tenant_id", "provider_configuration"],
|
||||
name="lh_prov_models_cfg_idx",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseprovidermodels",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id", name="rls_on_lighthouseprovidermodels"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthouseprovidermodels",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider_configuration", "model_id"),
|
||||
name="unique_provider_model_per_configuration",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthousetenantconfiguration",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id", name="rls_on_lighthousetenantconfiguration"
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="lighthousetenantconfiguration",
|
||||
constraint=models.UniqueConstraint(
|
||||
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)),
|
||||
)
|
||||
Reference in New Issue
Block a user