mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-11-09 04:38:49 +00:00
feat(Resources): add the /resources endpoint (#34)
* fix(Resources): add basic Resource MVC * fix(Resources): implement many-to-many tags via through table * fix(Resources): tsvector & migration migration needs work, need to create a custom django migration field to build the GENERATED ALWAYS column. Without it, django will complain about the ts column not exisiting when we try to create an index. * test(Resources): add tests for /resources views * fix(Resources): get basic test cases to pass need to work on tsvector column and filtering by provider columns like alias, name * fix(Resources): provide way to get tags as dict not a list of ResourceTag models * fix(Resources): annotate with return type * fix(Resources): rename search field to not conflict with filters * fix(Resources): filter by provider.id * fix(Resources): remove filter and sort by provider must use provider_id first * fix(Resources): test adding, clearing tags * fix(Scans): filter by provider_id * fix(Resources): remove FIXME comments * fix(Resources): filter and search on tags * fix(Resources): full text search * fix(Resources): full text search to include both resources and tags and get rid of the distinct() query * fix(Resources): document text search a bit more * fix(Resources): remove TODO comments * fix(Resources): move Resource migrations into 0001_initital * fix(Config): revert to match main * fix(Resources): use Django's default query builder for full text search * Apply suggestions from code review Mostly formatting changes Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com> * fix(Resources): just use text search instead of guessing at allowed split characters for each provider * fix(Resources): remove unused tests these cases are covered in parameterized lists * fix(Resources): set a read-only RLS policy * fix(Resources): rename add_tags to be upsert_or_delete_tags * fix(Resources): include primary key on through table django query builder doesn't like tables without primary keys * fix(Resources): default xx_at filters to date instead of date time * docs(Resources): describe what a Resource is in API docs * chore(Resources): rename file to match others --------- Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
This commit is contained in:
1 parent
2420aedde9
commit
f7645e8f25
12 files changed
+986
-9
No files matched your search
+155
-1
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVector, SearchVectorField
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -155,7 +157,7 @@ class Scan(RowLevelSecurityProtectedModel):
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["provider", "state", "trigger", "scheduled_at"],
|
||||
name="scans_prov_state_type_sche_idx",
|
||||
name="scans_prov_state_trig_sche_idx",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -189,3 +191,155 @@ class Task(RowLevelSecurityProtectedModel):
|
||||
name="tasks_id_trt_id_idx",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ResourceTag(RowLevelSecurityProtectedModel):
|
||||
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)
|
||||
|
||||
key = models.TextField(blank=False)
|
||||
value = models.TextField(blank=False)
|
||||
|
||||
text_search = models.GeneratedField(
|
||||
expression=SearchVector("key", weight="A", config="simple")
|
||||
+ SearchVector("value", weight="B", config="simple"),
|
||||
output_field=SearchVectorField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resource_tags"
|
||||
|
||||
indexes = [
|
||||
GinIndex(fields=["text_search"], name="gin_resource_tags_search_idx"),
|
||||
]
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "key", "value"),
|
||||
name="unique_resource_tags_by_tenant_key_value",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Resource(RowLevelSecurityProtectedModel):
|
||||
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.ForeignKey(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="resources",
|
||||
related_query_name="resource",
|
||||
)
|
||||
|
||||
uid = models.TextField(
|
||||
"Unique identifier for the resource, set by the provider", blank=False
|
||||
)
|
||||
name = models.TextField("Name of the resource, as set in the provider", blank=False)
|
||||
region = models.TextField(
|
||||
"Location of the resource, as set by the provider", blank=False
|
||||
)
|
||||
service = models.TextField(
|
||||
"Service of the resource, as set by the provider", blank=False
|
||||
)
|
||||
type = models.TextField("Type of the resource, as set by the provider", blank=False)
|
||||
|
||||
text_search = models.GeneratedField(
|
||||
expression=SearchVector("uid", weight="A", config="simple")
|
||||
+ SearchVector("name", weight="B", config="simple")
|
||||
+ SearchVector("region", weight="C", config="simple")
|
||||
+ SearchVector("service", "type", weight="D", config="simple"),
|
||||
output_field=SearchVectorField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
tags = models.ManyToManyField(
|
||||
ResourceTag,
|
||||
verbose_name="Tags associated with the resource, by provider",
|
||||
through="ResourceTagMapping",
|
||||
)
|
||||
|
||||
def get_tags(self) -> dict:
|
||||
return {tag.key: tag.value for tag in self.tags.all()}
|
||||
|
||||
def clear_tags(self):
|
||||
self.tags.clear()
|
||||
self.save()
|
||||
|
||||
def upsert_or_delete_tags(self, tags: list[ResourceTag] | None):
|
||||
if tags is None:
|
||||
self.clear_tags()
|
||||
return
|
||||
|
||||
# Add new relationships with the tenant_id field
|
||||
for tag in tags:
|
||||
ResourceTagMapping.objects.update_or_create(
|
||||
tag=tag, resource=self, tenant_id=self.tenant_id
|
||||
)
|
||||
|
||||
# Save the instance
|
||||
self.save()
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resources"
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["uid", "region", "service", "name"],
|
||||
name="idx_resource_uid_reg_serv_name",
|
||||
),
|
||||
GinIndex(fields=["text_search"], name="gin_resources_search_idx"),
|
||||
]
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider_id", "uid"),
|
||||
name="unique_resources_by_provider",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ResourceTagMapping(RowLevelSecurityProtectedModel):
|
||||
# NOTE that we don't really need a primary key here,
|
||||
# but everything is easier with django if we do
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
resource = models.ForeignKey(Resource, on_delete=models.DO_NOTHING)
|
||||
tag = models.ForeignKey(ResourceTag, on_delete=models.CASCADE)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resource_tag_mappings"
|
||||
|
||||
# django will automatically create indexes for:
|
||||
# - resource_id
|
||||
# - tag_id
|
||||
# - tenant_id
|
||||
# - id
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "resource_id", "tag_id"),
|
||||
name="unique_resource_tag_mappings_by_tenant_resource_tag",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT"],
|
||||
),
|
||||
]
|
||||
Reference in new issue
Block a user