Resolve provider type conflict

This commit is contained in:
Chandrapal Badshah committed 2025-05-14 14:42:43 +05:30
1 parent a765c1543e
commit 6bb0e2bd8d
54 files changed
+8627 -56

No files matched your search

@@ -0,0 +1,80 @@
# Generated by Django 5.1.7 on 2025-04-10 14:54
import api.rls
import django.core.validators
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0016_finding_compliance_resource_details_and_more"),
]
operations = [
migrations.CreateModel(
name="LighthouseConfig",
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)),
(
"name",
models.CharField(
max_length=100,
validators=[django.core.validators.MinLengthValidator(3)],
),
),
("api_key", models.BinaryField()),
("model", models.CharField(default="gpt-4o", max_length=50)),
("temperature", models.FloatField(default=0.7)),
("max_tokens", models.IntegerField(default=4000)),
(
"business_context",
models.TextField(
blank=True,
help_text="Additional business context for this AI model configuration",
null=True,
),
),
("is_active", models.BooleanField(default=True)),
(
"tenant",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
),
),
],
options={
"db_table": "lighthouse_config",
"abstract": False,
"indexes": [
models.Index(fields=["name"], name="lighthouse_config_name_idx"),
models.Index(
fields=["is_active"], name="lighthouse_config_active_idx"
),
],
"constraints": [
models.UniqueConstraint(
fields=("tenant_id",),
name="unique_lighthouse_config_per_tenant",
),
],
},
),
migrations.AddConstraint(
model_name="lighthouseconfig",
constraint=api.rls.RowLevelSecurityConstraint(
"tenant_id",
name="rls_on_lighthouseconfig",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
),
]
+60 -1
View File
@@ -1274,7 +1274,6 @@ class IntegrationProviderRelationship(RowLevelSecurityProtectedModel):
),
]
class ResourceScanSummary(RowLevelSecurityProtectedModel):
scan_id = models.UUIDField(default=uuid7, db_index=True)
resource_id = models.UUIDField(default=uuid4, db_index=True)
@@ -1322,3 +1321,63 @@ class ResourceScanSummary(RowLevelSecurityProtectedModel):
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
]
class LighthouseConfig(RowLevelSecurityProtectedModel):
"""
Stores configuration and API keys for LLM services.
"""
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
updated_at = models.DateTimeField(auto_now=True, editable=False)
name = models.CharField(max_length=100, validators=[MinLengthValidator(3)])
api_key = models.BinaryField(blank=False, null=False)
model = models.CharField(max_length=50, default="gpt-4o")
temperature = models.FloatField(default=0.7)
max_tokens = models.IntegerField(default=4000)
business_context = models.TextField(
blank=True,
null=True,
help_text="Additional business context for this AI model configuration",
)
is_active = models.BooleanField(default=True)
def __str__(self):
return self.name
@property
def api_key_decoded(self):
"""Return the decrypted API key."""
try:
return fernet.decrypt(self.api_key).decode()
except Exception:
return None
@api_key_decoded.setter
def api_key_decoded(self, value):
"""Store the encrypted API key."""
self.api_key = fernet.encrypt(value.encode())
class Meta(RowLevelSecurityProtectedModel.Meta):
db_table = "lighthouse_config"
constraints = [
RowLevelSecurityConstraint(
field="tenant_id",
name="rls_on_%(class)s",
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
),
# Add unique constraint for name within a tenant
models.UniqueConstraint(
fields=["tenant_id"], name="unique_lighthouse_config_per_tenant"
),
]
indexes = [
models.Index(fields=["name"], name="lighthouse_config_name_idx"),
models.Index(fields=["is_active"], name="lighthouse_config_active_idx"),
]
class JSONAPIMeta:
resource_name = "lighthouse-config"
+3 -1
View File
@@ -87,7 +87,9 @@ class RowLevelSecurityConstraint(models.BaseConstraint):
f"{grant_queries}{self.grant_sql_query.format(statement=statement)}"
)
full_create_sql_query = f"{self.rls_sql_query}{policy_queries}{grant_queries}"
full_create_sql_query = (
f"{self.rls_sql_query}" f"{policy_queries}" f"{grant_queries}"
)
table_name = model._meta.db_table
if self.partition_name:
@@ -296,5 +296,5 @@ class TestTokenSwitchTenant:
assert invalid_tenant_response.status_code == 400
assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid"
assert invalid_tenant_response.json()["errors"][0]["detail"] == (
"Tenant does not exist or user is not a " "member."
"Tenant does not exist or user is not a member."
)
+100
View File
@@ -20,6 +20,7 @@ from api.models import (
IntegrationProviderRelationship,
Invitation,
InvitationRoleRelationship,
LighthouseConfig,
Membership,
Provider,
ProviderGroup,
@@ -2119,3 +2120,102 @@ class IntegrationUpdateSerializer(BaseWriteIntegrationSerializer):
IntegrationProviderRelationship.objects.bulk_create(new_relationships)
return super().update(instance, validated_data)
class LighthouseConfigSerializer(RLSSerializer):
"""
Serializer for the LighthouseConfig model.
"""
api_key = serializers.CharField(write_only=True, required=False)
class Meta:
model = LighthouseConfig
fields = [
"id",
"name",
"api_key",
"model",
"temperature",
"max_tokens",
"business_context",
"is_active",
"inserted_at",
"updated_at",
"url",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
}
def to_representation(self, instance):
ret = super().to_representation(instance)
# Add back the masked API key for display
ret["api_key"] = "*" * len(instance.api_key) if instance.api_key else None
return ret
class LighthouseConfigCreateSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for creating LighthouseConfig instances.
"""
api_key = serializers.CharField(write_only=True)
class Meta:
model = LighthouseConfig
fields = [
"name",
"api_key",
"model",
"temperature",
"max_tokens",
"business_context",
"is_active",
]
def create(self, validated_data):
api_key = validated_data.pop("api_key", None)
instance = super().create(validated_data)
if api_key:
instance.api_key_decoded = api_key
instance.save(update_fields=["api_key"])
return instance
class LighthouseConfigUpdateSerializer(BaseWriteSerializer):
"""
Serializer for updating LighthouseConfig instances.
"""
api_key = serializers.CharField(write_only=True, required=False)
class Meta:
model = LighthouseConfig
fields = [
"id",
"name",
"api_key",
"model",
"temperature",
"max_tokens",
"business_context",
"is_active",
]
extra_kwargs = {
"id": {"read_only": True},
"name": {"required": False},
"model": {"required": False},
"temperature": {"required": False},
"max_tokens": {"required": False},
}
def update(self, instance, validated_data):
api_key = validated_data.pop("api_key", None)
instance = super().update(instance, validated_data)
if api_key:
instance.api_key_decoded = api_key
instance.save(update_fields=["api_key"])
return instance
+4
View File
@@ -3,6 +3,7 @@ from drf_spectacular.views import SpectacularRedocView
from rest_framework_nested import routers
from api.v1.views import (
LighthouseConfigViewSet,
ComplianceOverviewViewSet,
CustomTokenObtainView,
CustomTokenRefreshView,
@@ -49,6 +50,9 @@ router.register(
router.register(r"overviews", OverviewViewSet, basename="overview")
router.register(r"schedules", ScheduleViewSet, basename="schedule")
router.register(r"integrations", IntegrationViewSet, basename="integration")
router.register(
r"lighthouse-config", LighthouseConfigViewSet, basename="lighthouseconfig"
)
tenants_router = routers.NestedSimpleRouter(router, r"tenants", lookup="tenant")
tenants_router.register(
+134
View File
@@ -2,6 +2,7 @@ import glob
import os
from datetime import datetime, timedelta, timezone
import openai
import sentry_sdk
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
@@ -12,7 +13,9 @@ from config.settings.social_login import (
GITHUB_OAUTH_CALLBACK_URL,
GOOGLE_OAUTH_CALLBACK_URL,
)
from cryptography.fernet import Fernet
from dj_rest_auth.registration.views import SocialLoginView
from django.conf import settings
from django.conf import settings as django_settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.search import SearchQuery
@@ -83,6 +86,7 @@ from api.models import (
Finding,
Integration,
Invitation,
LighthouseConfig,
Membership,
Provider,
ProviderGroup,
@@ -124,6 +128,9 @@ from api.v1.serializers import (
InvitationCreateSerializer,
InvitationSerializer,
InvitationUpdateSerializer,
LighthouseConfigCreateSerializer,
LighthouseConfigSerializer,
LighthouseConfigUpdateSerializer,
MembershipSerializer,
OverviewFindingSerializer,
OverviewProviderSerializer,
@@ -2773,3 +2780,130 @@ class IntegrationViewSet(BaseRLSViewSet):
context = super().get_serializer_context()
context["allowed_providers"] = self.allowed_providers
return context
class LighthouseConfigViewSet(BaseRLSViewSet):
"""
API endpoint for managing OpenAI API configuration.
"""
filterset_fields = {
"name": ["exact", "icontains"],
"model": ["exact", "icontains"],
"is_active": ["exact"],
"inserted_at": ["gte", "lte"],
}
ordering_fields = ["name", "inserted_at", "updated_at", "is_active"]
ordering = ["-inserted_at"]
def get_queryset(self):
return LighthouseConfig.objects.all()
def get_serializer_class(self):
if self.action == "create":
return LighthouseConfigCreateSerializer
elif self.action in ["update", "partial_update"]:
return LighthouseConfigUpdateSerializer
return LighthouseConfigSerializer
@action(detail=True, methods=["get"])
def check_connection(self, request, pk=None):
"""
Check the connection to the OpenAI API.
"""
instance = self.get_object()
if not instance.api_key_decoded:
return Response(
{"detail": "API key is invalid or missing."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
client = openai.OpenAI(
api_key=instance.api_key_decoded,
)
models = client.models.list()
return Response(
{
"detail": "Connection successful!",
"available_models": [model.id for model in models.data],
},
status=status.HTTP_200_OK,
)
except Exception as e:
return Response(
{"detail": f"Connection failed: {str(e)}"},
status=status.HTTP_400_BAD_REQUEST,
)
def create(self, request, *args, **kwargs):
"""Create new AI configuration"""
if LighthouseConfig.objects.filter(tenant_id=request.tenant_id).exists():
return Response(
{"detail": "AI configuration already exists. Use PUT to update."},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(
LighthouseConfigSerializer(
instance, context=self.get_serializer_context()
).data,
status=status.HTTP_201_CREATED,
headers=headers,
)
def update(self, request, *args, **kwargs):
"""Update existing AI configuration"""
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
return Response(
LighthouseConfigSerializer(
instance, context=self.get_serializer_context()
).data
)
@action(detail=True, methods=["get"], url_path="show_key")
def show_key(self, request, pk=None):
"""
Return the decrypted API key for the specified AI configuration.
"""
instance = self.get_object()
try:
# Handle different binary formats appropriately
if isinstance(instance.api_key, memoryview):
encrypted_bytes = instance.api_key.tobytes()
elif isinstance(instance.api_key, str):
encrypted_bytes = instance.api_key.encode()
else:
encrypted_bytes = instance.api_key
key = settings.SECRETS_ENCRYPTION_KEY.encode()
fernet_instance = Fernet(key)
decrypted_key = fernet_instance.decrypt(encrypted_bytes).decode()
return Response(
{"api_key": decrypted_key},
status=status.HTTP_200_OK,
)
except Exception:
return Response(
{"detail": "API key is invalid or missing."},
status=status.HTTP_400_BAD_REQUEST,
)
def destroy(self, request, *args, **kwargs):
"""Delete AI configuration"""
instance = self.get_object()
# Perform deletion
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)