mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-16 08:24:15 +00:00
feat/PRWLR-3999 Implement providers endpoints (#21)
* feat(Backend): PRWLR-3989 add RLS to postgresql db and base models
* feat(API): PRWLR-3989 add TenantMiddleware
* chore(API, Backend): PRWLR-3989 create new db user without RLS bypass on migrations
* chore(Backend): PRWLR-3989 fix RLS bypasser for POST requests
* fix(Backend): PRWLR-3989 fix user permissions when migrating new models
* chore(Backend): PRWLR-3989 add testing view for RLS manual tests
* feat(API): PRWLR-3989 add tenant_id to API logging
* chore(API, Backend): PRWLR-3989 add TODOs
* test(API): PRWLR-3989 add new middleware unit tests
* chore(API): PRWLR-3989 refactor RLS code
* fix(tests): PRWLR-3989 fix testing db connector
* chore: PRWLR-3989 add references to JIRA tickets
* fix: PRWLR-3989 remove bypass logic and fix serializers
* fix: PRWLR-3989 improve drop SQL query for RLS models
* feat(Backend): PRWLR-3989 add specific permissions on each model
* fix(Backend): PRWLR-3989 fix database routing and grant select perms
* fix(test): PRWLR-3989 fix routing issues with unit tests
* chore: PRWLR-3989 remove references to JIRA tickets
* feat(Backend): PRWLR-3999 add Provider model
* feat: PRWLR-3999 add providers view logic
* fix: PRWLR-3999 fix unique index fields
* feat(API): PRWLR-3999 add custom exception handlers
* feat(API): PRWLR-3999 add /providers/{provider_id}/connection endpoint
* test(API): PRWLR-3999 add base unit tests for providers
* fix(API, Backend): PRWLR-3999 fix bugs after rebase
* chore(API, docker): PRWLR-3999 add dev feats for demo
* feat(API): PRWLR-3999 add drf-spectacular-jsonapi and improve docs
* test(API): PRWLR-3999 add providers unit tests
* chore(Backend): PRWLR-3989 adjust privileges for user on table tenant
* chore: PRWLR-3999 rename model custom validation error
* chore: PRWLR-3999 remove Test references
* chore: PRWLR-3999 update API v1 spec
* fix: PRWLR-3999 apply requested changes to filter and models
* feat: PRWLR-3999 add validation to PATCH /providers payload
* fix: PRWLR-3999 fix providers enum description
* chore: add more providers fixtures
* fix: PRWLR-3999 make providers.alias optional
This commit is contained in:
1 parent
e1a12bcb14
commit
4371ad1535
19 files changed
+1888
-481
No files matched your search
@@ -0,0 +1,32 @@
|
||||
from django.core.exceptions import ValidationError as django_validation_error
|
||||
from rest_framework_json_api.exceptions import exception_handler
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
|
||||
class ModelValidationError(ValidationError):
|
||||
def __init__(
|
||||
self,
|
||||
detail: str | None = None,
|
||||
code: str | None = None,
|
||||
pointer: str | None = None,
|
||||
status_code: int = 400,
|
||||
):
|
||||
super().__init__(
|
||||
detail=[
|
||||
{
|
||||
"detail": detail,
|
||||
"status": str(status_code),
|
||||
"source": {"pointer": pointer},
|
||||
"code": code,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
if isinstance(exc, django_validation_error):
|
||||
if hasattr(exc, "error_dict"):
|
||||
exc = ValidationError(exc.message_dict)
|
||||
else:
|
||||
exc = ValidationError(detail=exc.messages[0], code=exc.code)
|
||||
return exception_handler(exc, context)
|
||||
+15
-13
@@ -1,6 +1,7 @@
|
||||
from django_filters.rest_framework import FilterSet
|
||||
from django_filters.rest_framework import FilterSet, BooleanFilter
|
||||
from rest_framework_json_api.django_filters.backends import DjangoFilterBackend
|
||||
|
||||
from api.models import Provider
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
@@ -13,24 +14,25 @@ class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
return None
|
||||
|
||||
|
||||
class BaseFilter(FilterSet):
|
||||
class TenantFilter(FilterSet):
|
||||
class Meta:
|
||||
model = None
|
||||
model = Tenant
|
||||
fields = {
|
||||
"name": ["exact", "icontains"],
|
||||
"inserted_at": ["exact", "gte", "lte"],
|
||||
"updated_at": ["exact", "gte", "lte"],
|
||||
}
|
||||
|
||||
|
||||
class TenantFilter(BaseFilter):
|
||||
class Meta(BaseFilter.Meta):
|
||||
model = Tenant
|
||||
class ProviderFilter(FilterSet):
|
||||
connected = BooleanFilter()
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = {
|
||||
**BaseFilter.Meta.fields,
|
||||
"name": ["exact", "icontains"],
|
||||
"provider": ["exact"],
|
||||
"provider_id": ["exact", "icontains"],
|
||||
"alias": ["exact", "icontains"],
|
||||
"inserted_at": ["exact", "gte", "lte"],
|
||||
"updated_at": ["exact", "gte", "lte"],
|
||||
}
|
||||
|
||||
|
||||
class ProviderFilter(BaseFilter):
|
||||
# TODO implement proper filter
|
||||
pass
|
||||
@@ -0,0 +1,109 @@
|
||||
[
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "37b065f8-26b0-4218-a665-0b23d07b27d9",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:20:27.050Z",
|
||||
"updated_at": "2024-08-01T17:20:27.050Z",
|
||||
"provider": "gcp",
|
||||
"provider_id": "a12322-test321",
|
||||
"alias": "gcp_testing_2",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:19:42.453Z",
|
||||
"updated_at": "2024-08-01T17:19:42.453Z",
|
||||
"provider": "gcp",
|
||||
"provider_id": "a12345-test123",
|
||||
"alias": "gcp_testing_1",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:19:09.556Z",
|
||||
"updated_at": "2024-08-01T17:19:09.556Z",
|
||||
"provider": "aws",
|
||||
"provider_id": "123456789020",
|
||||
"alias": "aws_testing_2",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "baa7b895-8bac-4f47-b010-4226d132856e",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:20:16.962Z",
|
||||
"updated_at": "2024-08-01T17:20:16.962Z",
|
||||
"provider": "gcp",
|
||||
"provider_id": "a12322-test123",
|
||||
"alias": "gcp_testing_3",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "d7c7ea89-d9af-423b-a364-1290dcad5a01",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:18:58.132Z",
|
||||
"updated_at": "2024-08-01T17:18:58.132Z",
|
||||
"provider": "aws",
|
||||
"provider_id": "123456789015",
|
||||
"alias": "aws_testing_1",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-06T16:03:26.176Z",
|
||||
"updated_at": "2024-08-06T16:03:26.176Z",
|
||||
"provider": "azure",
|
||||
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
"alias": "azure_testing",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "26e55a24-cb2c-4cef-ac87-6f91fddb2c97",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-06T16:03:07.037Z",
|
||||
"updated_at": "2024-08-06T16:03:07.037Z",
|
||||
"provider": "kubernetes",
|
||||
"provider_id": "kubernetes-test-12345",
|
||||
"alias": "k8s_testing",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,8 +1,12 @@
|
||||
import uuid
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
import api.rls
|
||||
|
||||
DB_NAME = settings.DATABASES["default"]["NAME"]
|
||||
DB_USER_NAME = settings.DATABASES["default"]["USER"]
|
||||
DB_USER_PASSWORD = settings.DATABASES["default"]["PASSWORD"]
|
||||
@@ -61,4 +65,80 @@ class Migration(migrations.Migration):
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE tenant TO {DB_USER_NAME};
|
||||
"""
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Provider",
|
||||
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",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("aws", "Aws"),
|
||||
("azure", "Azure"),
|
||||
("gcp", "Gcp"),
|
||||
("kubernetes", "Kubernetes"),
|
||||
],
|
||||
default="aws",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"provider_id",
|
||||
models.CharField(
|
||||
max_length=63,
|
||||
validators=[django.core.validators.MinLengthValidator(3)],
|
||||
),
|
||||
),
|
||||
(
|
||||
"alias",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
null=True,
|
||||
max_length=100,
|
||||
validators=[django.core.validators.MinLengthValidator(3)],
|
||||
),
|
||||
),
|
||||
("connected", models.BooleanField(blank=True, null=True)),
|
||||
(
|
||||
"connection_last_checked_at",
|
||||
models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
("metadata", models.JSONField(default=dict)),
|
||||
("scanner_args", models.JSONField(default=dict)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="provider",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_provider",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="provider",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "provider_id"),
|
||||
name="unique_provider_ids",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
# Generated by Django 5.0.6 on 2024-08-01 15:11
|
||||
|
||||
import api.rls
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Test",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=100)),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "test",
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="test",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_test",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,17 +1,96 @@
|
||||
import re
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from api.rls import RowLevelSecurityProtectedModel, RowLevelSecurityConstraint
|
||||
from api.exceptions import ModelValidationError
|
||||
from api.rls import RowLevelSecurityConstraint
|
||||
from api.rls import RowLevelSecurityProtectedModel
|
||||
|
||||
|
||||
class Test(RowLevelSecurityProtectedModel):
|
||||
name = models.CharField(max_length=100)
|
||||
class Provider(RowLevelSecurityProtectedModel):
|
||||
class ProviderChoices(models.TextChoices):
|
||||
AWS = "aws", _("AWS")
|
||||
AZURE = "azure", _("Azure")
|
||||
GCP = "gcp", _("GCP")
|
||||
KUBERNETES = "kubernetes", _("Kubernetes")
|
||||
|
||||
class Meta:
|
||||
db_table = "test"
|
||||
@staticmethod
|
||||
def validate_aws_provider_id(value):
|
||||
if not re.match(r"^\d{12}$", value):
|
||||
raise ModelValidationError(
|
||||
detail="AWS provider ID must be exactly 12 digits.",
|
||||
code="aws-provider-id",
|
||||
pointer="/data/attributes/provider_id",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_azure_provider_id(value):
|
||||
try:
|
||||
val = UUID(value, version=4)
|
||||
if str(val) != value:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ModelValidationError(
|
||||
detail="Azure provider ID must be a valid UUID.",
|
||||
code="azure-provider-id",
|
||||
pointer="/data/attributes/provider_id",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_gcp_provider_id(value):
|
||||
if not re.match(r"^[a-z][a-z0-9-]{5,29}$", value):
|
||||
raise ModelValidationError(
|
||||
detail="GCP provider ID must be 6 to 30 characters, start with a letter, and contain only lowercase "
|
||||
"letters, numbers, and hyphens.",
|
||||
code="gcp-provider-id",
|
||||
pointer="/data/attributes/provider_id",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_kubernetes_provider_id(value):
|
||||
if not re.match(r"^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$", value):
|
||||
raise ModelValidationError(
|
||||
detail="K8s provider ID must be up to 63 characters, start and end with a lowercase letter or number, "
|
||||
"and contain only lowercase alphanumeric characters and hyphens.",
|
||||
code="kubernetes-provider-id",
|
||||
pointer="/data/attributes/provider_id",
|
||||
)
|
||||
|
||||
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)
|
||||
provider = models.CharField(
|
||||
max_length=10, choices=ProviderChoices.choices, default=ProviderChoices.AWS
|
||||
)
|
||||
provider_id = models.CharField(max_length=63, validators=[MinLengthValidator(3)])
|
||||
alias = models.CharField(
|
||||
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
|
||||
)
|
||||
connected = models.BooleanField(null=True, blank=True)
|
||||
connection_last_checked_at = models.DateTimeField(null=True, blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
scanner_args = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
getattr(self, f"validate_{self.provider}_provider_id")(self.provider_id)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "provider_id"),
|
||||
name="unique_provider_ids",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
)
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,8 @@ from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db import models
|
||||
from django.db.backends.ddl_references import Statement, Table
|
||||
|
||||
@@ -108,6 +110,10 @@ class RowLevelSecurityConstraint(models.BaseConstraint):
|
||||
path, _, kwargs = super().deconstruct()
|
||||
return (path, (self.target_field,), kwargs)
|
||||
|
||||
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS): # noqa: F841
|
||||
if not hasattr(instance, "tenant_id"):
|
||||
raise ValidationError(f"{model.__name__} does not have a tenant_id field.")
|
||||
|
||||
|
||||
class RowLevelSecurityProtectedModel(models.Model):
|
||||
tenant = models.ForeignKey("Tenant", on_delete=models.CASCADE)
|
||||
|
||||
@@ -1,14 +1,39 @@
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework_json_api import serializers
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.models import Test
|
||||
from api.models import Provider
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
# Tenants
|
||||
|
||||
|
||||
class TenantSerializer(BaseSerializerV1):
|
||||
"""
|
||||
Serializer for the Tenant model.
|
||||
@@ -19,16 +44,57 @@ class TenantSerializer(BaseSerializerV1):
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class TestSerializer(BaseSerializerV1):
|
||||
tenant = serializers.PrimaryKeyRelatedField(
|
||||
queryset=Tenant.objects.all(), write_only=True
|
||||
)
|
||||
# Providers
|
||||
|
||||
|
||||
class ProviderSerializer(RLSSerializer):
|
||||
"""
|
||||
Serializer for the Provider model.
|
||||
"""
|
||||
|
||||
connection = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Test
|
||||
fields = "__all__"
|
||||
model = Provider
|
||||
fields = [
|
||||
"id",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
"provider",
|
||||
"provider_id",
|
||||
"alias",
|
||||
"connection",
|
||||
"scanner_args",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"connection": {"read_only": True},
|
||||
}
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
representation.pop("tenant", None)
|
||||
return representation
|
||||
@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 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"]
|
||||
+857
-30
@@ -7,30 +7,132 @@ info:
|
||||
|
||||
This file is auto-generated.
|
||||
paths:
|
||||
/api/v1/schema:
|
||||
/api/v1/providers:
|
||||
get:
|
||||
operationId: schema_retrieve
|
||||
description: |-
|
||||
OpenApi3 schema for this API. Format can be selected via content negotiation.
|
||||
|
||||
- YAML: application/vnd.oai.openapi
|
||||
- JSON: application/vnd.oai.openapi+json
|
||||
operationId: providers_list
|
||||
description: Retrieve a list of all providers with options for filtering by
|
||||
various criteria.
|
||||
summary: List all providers
|
||||
parameters:
|
||||
- in: query
|
||||
name: format
|
||||
name: fields[Provider]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- json
|
||||
- yaml
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- provider
|
||||
- provider_id
|
||||
- alias
|
||||
- connection
|
||||
- metadata
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: lang
|
||||
name: filter[alias]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[alias__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[connected]
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: filter[inserted_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[provider]
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- en
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
description: |-
|
||||
* `aws` - Aws
|
||||
* `azure` - Azure
|
||||
* `gcp` - Gcp
|
||||
* `kubernetes` - Kubernetes
|
||||
- in: query
|
||||
name: filter[provider_id]
|
||||
schema:
|
||||
type: string
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[updated_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[updated_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[updated_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: page[number]
|
||||
required: false
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
schema:
|
||||
type: integer
|
||||
- name: page[size]
|
||||
required: false
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- provider
|
||||
- -provider
|
||||
- provider_id
|
||||
- -provider_id
|
||||
- alias
|
||||
- -alias
|
||||
- connected
|
||||
- -connected
|
||||
- inserted_at
|
||||
- -inserted_at
|
||||
- updated_at
|
||||
- -updated_at
|
||||
explode: false
|
||||
tags:
|
||||
- schema
|
||||
- Provider
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
@@ -38,24 +140,749 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.oai.openapi:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
application/yaml:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
application/vnd.oai.openapi+json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties: {}
|
||||
$ref: '#/components/schemas/PaginatedProviderList'
|
||||
description: ''
|
||||
post:
|
||||
operationId: providers_create
|
||||
description: Add a new provider to the system by providing the required provider
|
||||
details.
|
||||
summary: Create a new provider
|
||||
tags:
|
||||
- Provider
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProviderRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProviderRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProviderRequest'
|
||||
required: true
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProviderResponse'
|
||||
description: ''
|
||||
/api/v1/providers/{id}:
|
||||
get:
|
||||
operationId: providers_retrieve
|
||||
description: Fetch detailed information about a specific provider by their ID.
|
||||
summary: Retrieve data from a provider
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[Provider]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- provider
|
||||
- provider_id
|
||||
- alias
|
||||
- connection
|
||||
- metadata
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this provider.
|
||||
required: true
|
||||
tags:
|
||||
- Provider
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProviderResponse'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: providers_partial_update
|
||||
description: Update certain fields of an existing provider's information without
|
||||
affecting other fields.
|
||||
summary: Partially update a provider
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this provider.
|
||||
required: true
|
||||
tags:
|
||||
- Provider
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedProviderRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedProviderRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedProviderRequest'
|
||||
required: true
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ProviderResponse'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: providers_destroy
|
||||
description: Remove a provider from the system by their ID.
|
||||
summary: Delete a provider
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this provider.
|
||||
required: true
|
||||
tags:
|
||||
- Provider
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'202':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SerializerMetaclassResponse'
|
||||
description: ''
|
||||
/api/v1/providers/{id}/connection:
|
||||
post:
|
||||
operationId: providers_connection_create
|
||||
description: Try to verify connection. For instance, Role & Credentials are
|
||||
set correctly
|
||||
summary: Check connection
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this provider.
|
||||
required: true
|
||||
tags:
|
||||
- Provider
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'202':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SerializerMetaclassResponse'
|
||||
description: ''
|
||||
/api/v1/tenants:
|
||||
get:
|
||||
operationId: tenants_list
|
||||
description: Retrieve a list of all tenants with options for filtering by various
|
||||
criteria.
|
||||
summary: List all tenants
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[Tenant]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- name
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: query
|
||||
name: filter[inserted_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[inserted_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[name]
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[name__icontains]
|
||||
schema:
|
||||
type: string
|
||||
- name: filter[search]
|
||||
required: false
|
||||
in: query
|
||||
description: A search term.
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: filter[updated_at]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[updated_at__gte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: filter[updated_at__lte]
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: page[number]
|
||||
required: false
|
||||
in: query
|
||||
description: A page number within the paginated result set.
|
||||
schema:
|
||||
type: integer
|
||||
- name: page[size]
|
||||
required: false
|
||||
in: query
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- name: sort
|
||||
required: false
|
||||
in: query
|
||||
description: '[list of fields to sort by](https://jsonapi.org/format/#fetching-sorting)'
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- name
|
||||
- -name
|
||||
- inserted_at
|
||||
- -inserted_at
|
||||
- updated_at
|
||||
- -updated_at
|
||||
explode: false
|
||||
tags:
|
||||
- Tenant
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PaginatedTenantList'
|
||||
description: ''
|
||||
post:
|
||||
operationId: tenants_create
|
||||
description: Add a new tenant to the system by providing the required tenant
|
||||
details.
|
||||
summary: Create a new tenant
|
||||
tags:
|
||||
- Tenant
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TenantRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TenantRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TenantRequest'
|
||||
required: true
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'201':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TenantResponse'
|
||||
description: ''
|
||||
/api/v1/tenants/{id}:
|
||||
get:
|
||||
operationId: tenants_retrieve
|
||||
description: Fetch detailed information about a specific tenant by their ID.
|
||||
summary: Retrieve data from a tenant
|
||||
parameters:
|
||||
- in: query
|
||||
name: fields[Tenant]
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- inserted_at
|
||||
- updated_at
|
||||
- name
|
||||
description: endpoint return only specific fields in the response on a per-type
|
||||
basis by including a fields[TYPE] query parameter.
|
||||
explode: false
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this tenant.
|
||||
required: true
|
||||
tags:
|
||||
- Tenant
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TenantResponse'
|
||||
description: ''
|
||||
patch:
|
||||
operationId: tenants_partial_update
|
||||
description: Update certain fields of an existing tenant's information without
|
||||
affecting other fields.
|
||||
summary: Partially update a tenant
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this tenant.
|
||||
required: true
|
||||
tags:
|
||||
- Tenant
|
||||
requestBody:
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedTenantRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedTenantRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PatchedTenantRequest'
|
||||
required: true
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/vnd.api+json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TenantResponse'
|
||||
description: ''
|
||||
delete:
|
||||
operationId: tenants_destroy
|
||||
description: Remove a tenant from the system by their ID.
|
||||
summary: Delete a tenant
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: A UUID string identifying this tenant.
|
||||
required: true
|
||||
tags:
|
||||
- Tenant
|
||||
security:
|
||||
- cookieAuth: []
|
||||
- basicAuth: []
|
||||
- {}
|
||||
responses:
|
||||
'204':
|
||||
description: No response body
|
||||
components:
|
||||
schemas:
|
||||
PaginatedProviderList:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Provider'
|
||||
required:
|
||||
- data
|
||||
PaginatedTenantList:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Tenant'
|
||||
required:
|
||||
- data
|
||||
PatchedProviderRequest:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- Provider
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
provider:
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
type: string
|
||||
description: |-
|
||||
* `aws` - Aws
|
||||
* `azure` - Azure
|
||||
* `gcp` - Gcp
|
||||
* `kubernetes` - Kubernetes
|
||||
provider_id:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 63
|
||||
alias:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 100
|
||||
connection:
|
||||
type: object
|
||||
properties:
|
||||
connected:
|
||||
type: boolean
|
||||
last_checked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
metadata: {}
|
||||
required:
|
||||
- provider_id
|
||||
- alias
|
||||
required:
|
||||
- data
|
||||
PatchedTenantRequest:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- Tenant
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- data
|
||||
Provider:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/ProviderTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
provider:
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
type: string
|
||||
description: |-
|
||||
* `aws` - Aws
|
||||
* `azure` - Azure
|
||||
* `gcp` - Gcp
|
||||
* `kubernetes` - Kubernetes
|
||||
provider_id:
|
||||
type: string
|
||||
maxLength: 63
|
||||
minLength: 3
|
||||
alias:
|
||||
type: string
|
||||
maxLength: 100
|
||||
minLength: 3
|
||||
connection:
|
||||
type: object
|
||||
properties:
|
||||
connected:
|
||||
type: boolean
|
||||
last_checked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
metadata: {}
|
||||
required:
|
||||
- provider_id
|
||||
- alias
|
||||
ProviderRequest:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- Provider
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
provider:
|
||||
enum:
|
||||
- aws
|
||||
- azure
|
||||
- gcp
|
||||
- kubernetes
|
||||
type: string
|
||||
description: |-
|
||||
* `aws` - Aws
|
||||
* `azure` - Azure
|
||||
* `gcp` - Gcp
|
||||
* `kubernetes` - Kubernetes
|
||||
provider_id:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 63
|
||||
alias:
|
||||
type: string
|
||||
minLength: 3
|
||||
maxLength: 100
|
||||
connection:
|
||||
type: object
|
||||
properties:
|
||||
connected:
|
||||
type: boolean
|
||||
last_checked_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
metadata: {}
|
||||
required:
|
||||
- provider_id
|
||||
- alias
|
||||
required:
|
||||
- data
|
||||
ProviderResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Provider'
|
||||
required:
|
||||
- data
|
||||
ProviderTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- Provider
|
||||
SerializerMetaclassResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Provider'
|
||||
required:
|
||||
- data
|
||||
Tenant:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
- id
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/TenantTypeEnum'
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
maxLength: 100
|
||||
required:
|
||||
- name
|
||||
TenantRequest:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
additionalProperties: false
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
|
||||
member is used to describe resource objects that share common attributes
|
||||
and relationships.
|
||||
enum:
|
||||
- Tenant
|
||||
attributes:
|
||||
type: object
|
||||
properties:
|
||||
inserted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 100
|
||||
required:
|
||||
- name
|
||||
required:
|
||||
- data
|
||||
TenantResponse:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Tenant'
|
||||
required:
|
||||
- data
|
||||
TenantTypeEnum:
|
||||
type: string
|
||||
enum:
|
||||
- Tenant
|
||||
securitySchemes:
|
||||
basicAuth:
|
||||
type: http
|
||||
|
||||
@@ -4,7 +4,6 @@ from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db.utils import ConnectionRouter
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.models import Test
|
||||
from api.rls import Tenant
|
||||
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
|
||||
|
||||
@@ -17,7 +16,7 @@ class TestMainDatabaseRouter:
|
||||
yield ConnectionRouter()
|
||||
settings.DATABASE_ROUTERS = testing_routers
|
||||
|
||||
@pytest.mark.parametrize("api_model", [Tenant, Test])
|
||||
@pytest.mark.parametrize("api_model", [Tenant])
|
||||
def test_router_api_models(self, api_model, router):
|
||||
assert router.db_for_read(api_model) == MainRouter.default_db
|
||||
assert router.db_for_write(api_model) == MainRouter.default_db
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from api.models import Provider
|
||||
from api.rls import Tenant
|
||||
|
||||
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
|
||||
# TODO Change to 401 when authentication/authorization is implemented
|
||||
NO_TENANT_HTTP_STATUS = status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -50,8 +55,7 @@ class TestTenantViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["name"] == tenant1.name
|
||||
|
||||
def test_tenants_invalid_retrieve(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_invalid_retrieve(self, client):
|
||||
response = client.get(reverse("tenant-detail", kwargs={"pk": "random_id"}))
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@@ -177,3 +181,343 @@ class TestTenantViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 2
|
||||
assert response.json()["data"][0]["attributes"]["name"] == tenant2.name
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProviderViewSet:
|
||||
@pytest.fixture
|
||||
def providers(self, get_tenant):
|
||||
tenant = get_tenant
|
||||
provider1 = Provider.objects.create(
|
||||
provider="aws",
|
||||
provider_id="123456789012",
|
||||
alias="aws_testing_1",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider2 = Provider.objects.create(
|
||||
provider="aws",
|
||||
provider_id="123456789013",
|
||||
alias="aws_testing_2",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider3 = Provider.objects.create(
|
||||
provider="gcp",
|
||||
provider_id="a12322-test321",
|
||||
alias="gcp_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider4 = Provider.objects.create(
|
||||
provider="kubernetes",
|
||||
provider_id="kubernetes-test-12345",
|
||||
alias="k8s_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider5 = Provider.objects.create(
|
||||
provider="azure",
|
||||
provider_id="37b065f8-26b0-4218-a665-0b23d07b27d9",
|
||||
alias="azure_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
return provider1, provider2, provider3, provider4, provider5
|
||||
|
||||
def test_providers_rls(self, client):
|
||||
response = client.get(reverse("provider-list"))
|
||||
assert response.status_code == NO_TENANT_HTTP_STATUS
|
||||
|
||||
def test_providers_list(self, client, providers, tenant_header):
|
||||
response = client.get(reverse("provider-list"), headers=tenant_header)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(providers)
|
||||
|
||||
def test_providers_retrieve(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["provider"] == provider1.provider
|
||||
assert (
|
||||
response.json()["data"]["attributes"]["provider_id"]
|
||||
== provider1.provider_id
|
||||
)
|
||||
assert response.json()["data"]["attributes"]["alias"] == provider1.alias
|
||||
|
||||
def test_providers_invalid_retrieve(self, client, tenant_header):
|
||||
response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": "random_id"}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider_json_payload",
|
||||
(
|
||||
[
|
||||
{"provider": "aws", "provider_id": "111111111111", "alias": "test"},
|
||||
{"provider": "gcp", "provider_id": "a12322-test54321", "alias": "test"},
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"provider_id": "kubernetes-test-123456789",
|
||||
"alias": "test",
|
||||
},
|
||||
{
|
||||
"provider": "azure",
|
||||
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
"alias": "test",
|
||||
},
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_providers_create_valid(self, client, tenant_header, provider_json_payload):
|
||||
response = client.post(
|
||||
reverse("provider-list"),
|
||||
data=provider_json_payload,
|
||||
format="json",
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert Provider.objects.count() == 1
|
||||
assert Provider.objects.get().provider == provider_json_payload["provider"]
|
||||
assert (
|
||||
Provider.objects.get().provider_id == provider_json_payload["provider_id"]
|
||||
)
|
||||
assert Provider.objects.get().alias == provider_json_payload["alias"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider_json_payload, error_code",
|
||||
(
|
||||
[
|
||||
(
|
||||
{"provider": "aws", "provider_id": "1", "alias": "test"},
|
||||
"min_length",
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "aws",
|
||||
"provider_id": "1111111111111",
|
||||
"alias": "test",
|
||||
},
|
||||
"aws-provider-id",
|
||||
),
|
||||
(
|
||||
{"provider": "aws", "provider_id": "aaaaaaaaaaaa", "alias": "test"},
|
||||
"aws-provider-id",
|
||||
),
|
||||
(
|
||||
{"provider": "gcp", "provider_id": "1234asdf", "alias": "test"},
|
||||
"gcp-provider-id",
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "kubernetes",
|
||||
"provider_id": "-1234asdf",
|
||||
"alias": "test",
|
||||
},
|
||||
"kubernetes-provider-id",
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "azure",
|
||||
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e87",
|
||||
"alias": "test",
|
||||
},
|
||||
"azure-provider-id",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_providers_invalid_create(
|
||||
self, client, tenant_header, provider_json_payload, error_code
|
||||
):
|
||||
response = client.post(
|
||||
reverse("provider-list"),
|
||||
data=provider_json_payload,
|
||||
format="json",
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == error_code
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/provider_id"
|
||||
)
|
||||
|
||||
def test_providers_partial_update(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
new_alias = "This is the new name"
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "Provider",
|
||||
"id": provider1.id,
|
||||
"attributes": {"alias": new_alias},
|
||||
},
|
||||
}
|
||||
response = client.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
provider1.refresh_from_db()
|
||||
assert provider1.alias == new_alias
|
||||
|
||||
def test_providers_partial_update_invalid_content_type(
|
||||
self, client, providers, tenant_header
|
||||
):
|
||||
provider1, *_ = providers
|
||||
response = client.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
data={},
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
|
||||
def test_providers_partial_update_invalid_content(
|
||||
self, client, providers, tenant_header
|
||||
):
|
||||
provider1, *_ = providers
|
||||
new_name = "This is the new name"
|
||||
payload = {"alias": new_name}
|
||||
response = client.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"attribute_key, attribute_value",
|
||||
[
|
||||
("provider", "aws"),
|
||||
("provider_id", "123456789012"),
|
||||
],
|
||||
)
|
||||
def test_providers_partial_update_invalid_fields(
|
||||
self, client, providers, tenant_header, attribute_key, attribute_value
|
||||
):
|
||||
provider1, *_ = providers
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "Provider",
|
||||
"id": provider1.id,
|
||||
"attributes": {attribute_key: attribute_value},
|
||||
},
|
||||
}
|
||||
response = client.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_providers_delete(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
response = client.delete(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert "Content-Location" in response.headers
|
||||
assert Provider.objects.count() == len(providers) - 1
|
||||
# TODO Assert a task is returned when they are implemented
|
||||
|
||||
def test_providers_delete_invalid(self, client, tenant_header):
|
||||
response = client.delete(
|
||||
reverse("provider-detail", kwargs={"pk": "random_id"}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_providers_connection(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
assert provider1.connected is None
|
||||
assert provider1.connection_last_checked_at is None
|
||||
|
||||
response = client.post(
|
||||
reverse("provider-connection", kwargs={"pk": provider1.id}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert "Content-Location" in response.headers
|
||||
# TODO Assert a task is returned when they are implemented
|
||||
|
||||
provider1.refresh_from_db()
|
||||
assert provider1.connected is True
|
||||
assert isinstance(provider1.connection_last_checked_at, datetime)
|
||||
|
||||
def test_providers_connection_invalid_provider(
|
||||
self, client, providers, tenant_header
|
||||
):
|
||||
response = client.post(
|
||||
reverse("provider-connection", kwargs={"pk": "random_id"}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_name, filter_value",
|
||||
(
|
||||
[
|
||||
("provider", "aws"),
|
||||
("provider_id", "12345"),
|
||||
("alias", "test"),
|
||||
("search", "test"),
|
||||
("inserted_at", "2024-01-01 00:00:00"),
|
||||
("updated_at", "2024-01-01 00:00:00"),
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_providers_filters(self, client, tenant_header, filter_name, filter_value):
|
||||
response = client.get(
|
||||
reverse("provider-list"),
|
||||
{f"filter[{filter_name}]": filter_value},
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_name",
|
||||
(
|
||||
[
|
||||
"provider", # Valid filter, invalid value
|
||||
"invalid",
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_providers_filters_invalid(self, client, tenant_header, filter_name):
|
||||
response = client.get(
|
||||
reverse("provider-list"),
|
||||
{f"filter[{filter_name}]": "whatever"},
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sort_field",
|
||||
(
|
||||
[
|
||||
"provider",
|
||||
"provider_id",
|
||||
"alias",
|
||||
"connected",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_providers_sort(self, client, tenant_header, sort_field):
|
||||
response = client.get(
|
||||
reverse("provider-list"), {"sort": sort_field}, headers=tenant_header
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_providers_sort_invalid(self, client, tenant_header):
|
||||
response = client.get(
|
||||
reverse("provider-list"), {"sort": "invalid"}, headers=tenant_header
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
@@ -2,7 +2,7 @@ from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularRedocView
|
||||
from rest_framework import routers
|
||||
|
||||
from api.views.v1.views import SchemaView, TenantViewSet, ProviderViewSet, TestView
|
||||
from api.views.v1.views import SchemaView, TenantViewSet, ProviderViewSet
|
||||
|
||||
router = routers.DefaultRouter(trailing_slash=False)
|
||||
|
||||
@@ -13,5 +13,4 @@ urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
path("schema", SchemaView.as_view(), name="schema"),
|
||||
path("docs", SpectacularRedocView.as_view(url_name="schema"), name="docs"),
|
||||
path("test", TestView.as_view(), name="test"),
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework_json_api import filters
|
||||
from rest_framework_json_api.views import ModelViewSet
|
||||
@@ -21,3 +22,18 @@ class BaseViewSet(ModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseRLSViewSet(BaseViewSet):
|
||||
def initial(self, request, *args, **kwargs):
|
||||
super().initial(request, *args, **kwargs)
|
||||
if "X-Tenant-ID" not in request.headers:
|
||||
# This will return a 403 until we implement authentication/authorization
|
||||
# https://www.django-rest-framework.org/api-guide/authentication/#unauthorized-and-forbidden-responses
|
||||
raise NotAuthenticated("X-Tenant-ID header is required")
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
tenant_id = self.request.headers.get("X-Tenant-ID")
|
||||
context["tenant_id"] = tenant_id
|
||||
return context
|
||||
@@ -1,17 +1,23 @@
|
||||
from django.conf import settings as django_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework_json_api.views import Response
|
||||
|
||||
from api.filters import ProviderFilter
|
||||
from api.filters import TenantFilter
|
||||
from rest_framework.generics import ListCreateAPIView
|
||||
from api.models import Test
|
||||
from api.models import Provider
|
||||
from api.rls import Tenant
|
||||
from api.serializers import TenantSerializer, TestSerializer
|
||||
from api.serializers import ProviderSerializer, ProviderUpdateSerializer
|
||||
from api.serializers import TenantSerializer
|
||||
from api.views.base_views import BaseRLSViewSet
|
||||
from api.views.base_views import BaseViewSet
|
||||
|
||||
CACHE_DECORATOR = cache_control(
|
||||
@@ -86,240 +92,74 @@ class TenantViewSet(BaseViewSet):
|
||||
partial_update=extend_schema(
|
||||
summary="Partially update a provider",
|
||||
description="Update certain fields of an existing provider's information without affecting other fields.",
|
||||
request=ProviderUpdateSerializer,
|
||||
responses={200: ProviderSerializer},
|
||||
),
|
||||
destroy=extend_schema(
|
||||
summary="Delete a provider",
|
||||
description="Remove a provider from the system by their ID.",
|
||||
# TODO Update with task response when implemented
|
||||
responses={202: ProviderSerializer},
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
@method_decorator(CACHE_DECORATOR, name="retrieve")
|
||||
class ProviderViewSet(viewsets.ViewSet):
|
||||
content_location_header = {
|
||||
"Content-Location": "https://api.prowler.com/api/v1/tasks/5234"
|
||||
}
|
||||
|
||||
mock_links_data = {
|
||||
"first": "https://api.prowler.com/api/v1/providers?page%5Bnumber%5D=1",
|
||||
"last": "https://api.prowler.com/api/v1/providers?page%5Bnumber%5D=1",
|
||||
"next": None,
|
||||
"prev": None,
|
||||
}
|
||||
|
||||
mock_provider_data = [
|
||||
{
|
||||
"id": "5fd8f121-269e-4715-84cf-f92373f15dfa",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "aws",
|
||||
"provider_id": "1234567890",
|
||||
"alias": "mock_aws_connected",
|
||||
"connection": {
|
||||
"connected": True,
|
||||
"last_checked_at": "2024-07-17T09:55:14.191475Z",
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": True,
|
||||
"excluded_checks": [
|
||||
"awslambda_function_no_secrets_in_code",
|
||||
"cloudwatch_log_group_no_secrets_in_logs",
|
||||
],
|
||||
"aws_retries_max_attempts": 5,
|
||||
},
|
||||
"last_resource_count": 1234,
|
||||
"inserted_at": "2024-07-17T09:55:14.191475Z",
|
||||
"updated_at": "2024-07-17T09:55:14.191475Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "16aaeb4e-d3cd-4bb6-86f8-6c39cf93821e",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "aws",
|
||||
"provider_id": "1234567891",
|
||||
"alias": "mock_aws_not_connected",
|
||||
"connection": {
|
||||
"connected": False,
|
||||
"last_checked_at": "2024-07-17T09:55:18.987425Z",
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": True,
|
||||
"excluded_checks": [
|
||||
"awslambda_function_no_secrets_in_code",
|
||||
"cloudwatch_log_group_no_secrets_in_logs",
|
||||
],
|
||||
"aws_retries_max_attempts": 5,
|
||||
},
|
||||
"last_resource_count": 0,
|
||||
"inserted_at": "2024-07-17T09:55:18.987425Z",
|
||||
"updated_at": "2024-07-17T09:55:18.987425Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "a8f5e964-5964-4aaf-9176-844e2c3b0716",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "63f16b03-7849-4054-b40b-300e331f46f0",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "gcp",
|
||||
"provider_id": "1234567895",
|
||||
"alias": "mock_gcp",
|
||||
"connection": {
|
||||
"connected": True,
|
||||
"last_checked_at": "2024-07-17T09:55:18.987425Z",
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": True,
|
||||
"excluded_checks": [
|
||||
"apikeys_key_exists",
|
||||
"cloudsql_instance_public_ip",
|
||||
],
|
||||
"excluded_services": ["kms"],
|
||||
},
|
||||
"last_resource_count": 1111,
|
||||
"inserted_at": "2024-07-17T09:55:18.987425Z",
|
||||
"updated_at": "2024-07-17T09:55:18.987425Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f",
|
||||
},
|
||||
},
|
||||
},
|
||||
class ProviderViewSet(BaseRLSViewSet):
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
filterset_class = ProviderFilter
|
||||
search_fields = ["provider", "provider_id", "alias"]
|
||||
ordering = ["inserted_at"]
|
||||
ordering_fields = [
|
||||
"provider",
|
||||
"provider_id",
|
||||
"alias",
|
||||
"connected",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
mock_meta_data = {"version": "v1"}
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
resource_name = False
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
mock_data = {
|
||||
"links": self.mock_links_data,
|
||||
"data": self.mock_provider_data,
|
||||
"meta": {
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"pages": 1,
|
||||
"count": len(self.mock_provider_data),
|
||||
},
|
||||
**self.mock_meta_data,
|
||||
},
|
||||
}
|
||||
|
||||
return Response(mock_data, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, pk=None, *args, **kwargs):
|
||||
provider = next(
|
||||
(item for item in self.mock_provider_data if item["id"] == pk), None
|
||||
)
|
||||
if provider is not None:
|
||||
return Response({**provider, **self.mock_meta_data})
|
||||
return Response(
|
||||
[{"detail": "Not found.", "status": "404", "code": "not_found"}],
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
return Response(
|
||||
{"data": self.mock_provider_data[0], "meta": self.mock_meta_data},
|
||||
status.HTTP_201_CREATED,
|
||||
)
|
||||
def get_queryset(self):
|
||||
return Provider.objects.all()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
return Response(
|
||||
{"data": self.mock_provider_data[0], "meta": self.mock_meta_data},
|
||||
status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
task_mock = {
|
||||
"data": {
|
||||
"type": "tasks",
|
||||
"id": "5234",
|
||||
"attributes": {
|
||||
"status": "pending",
|
||||
},
|
||||
"links": {"self": "/tasks/5234"},
|
||||
}
|
||||
}
|
||||
return Response(
|
||||
task_mock,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers=self.content_location_header,
|
||||
)
|
||||
instance = self.get_object()
|
||||
serializer = ProviderUpdateSerializer(instance, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
read_serializer = ProviderSerializer(instance)
|
||||
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
summary="Check connection",
|
||||
description="Try to verify connection. For instance, Role & Credentials are set correctly",
|
||||
request=None,
|
||||
# TODO Update with task response when implemented
|
||||
responses={202: ProviderSerializer},
|
||||
)
|
||||
@action(detail=False, methods=["post"], url_path="check_connection")
|
||||
def check_connection(self, request):
|
||||
connection_data_mock = {
|
||||
"meta": {"version": "v1"},
|
||||
"data": {
|
||||
"id": "63f16b03-7849-4054-b40b-300e331f46f0",
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"provider": "gcp",
|
||||
"provider_id": "1234567895",
|
||||
"alias": "mock_gcp",
|
||||
"connection": {
|
||||
"connected": True,
|
||||
"last_checked_at": "2024-07-17T09:55:18.987425Z",
|
||||
},
|
||||
"scanner_args": {
|
||||
"only_logs": True,
|
||||
"excluded_checks": [
|
||||
"apikeys_key_exists",
|
||||
"cloudsql_instance_public_ip",
|
||||
],
|
||||
"excluded_services": ["kms"],
|
||||
},
|
||||
"last_resource_count": 1111,
|
||||
"inserted_at": "2024-07-17T09:55:18.987425Z",
|
||||
"updated_at": "2024-07-17T09:55:18.987425Z",
|
||||
"created_by": {
|
||||
"object": "user",
|
||||
"id": "eea048ab-7cb3-47eb-9e5e-dce591ade41f",
|
||||
},
|
||||
},
|
||||
},
|
||||
"relationships": {
|
||||
"tenant": {
|
||||
"links": {
|
||||
"self": "http://api.prowler.com/api/v1/providers/41f1d90e-5e3b-4eb0-b565-893f277d96c1",
|
||||
"related": "http://api.prowler.com/api/v1/tenants/22f9341e-4e13-44a8-996a-11f370696c54",
|
||||
},
|
||||
"data": {"type": "tenants", "id": "uuid"},
|
||||
},
|
||||
"credential": {
|
||||
"links": {
|
||||
"self": "http://api.prowler.com/api/v1/providers/41f1d90e-5e3b-4eb0-b565-893f277d96c1",
|
||||
"related": "http://api.prowler.com/api/v1/credentials/22f9341e-4e13-44a8-996a-11f370696c54",
|
||||
},
|
||||
"data": {"type": "credentials", "id": "uuid"},
|
||||
},
|
||||
"schedules": {
|
||||
"links": {
|
||||
"self": "http://api.prowler.com/api/v1/providers/41f1d90e-5e3b-4eb0-b565-893f277d96c1",
|
||||
"related": "http://api.prowler.com/api/v1/schedules/22f9341e-4e13-44a8-996a-11f370696c54",
|
||||
},
|
||||
"data": {
|
||||
"type": "schedules",
|
||||
"id": "ee15292e-687f-41cf-8652-30a8b7cded35",
|
||||
},
|
||||
},
|
||||
},
|
||||
@action(detail=True, methods=["post"], url_name="connection")
|
||||
def connection(self, request, pk=None):
|
||||
# TODO Connection check to third parties
|
||||
# TODO Returning provider data as a placeholder for tasks. We will do something similar for the created task
|
||||
provider = get_object_or_404(Provider, pk=pk)
|
||||
provider.connected = True
|
||||
provider.connection_last_checked_at = timezone.now()
|
||||
provider.save()
|
||||
serializer = ProviderSerializer(provider)
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={"Content-Location": reverse("provider-detail", kwargs={"pk": pk})},
|
||||
)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
response = super().destroy(request, *args, **kwargs)
|
||||
# TODO Background task to delete provider. For now, it will delete the provider from the system
|
||||
# Same as /connection endpoint
|
||||
response.status_code = status.HTTP_202_ACCEPTED
|
||||
response.headers = {
|
||||
"Content-Location": "/api/v1/tasks/5234",
|
||||
**response.headers,
|
||||
}
|
||||
|
||||
return Response(connection_data_mock, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class TestView(ListCreateAPIView):
|
||||
serializer_class = TestSerializer
|
||||
queryset = Test.objects.all()
|
||||
return response
|
||||
@@ -57,10 +57,10 @@ TEMPLATES = [
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular_jsonapi.schemas.openapi.JsonApiAutoSchema",
|
||||
"PAGE_SIZE": 10,
|
||||
"EXCEPTION_HANDLER": "rest_framework_json_api.exceptions.exception_handler",
|
||||
"DEFAULT_PAGINATION_CLASS": "rest_framework_json_api.pagination.JsonApiPageNumberPagination",
|
||||
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
|
||||
"DEFAULT_PAGINATION_CLASS": "drf_spectacular_jsonapi.schemas.pagination.JsonApiPageNumberPagination",
|
||||
"DEFAULT_PARSER_CLASSES": (
|
||||
"rest_framework_json_api.parsers.JSONParser",
|
||||
"rest_framework.parsers.FormParser",
|
||||
@@ -84,6 +84,10 @@ REST_FRAMEWORK = {
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"PREPROCESSING_HOOKS": [
|
||||
"drf_spectacular_jsonapi.hooks.fix_nested_path_parameters",
|
||||
],
|
||||
}
|
||||
|
||||
WSGI_APPLICATION = "config.wsgi.application"
|
||||
|
||||
@@ -2,7 +2,24 @@ import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_logging():
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_tenant():
|
||||
tenant = Tenant.objects.create(
|
||||
name="Tenant One",
|
||||
inserted_at="2023-01-01T00:00:00Z",
|
||||
updated_at="2023-01-02T00:00:00Z",
|
||||
)
|
||||
return tenant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant_header(get_tenant):
|
||||
return {"X-Tenant-ID": str(get_tenant.id)}
|
||||
Reference in new issue
Block a user