Files
prowler/src/backend/api/v1/serializers.py
T
Víctor Fernández Poyatos 6d69a192f3 fix(Finding, Resource): PRWLR-5057 Fix include query parameter for /findings and /resources (#55)
* fix(Finding, Resource): PRWLR-5057 fix include query parameter

* fix(Finding, Resource): PRWLR-5057 optimize requests

* test(Finding, Resource): PRWLR-5057 add unit tests for include
2024-10-17 18:07:06 +02:00

761 lines
21 KiB
Python

import json
from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.password_validation import validate_password
from drf_spectacular.utils import extend_schema_field
from jwt.exceptions import InvalidKeyError
from rest_framework_json_api import serializers
from rest_framework_json_api.serializers import ValidationError
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.tokens import RefreshToken
from api.models import (
StateChoices,
User,
Membership,
Provider,
Scan,
Task,
Resource,
ResourceTag,
Finding,
ProviderSecret,
)
from api.rls import Tenant
from api.utils import merge_dicts
# Tokens
class TokenSerializer(serializers.Serializer):
email = serializers.EmailField(write_only=True)
password = serializers.CharField(write_only=True)
tenant_id = serializers.UUIDField(
write_only=True,
required=False,
help_text="If not provided, the tenant ID of the first membership that was added"
" to the user will be used.",
)
# Output tokens
refresh = serializers.CharField(read_only=True)
access = serializers.CharField(read_only=True)
class JSONAPIMeta:
resource_name = "Token"
def validate(self, attrs):
email = attrs.get("email")
password = attrs.get("password")
tenant_id = str(attrs.get("tenant_id", ""))
# Authenticate user
user = authenticate(username=email, password=password)
if user is None:
raise ValidationError("Invalid credentials")
if tenant_id:
if not user.is_member_of_tenant(tenant_id):
raise ValidationError("Tenant does not exist or user is not a member.")
else:
first_membership = user.memberships.order_by("date_joined").first()
if first_membership is None:
raise ValidationError("User has no memberships.")
tenant_id = str(first_membership.tenant_id)
# Generate tokens
try:
refresh = RefreshToken.for_user(user)
refresh["tenant_id"] = tenant_id
access = refresh.access_token
except InvalidKeyError:
# Handle invalid key error
raise ValidationError(
{
"detail": "Token generation failed due to invalid key configuration. Provide valid "
"DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment."
}
)
except Exception as e:
raise ValidationError({"detail": str(e)})
return {"access": str(access), "refresh": str(refresh)}
class TokenRefreshSerializer(serializers.Serializer):
refresh = serializers.CharField()
# Output token
access = serializers.CharField(read_only=True)
class JSONAPIMeta:
resource_name = "TokenRefresh"
def validate(self, attrs):
refresh_token = attrs.get("refresh")
try:
# Validate the refresh token
refresh = RefreshToken(refresh_token)
# Generate new access token
access_token = refresh.access_token
if settings.SIMPLE_JWT["ROTATE_REFRESH_TOKENS"]:
if settings.SIMPLE_JWT["BLACKLIST_AFTER_ROTATION"]:
try:
refresh.blacklist()
except AttributeError:
pass
refresh.set_jti()
refresh.set_exp()
refresh.set_iat()
return {"access": str(access_token), "refresh": str(refresh)}
except TokenError:
raise ValidationError({"refresh": "Invalid or expired token"})
# Base
class BaseSerializerV1(serializers.ModelSerializer):
def get_root_meta(self, _resource, _many):
return {"version": "v1"}
class BaseWriteSerializer(BaseSerializerV1):
def validate(self, data):
if hasattr(self, "initial_data"):
initial_data = set(self.initial_data.keys()) - {"id", "type"}
unknown_keys = initial_data - set(self.fields.keys())
if unknown_keys:
raise ValidationError(f"Invalid fields: {unknown_keys}")
return data
class RLSSerializer(BaseSerializerV1):
def create(self, validated_data):
tenant_id = self.context.get("tenant_id")
validated_data["tenant_id"] = tenant_id
return super().create(validated_data)
class StateEnumSerializerField(serializers.ChoiceField):
def __init__(self, **kwargs):
kwargs["choices"] = StateChoices.choices
super().__init__(**kwargs)
# Users
class UserSerializer(BaseSerializerV1):
"""
Serializer for the User model.
"""
memberships = serializers.ResourceRelatedField(many=True, read_only=True)
class Meta:
model = User
fields = ["id", "name", "email", "company_name", "date_joined", "memberships"]
class UserCreateSerializer(BaseWriteSerializer):
password = serializers.CharField(write_only=True)
company_name = serializers.CharField(required=False)
class Meta:
model = User
fields = ["name", "password", "email", "company_name"]
def validate_password(self, value):
user = User(**{k: v for k, v in self.initial_data.items() if k != "type"})
validate_password(value, user=user)
return value
def create(self, validated_data):
password = validated_data.pop("password")
user = User(**validated_data)
validate_password(password, user=user)
user.set_password(password)
user.save()
return user
class UserUpdateSerializer(BaseWriteSerializer):
password = serializers.CharField(write_only=True, required=False)
class Meta:
model = User
fields = ["id", "name", "password", "email", "company_name"]
extra_kwargs = {
"id": {"read_only": True},
}
def validate_password(self, value):
validate_password(value, user=self.instance)
return value
def update(self, instance, validated_data):
password = validated_data.pop("password", None)
if password:
validate_password(password, user=instance)
instance.set_password(password)
return super().update(instance, validated_data)
# Tasks
class TaskBase(serializers.ModelSerializer):
state_mapping = {
"PENDING": StateChoices.AVAILABLE,
"STARTED": StateChoices.EXECUTING,
"PROGRESS": StateChoices.EXECUTING,
"SUCCESS": StateChoices.COMPLETED,
"FAILURE": StateChoices.FAILED,
"REVOKED": StateChoices.CANCELLED,
}
class Meta:
fields = ["id"]
model = Task
def map_state(self, task_result_state):
return self.state_mapping.get(task_result_state, StateChoices.AVAILABLE)
@extend_schema_field(
{
"type": "string",
"enum": StateChoices.values,
}
)
def get_state(self, obj):
task_result_state = (
obj.task_runner_task.status if obj.task_runner_task else None
)
return self.map_state(task_result_state)
class TaskSerializer(RLSSerializer, TaskBase):
state = serializers.SerializerMethodField(read_only=True)
metadata = serializers.SerializerMethodField(read_only=True)
result = serializers.SerializerMethodField(read_only=True)
task_args = serializers.SerializerMethodField(read_only=True)
completed_at = serializers.DateTimeField(
source="task_runner_task.date_done", read_only=True
)
name = serializers.CharField(source="task_runner_task.task_name", read_only=True)
class Meta:
model = Task
fields = [
"id",
"inserted_at",
"completed_at",
"name",
"state",
"result",
"task_args",
"metadata",
]
@extend_schema_field(serializers.JSONField())
def get_metadata(self, obj):
return self.get_json_field(obj, "metadata")
@extend_schema_field(serializers.JSONField())
def get_result(self, obj):
return self.get_json_field(obj, "result")
@extend_schema_field(serializers.JSONField())
def get_task_args(self, obj):
task_args = self.get_json_field(obj, "task_kwargs")
# Celery task_kwargs are stored as a double string JSON in the database when not empty
if isinstance(task_args, str):
task_args = json.loads(task_args.replace("'", '"'))
# Remove tenant_id from task_kwargs if present
task_args.pop("tenant_id", None)
return task_args
@staticmethod
def get_json_field(obj, field_name):
"""Helper method to DRY the logic for loading JSON fields from task_runner_task."""
task_result_field = (
getattr(obj.task_runner_task, field_name, None)
if obj.task_runner_task
else None
)
return json.loads(task_result_field) if task_result_field else {}
# Tenants
class TenantSerializer(BaseSerializerV1):
"""
Serializer for the Tenant model.
"""
memberships = serializers.ResourceRelatedField(many=True, read_only=True)
class Meta:
model = Tenant
fields = ["id", "name", "memberships"]
# Memberships
class MemberRoleEnumSerializerField(serializers.ChoiceField):
def __init__(self, **kwargs):
kwargs["choices"] = Membership.RoleChoices.choices
super().__init__(**kwargs)
class MembershipSerializer(serializers.ModelSerializer):
role = MemberRoleEnumSerializerField()
user = serializers.HyperlinkedRelatedField(view_name="user-detail", read_only=True)
tenant = serializers.HyperlinkedRelatedField(
view_name="tenant-detail", read_only=True
)
class Meta:
model = Membership
fields = ["id", "user", "tenant", "role", "date_joined"]
# Providers
class ProviderEnumSerializerField(serializers.ChoiceField):
def __init__(self, **kwargs):
kwargs["choices"] = Provider.ProviderChoices.choices
super().__init__(**kwargs)
class ProviderSerializer(RLSSerializer):
"""
Serializer for the Provider model.
"""
provider = ProviderEnumSerializerField()
connection = serializers.SerializerMethodField(read_only=True)
class Meta:
model = Provider
fields = [
"id",
"inserted_at",
"updated_at",
"provider",
"uid",
"alias",
"connection",
"scanner_args",
"secret",
"url",
]
@extend_schema_field(
{
"type": "object",
"properties": {
"connected": {"type": "boolean"},
"last_checked_at": {"type": "string", "format": "date-time"},
},
}
)
def get_connection(self, obj):
return {
"connected": obj.connected,
"last_checked_at": obj.connection_last_checked_at,
}
class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = Provider
fields = ["alias", "provider", "uid", "scanner_args"]
class ProviderUpdateSerializer(BaseWriteSerializer):
"""
Serializer for updating the Provider model.
Only allows "alias" and "scanner_args" fields to be updated.
"""
class Meta:
model = Provider
fields = ["alias", "scanner_args"]
# Scans
class ScanTriggerEnumSerializerField(serializers.ChoiceField):
def __init__(self, **kwargs):
kwargs["choices"] = Scan.TriggerChoices.choices
super().__init__(**kwargs)
class ScanSerializer(RLSSerializer):
trigger = serializers.ChoiceField(
choices=Scan.TriggerChoices.choices, read_only=True
)
state = StateEnumSerializerField(read_only=True)
class Meta:
model = Scan
fields = [
"id",
"name",
"trigger",
"state",
"unique_resource_count",
"progress",
"scanner_args",
"duration",
"provider",
"task",
"started_at",
"completed_at",
"scheduled_at",
"url",
]
class ScanCreateSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = Scan
# TODO: add mutelist when implemented
fields = ["id", "provider", "scanner_args", "name"]
def create(self, validated_data):
provider = validated_data.get("provider")
if not validated_data.get("scanner_args"):
validated_data["scanner_args"] = provider.scanner_args
else:
validated_data["scanner_args"] = merge_dicts(
provider.scanner_args, validated_data["scanner_args"]
)
if not validated_data.get("trigger"):
validated_data["trigger"] = Scan.TriggerChoices.MANUAL.value
return RLSSerializer.create(self, validated_data)
class ScanUpdateSerializer(BaseWriteSerializer):
"""
Serializer for updating the Provider model.
Only allows "alias" and "scanner_args" fields to be updated.
"""
class Meta:
model = Scan
# TODO: add mutelist when implemented
fields = ["id", "name"]
extra_kwargs = {
"id": {"read_only": True},
}
class ScanTaskSerializer(RLSSerializer):
trigger = serializers.ChoiceField(
choices=Scan.TriggerChoices.choices, read_only=True
)
state = StateEnumSerializerField(read_only=True)
class Meta:
model = Scan
fields = [
"id",
"name",
"trigger",
"state",
"unique_resource_count",
"progress",
"scanner_args",
"duration",
"started_at",
"completed_at",
"scheduled_at",
]
class ResourceTagSerializer(RLSSerializer):
"""
Serializer for the ResourceTag model
"""
class Meta:
model = ResourceTag
fields = ["key", "value"]
class ResourceSerializer(RLSSerializer):
"""
Serializer for the Resource model.
"""
tags = serializers.SerializerMethodField()
type_ = serializers.CharField(read_only=True)
findings = serializers.ResourceRelatedField(many=True, read_only=True)
class Meta:
model = Resource
fields = [
"id",
"inserted_at",
"updated_at",
"uid",
"name",
"region",
"service",
"type_",
"tags",
"provider",
"findings",
"url",
]
extra_kwargs = {
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
}
included_serializers = {
"findings": "api.v1.serializers.FindingSerializer",
"provider": "api.v1.serializers.ProviderSerializer",
}
@extend_schema_field(
{
"type": "object",
"description": "Tags associated with the resource",
"example": {"env": "prod", "owner": "johndoe"},
}
)
def get_tags(self, obj):
return obj.get_tags()
def get_fields(self):
"""`type` is a Python reserved keyword."""
fields = super().get_fields()
type_ = fields.pop("type_")
fields["type"] = type_
return fields
class FindingSerializer(RLSSerializer):
"""
Serializer for the Finding model.
"""
resources = serializers.ResourceRelatedField(many=True, read_only=True)
class Meta:
model = Finding
fields = [
"id",
"uid",
"delta",
"status",
"status_extended",
"severity",
"check_id",
"check_metadata",
"raw_result",
"inserted_at",
"updated_at",
"url",
# Relationships
"scan",
"resources",
]
included_serializers = {
"scan": ScanSerializer,
"resources": ResourceSerializer,
}
# Provider secrets
class BaseWriteProviderSecretSerializer(BaseWriteSerializer):
@staticmethod
def validate_secret_based_on_provider(provider_type: str, secret: dict):
if provider_type == Provider.ProviderChoices.AWS.value:
serializer = AwsProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.AZURE.value:
serializer = AzureProviderSecret(data=secret)
elif provider_type == Provider.ProviderChoices.GCP.value:
serializer = GCPProviderSecret(data=secret)
else:
raise serializers.ValidationError(
{"provider": f"Provider type not supported {provider_type}"}
)
try:
serializer.is_valid(raise_exception=True)
except ValidationError as validation_error:
# Customize the error message
details = validation_error.detail.copy()
for key, value in details.items():
validation_error.detail[f"secret/{key}"] = value
del validation_error.detail[key]
raise validation_error
class AwsProviderSecret(serializers.Serializer):
aws_access_key_id = serializers.CharField()
aws_secret_access_key = serializers.CharField()
aws_session_token = serializers.CharField(required=False)
class Meta:
resource_name = "ProviderSecret"
class AzureProviderSecret(serializers.Serializer):
client_id = serializers.CharField()
client_secret = serializers.CharField()
tenant_id = serializers.CharField()
class Meta:
resource_name = "ProviderSecret"
class GCPProviderSecret(serializers.Serializer):
client_id = serializers.CharField()
client_secret = serializers.CharField()
refresh_token = serializers.CharField()
class Meta:
resource_name = "ProviderSecret"
@extend_schema_field(
{
"oneOf": [
{
"type": "object",
"title": "AWS static credentials",
"properties": {
"aws_access_key_id": {"type": "string"},
"aws_secret_access_key": {"type": "string"},
"aws_session_token": {"type": "string"},
},
"required": ["aws_access_key_id", "aws_secret_access_key"],
},
{
"type": "object",
"title": "Azure static credentials",
"properties": {
"client_id": {"type": "string"},
"client_secret": {"type": "string"},
"tenant_id": {"type": "string", "description": "Azure tenant ID"},
},
"required": [
"client_id",
"client_secret",
"tenant_id",
],
},
{
"type": "object",
"title": "GCP static credentials",
"properties": {
"client_id": {"type": "string"},
"client_secret": {"type": "string"},
"refresh_token": {"type": "string"},
},
"required": [
"client_id",
"client_secret",
"refresh_token",
],
},
]
}
)
class ProviderSecretField(serializers.JSONField):
pass
class ProviderSecretSerializer(RLSSerializer):
"""
Serializer for the ProviderSecret model.
"""
class Meta:
model = ProviderSecret
fields = [
"id",
"inserted_at",
"updated_at",
"name",
"secret_type",
"provider",
"url",
]
class ProviderSecretCreateSerializer(RLSSerializer, BaseWriteProviderSecretSerializer):
secret = ProviderSecretField(write_only=True)
class Meta:
model = ProviderSecret
fields = [
"inserted_at",
"updated_at",
"name",
"secret_type",
"secret",
"provider",
]
extra_kwargs = {
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
}
def validate(self, attrs):
provider = attrs.get("provider")
secret = attrs.get("secret")
validated_attrs = super().validate(attrs)
self.validate_secret_based_on_provider(provider.provider, secret)
return validated_attrs
class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer):
secret = serializers.JSONField(write_only=True)
class Meta:
model = ProviderSecret
fields = [
"inserted_at",
"updated_at",
"name",
"secret_type",
"secret",
"provider",
]
extra_kwargs = {
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"provider": {"read_only": True},
"secret_type": {"read_only": True},
}
def validate(self, attrs):
provider = self.instance.provider
secret = attrs.get("secret")
validated_attrs = super().validate(attrs)
self.validate_secret_based_on_provider(provider.provider, secret)
return validated_attrs