mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-28 06:14:20 +00:00
Resolve provider type conflict
This commit is contained in:
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"],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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."
|
||||
)
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user