1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Resolve "Every snapshot causes additional queries to check_permissions when rbac is enabled"

This commit is contained in:
Davide Silvestri 2024-09-03 12:02:05 +00:00
parent a4eed09b2d
commit 5d195c1523
28 changed files with 367 additions and 159 deletions
backend
changelog/entries/unreleased/bug
docs/installation
enterprise/backend
src/baserow_enterprise
tests/baserow_enterprise_tests/role

View file

@ -135,7 +135,9 @@ class SnapshotsView(APIView):
""" """
handler = SnapshotHandler() handler = SnapshotHandler()
snapshot_created = handler.create(application_id, request.user, data["name"]) snapshot_created = handler.start_create_job(
application_id, request.user, data["name"]
)
serializer = JobSerializer(snapshot_created["job"]) serializer = JobSerializer(snapshot_created["job"])
return Response(serializer.data, status=HTTP_202_ACCEPTED) return Response(serializer.data, status=HTTP_202_ACCEPTED)
@ -195,7 +197,7 @@ class RestoreSnapshotView(APIView):
""" """
handler = SnapshotHandler() handler = SnapshotHandler()
job = handler.restore(snapshot_id, request.user) job = handler.start_restore_job(snapshot_id, request.user)
serializer = JobSerializer(job) serializer = JobSerializer(job)
return Response(serializer.data) return Response(serializer.data)

View file

@ -1075,7 +1075,7 @@ BASEROW_ROW_HISTORY_RETENTION_DAYS = int(
BASEROW_MAX_ROW_REPORT_ERROR_COUNT = int( BASEROW_MAX_ROW_REPORT_ERROR_COUNT = int(
os.getenv("BASEROW_MAX_ROW_REPORT_ERROR_COUNT", 30) os.getenv("BASEROW_MAX_ROW_REPORT_ERROR_COUNT", 30)
) )
BASEROW_MAX_SNAPSHOTS_PER_GROUP = int(os.getenv("BASEROW_MAX_SNAPSHOTS_PER_GROUP", -1)) BASEROW_MAX_SNAPSHOTS_PER_GROUP = int(os.getenv("BASEROW_MAX_SNAPSHOTS_PER_GROUP", 50))
BASEROW_SNAPSHOT_EXPIRATION_TIME_DAYS = int( BASEROW_SNAPSHOT_EXPIRATION_TIME_DAYS = int(
os.getenv("BASEROW_SNAPSHOT_EXPIRATION_TIME_DAYS", 360) # 360 days os.getenv("BASEROW_SNAPSHOT_EXPIRATION_TIME_DAYS", 360) # 360 days
) )

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.builder.data_sources.models import DataSource from baserow.contrib.builder.data_sources.models import DataSource
from baserow.contrib.builder.object_scopes import BuilderObjectScopeType from baserow.contrib.builder.object_scopes import BuilderObjectScopeType
@ -19,9 +19,16 @@ class BuilderDataSourceObjectScopeType(ObjectScopeType):
def get_parent_scope(self) -> Optional["ObjectScopeType"]: def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("builder_page") return object_scope_type_registry.get("builder_page")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"page", "page__builder", "page__builder__workspace" super()
.get_base_queryset(include_trash)
.filter(page__builder__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"page__builder__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.builder.domains.models import Domain from baserow.contrib.builder.domains.models import Domain
from baserow.contrib.builder.object_scopes import BuilderObjectScopeType from baserow.contrib.builder.object_scopes import BuilderObjectScopeType
@ -22,9 +22,16 @@ class BuilderDomainObjectScopeType(ObjectScopeType):
def get_parent(self, context: ContextObject) -> Optional[ContextObject]: def get_parent(self, context: ContextObject) -> Optional[ContextObject]:
return context.builder return context.builder
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"builder", "builder__workspace" super()
.get_base_queryset(include_trash)
.filter(builder__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"builder__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.builder.elements.models import Element from baserow.contrib.builder.elements.models import Element
from baserow.contrib.builder.object_scopes import BuilderObjectScopeType from baserow.contrib.builder.object_scopes import BuilderObjectScopeType
@ -19,9 +19,16 @@ class BuilderElementObjectScopeType(ObjectScopeType):
def get_parent_scope(self) -> Optional["ObjectScopeType"]: def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("builder_page") return object_scope_type_registry.get("builder_page")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"page", "page__builder", "page__builder__workspace" super()
.get_base_queryset(include_trash)
.filter(page__builder__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"page__builder__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.builder.models import Builder from baserow.contrib.builder.models import Builder
from baserow.core.object_scopes import ( from baserow.core.object_scopes import (
@ -15,8 +15,11 @@ class BuilderObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("application") return object_scope_type_registry.get("application")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related("workspace") return super().get_base_queryset(include_trash)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related("workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.builder.object_scopes import BuilderObjectScopeType from baserow.contrib.builder.object_scopes import BuilderObjectScopeType
from baserow.contrib.builder.pages.models import Page from baserow.contrib.builder.pages.models import Page
@ -18,9 +18,16 @@ class BuilderPageObjectScopeType(ObjectScopeType):
def get_parent_scope(self) -> Optional["ObjectScopeType"]: def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("builder") return object_scope_type_registry.get("builder")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"builder", "builder__workspace" super()
.get_base_queryset(include_trash)
.filter(builder__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"builder__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.builder.elements.object_scopes import BuilderElementObjectScopeType from baserow.contrib.builder.elements.object_scopes import BuilderElementObjectScopeType
from baserow.contrib.builder.object_scopes import BuilderObjectScopeType from baserow.contrib.builder.object_scopes import BuilderObjectScopeType
@ -18,7 +18,19 @@ class BuilderWorkflowActionScopeType(ObjectScopeType):
model_class = BuilderWorkflowAction model_class = BuilderWorkflowAction
def get_parent_scope(self) -> Optional["ObjectScopeType"]: def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("builder_element") return object_scope_type_registry.get_by_type(BuilderElementObjectScopeType)
def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return (
super()
.get_base_queryset(include_trash)
.filter(page__builder__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"page__builder__workspace"
)
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.database.models import Field from baserow.contrib.database.models import Field
from baserow.contrib.database.object_scopes import DatabaseObjectScopeType from baserow.contrib.database.object_scopes import DatabaseObjectScopeType
@ -17,9 +17,18 @@ class FieldObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_table") return object_scope_type_registry.get("database_table")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"table", "table__database", "table__database__workspace" super()
.get_base_queryset(include_trash)
.filter(
table__database__workspace__isnull=False,
)
)
def get_enhanced_queryset(self, include_trash: bool = False):
return self.get_base_queryset(include_trash).select_related(
"table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -67,4 +67,8 @@ __all__ = [
class Database(Application): class Database(Application):
def get_parent(self): def get_parent(self):
# This is a bit of a hack to prevent an unecesary query to the database to
# get the parent workspace that we already have.
if "workspace" in self._state.fields_cache:
self.application_ptr.workspace = self.workspace
return self.application_ptr return self.application_ptr

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.database.models import Database from baserow.contrib.database.models import Database
from baserow.core.object_scopes import ( from baserow.core.object_scopes import (
@ -15,8 +15,11 @@ class DatabaseObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("application") return object_scope_type_registry.get("application")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet[Database]:
return self.get_base_queryset().prefetch_related("workspace") return super().get_base_queryset(include_trash).filter(workspace__isnull=False)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet[Database]:
return self.get_base_queryset(include_trash).select_related("workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.database.object_scopes import DatabaseObjectScopeType from baserow.contrib.database.object_scopes import DatabaseObjectScopeType
from baserow.contrib.database.table.models import Table from baserow.contrib.database.table.models import Table
@ -16,9 +16,16 @@ class DatabaseTableObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database") return object_scope_type_registry.get("database")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"database", "database__workspace" super()
.get_base_queryset(include_trash)
.filter(database__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.contrib.database.object_scopes import DatabaseObjectScopeType from baserow.contrib.database.object_scopes import DatabaseObjectScopeType
from baserow.contrib.database.table.object_scopes import DatabaseTableObjectScopeType from baserow.contrib.database.table.object_scopes import DatabaseTableObjectScopeType
@ -24,9 +24,16 @@ class DatabaseViewObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_table") return object_scope_type_registry.get("database_table")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"table", "table__database", "table__database__workspace" super()
.get_base_queryset(include_trash)
.filter(table__database__workspace__isnull=False)
)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
@ -52,12 +59,16 @@ class DatabaseViewDecorationObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_view") return object_scope_type_registry.get("database_view")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"view", super()
"view__table", .get_base_queryset(include_trash)
"view__table__database", .filter(view__table__database__workspace__isnull=False)
"view__table__database__workspace", )
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"view__table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
@ -86,12 +97,16 @@ class DatabaseViewSortObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_view") return object_scope_type_registry.get("database_view")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"view", super()
"view__table", .get_base_queryset(include_trash)
"view__table__database", .filter(view__table__database__workspace__isnull=False)
"view__table__database__workspace", )
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"view__table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
@ -120,12 +135,16 @@ class DatabaseViewFilterObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_view") return object_scope_type_registry.get("database_view")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"view", super()
"view__table", .get_base_queryset(include_trash)
"view__table__database", .filter(view__table__database__workspace__isnull=False)
"view__table__database__workspace", )
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"view__table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
@ -154,12 +173,16 @@ class DatabaseViewFilterGroupObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_view") return object_scope_type_registry.get("database_view")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"view", super()
"view__table", .get_base_queryset(include_trash)
"view__table__database", .filter(view__table__database__workspace__isnull=False)
"view__table__database__workspace", )
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"view__table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
@ -188,12 +211,16 @@ class DatabaseViewGroupByObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("database_view") return object_scope_type_registry.get("database_view")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return (
"view", super()
"view__table", .get_base_queryset(include_trash)
"view__table__database", .filter(view__table__database__workspace__isnull=False)
"view__table__database__workspace", )
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related(
"view__table__database__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.core.integrations.models import Integration from baserow.core.integrations.models import Integration
from baserow.core.object_scopes import ( from baserow.core.object_scopes import (
@ -17,9 +17,9 @@ class IntegrationObjectScopeType(ObjectScopeType):
def get_parent_scope(self) -> Optional["ObjectScopeType"]: def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("application") return object_scope_type_registry.get("application")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return self.get_base_queryset(include_trash).select_related(
"application", "application__workspace" "application__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -362,7 +362,8 @@ class HierarchicalModelMixin(models.Model, metaclass=AbstractModelMeta):
This mixin introduce some helpers for working with hierarchical models. This mixin introduce some helpers for working with hierarchical models.
""" """
@abc.abstractclassmethod @classmethod
@abc.abstractmethod
def get_parent(self): def get_parent(self):
""" """
:return: The parent of this model. Returns None if this is the root. :return: The parent of this model. Returns None if this is the root.

View file

@ -419,13 +419,11 @@ class Application(
return cls.get_highest_order_of_queryset(queryset) + 1 return cls.get_highest_order_of_queryset(queryset) + 1
def get_parent(self): def get_parent(self):
# If this application is an application snapshot, then it'll if not self.workspace_id:
# have a None workspace, so instead we define its parent as raise ValueError(
# the source snapshot's `snapshot_from`. "Cannot call get_parent if workspace is None. Please check your hierarchy."
if self.workspace_id: )
return self.workspace return self.workspace
else:
return self.snapshot_from.get()
class TemplateCategory(models.Model): class TemplateCategory(models.Model):

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.core.models import ( from baserow.core.models import (
Application, Application,
@ -32,8 +32,11 @@ class ApplicationObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("workspace") return object_scope_type_registry.get("workspace")
def get_enhanced_queryset(self): def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related("workspace") return super().get_base_queryset(include_trash).filter(workspace__isnull=False)
def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset(include_trash).select_related("workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:
@ -49,8 +52,8 @@ class WorkspaceInvitationObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("workspace") return object_scope_type_registry.get("workspace")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related("workspace") return self.get_base_queryset(include_trash).select_related("workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:
@ -66,8 +69,8 @@ class WorkspaceUserObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("workspace") return object_scope_type_registry.get("workspace")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related("workspace") return self.get_base_queryset(include_trash).select_related("workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:

View file

@ -769,20 +769,29 @@ class ObjectScopeType(Instance, ModelInstanceMixin):
f"Must be implemented by the specific type <{self.type}>" f"Must be implemented by the specific type <{self.type}>"
) )
def get_base_queryset(self) -> QuerySet: def get_base_queryset(self, include_trash: bool = False) -> QuerySet:
""" """
:params include_trash: If true then also includes trashed objects in the
queryset. Needed to verify if a user needs to be included in the recipient
list of a deleted_* signal.
Returns the base queryset for the objects of this scope Returns the base queryset for the objects of this scope
""" """
return self.model_class.objects.all() model_manager = self.model_class.objects
if include_trash and hasattr(self.model_class, "objects_and_trash"):
model_manager = self.model_class.objects_and_trash
return model_manager.order_by().all()
def get_enhanced_queryset(self) -> QuerySet: def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
""" """
:params include_trash: If true then also includes trashed objects in the
queryset. Needed to verify if a user needs to be included in the recipient
list of a deleted_* signal.
Returns the enhanced queryset for the objects of this scope enhanced for better Returns the enhanced queryset for the objects of this scope enhanced for better
performances. performances.
""" """
return self.get_base_queryset() return self.get_base_queryset(include_trash=include_trash)
def are_objects_child_of( def are_objects_child_of(
self, child_objects: List[Any], parent_object: ScopeObject self, child_objects: List[Any], parent_object: ScopeObject
@ -849,14 +858,18 @@ class ObjectScopeType(Instance, ModelInstanceMixin):
""" """
objects_per_scope = {} objects_per_scope = {}
parent_scope_types = set()
parent_scopes = [] parent_scopes = []
for scope in scopes: for scope in scopes:
if object_scope_type_registry.get_by_model(scope).type == self.type: object_scope_type = object_scope_type_registry.get_by_model(scope)
if object_scope_type.type == self.type:
# Scope of the same type doesn't need to be queried # Scope of the same type doesn't need to be queried
objects_per_scope[scope] = set([scope]) objects_per_scope[scope] = set([scope])
else: else:
parent_scopes.append(scope) parent_scopes.append(scope)
objects_per_scope[scope] = set()
parent_scope_types.add(object_scope_type)
if parent_scopes: if parent_scopes:
query_result = list( query_result = list(
@ -867,15 +880,13 @@ class ObjectScopeType(Instance, ModelInstanceMixin):
# We have all the objects in the queryset, but now we want to sort them # We have all the objects in the queryset, but now we want to sort them
# into buckets per original scope they are a child of. # into buckets per original scope they are a child of.
for scope in parent_scopes: for obj in query_result:
objects_per_scope[scope] = set() for scope_type in parent_scope_types:
scope_type = object_scope_type_registry.get_by_model(scope)
for obj in query_result:
parent_scope = object_scope_type_registry.get_parent( parent_scope = object_scope_type_registry.get_parent(
obj, at_scope_type=scope_type obj, at_scope_type=scope_type
) )
if parent_scope == scope: if parent_scope in objects_per_scope:
objects_per_scope[scope].add(obj) objects_per_scope[parent_scope].add(obj)
return objects_per_scope return objects_per_scope

View file

@ -1,6 +1,7 @@
import datetime import datetime
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db import IntegrityError, OperationalError from django.db import IntegrityError, OperationalError
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
@ -17,7 +18,7 @@ from baserow.core.exceptions import (
from baserow.core.handler import CoreHandler from baserow.core.handler import CoreHandler
from baserow.core.jobs.handler import JobHandler from baserow.core.jobs.handler import JobHandler
from baserow.core.jobs.models import Job from baserow.core.jobs.models import Job
from baserow.core.models import Application, Snapshot, User, Workspace from baserow.core.models import Application, Snapshot, Workspace
from baserow.core.registries import ImportExportConfig, application_type_registry from baserow.core.registries import ImportExportConfig, application_type_registry
from baserow.core.signals import application_created from baserow.core.signals import application_created
from baserow.core.snapshots.exceptions import ( from baserow.core.snapshots.exceptions import (
@ -77,7 +78,7 @@ class SnapshotHandler:
if snapshot.mark_for_deletion is True: if snapshot.mark_for_deletion is True:
raise SnapshotIsBeingDeleted() raise SnapshotIsBeingDeleted()
def list(self, application_id: int, performed_by: User) -> QuerySet: def list(self, application_id: int, performed_by: AbstractUser) -> QuerySet:
""" """
Lists all snapshots for the given application id if the provided Lists all snapshots for the given application id if the provided
user is in the same workspace as the application. user is in the same workspace as the application.
@ -122,31 +123,46 @@ class SnapshotHandler:
.order_by("-created_at", "-id") .order_by("-created_at", "-id")
) )
def create(self, application_id: int, performed_by: User, name: str): def start_create_job(
self, application_id: int, performed_by: AbstractUser, name: str
):
""" """
Creates a new application snapshot of the given application if the provided Create a snapshot instance to track the creation of a snapshot and start the job
user is in the same workspace as the application. to perform the snapshot creation.
:param application_id: The ID of the application for which to list :param application_id: The ID of the application for which to list snapshots.
snapshots. :param performed_by: The user performing the operation that should have
:param performed_by: The user performing the operation that should sufficient permissions.
have sufficient permissions.
:param name: The name for the new snapshot. :param name: The name for the new snapshot.
:raises ApplicationDoesNotExist: When the application with the provided id :raises ApplicationDoesNotExist: When the application with the provided id does
does not exist. not exist.
:raises UserNotInWorkspace: When the user doesn't belong to the same workspace :raises UserNotInWorkspace: When the user doesn't belong to the same workspace
as the application. as the application.
:raises MaximumSnapshotsReached: When the workspace has already reached :raises MaximumSnapshotsReached: When the workspace has already reached the
the maximum of allowed snapshots. maximum of allowed snapshots.
:raises ApplicationOperationNotSupported: When the application type :raises ApplicationOperationNotSupported: When the application type doesn't
doesn't support creating snapshots. support creating snapshots.
:raises SnapshotIsBeingCreated: When creating a snapshot is already :raises SnapshotIsBeingCreated: When creating a snapshot is already scheduled
scheduled for the application. for the application.
:raises MaxJobCountExceeded: When the user already has a running :raises MaxJobCountExceeded: When the user already has a running job to create a
job to create a snapshot of the same type. snapshot of the same type.
:return: The snapshot object that was created. :return: The snapshot object that was created and the started job.
""" """
snapshot = self.create(application_id, performed_by, name)
job = JobHandler().create_and_start_job(
performed_by,
CreateSnapshotJobType.type,
snapshot=snapshot,
)
return {
"snapshot": snapshot,
"job": job,
}
def create(self, application_id, performed_by, name):
try: try:
application = ( application = (
Application.objects.filter(id=application_id) Application.objects.filter(id=application_id)
@ -192,23 +208,12 @@ class SnapshotHandler:
if "unique constraint" in e.args[0]: if "unique constraint" in e.args[0]:
raise SnapshotNameNotUnique() raise SnapshotNameNotUnique()
raise e raise e
return snapshot
job = JobHandler().create_and_start_job( def start_restore_job(
performed_by,
CreateSnapshotJobType.type,
False,
snapshot=snapshot,
)
return {
"snapshot": snapshot,
"job": job,
}
def restore(
self, self,
snapshot_id: int, snapshot_id: int,
performed_by: User, performed_by: AbstractUser,
) -> Job: ) -> Job:
""" """
Restores a previously created snapshot with the given ID if the Restores a previously created snapshot with the given ID if the
@ -262,7 +267,6 @@ class SnapshotHandler:
job = JobHandler().create_and_start_job( job = JobHandler().create_and_start_job(
performed_by, performed_by,
RestoreSnapshotJobType.type, RestoreSnapshotJobType.type,
False,
snapshot=snapshot, snapshot=snapshot,
) )
@ -274,7 +278,7 @@ class SnapshotHandler:
if snapshot.snapshot_to_application is not None: if snapshot.snapshot_to_application is not None:
delete_application_snapshot.delay(snapshot.snapshot_to_application.id) delete_application_snapshot.delay(snapshot.snapshot_to_application.id)
def delete(self, snapshot_id: int, performed_by: User) -> None: def delete(self, snapshot_id: int, performed_by: AbstractUser) -> None:
""" """
Deletes a previously created snapshot with the given ID if the Deletes a previously created snapshot with the given ID if the
provided user belongs to the same workspace as the application. provided user belongs to the same workspace as the application.
@ -444,6 +448,9 @@ class SnapshotHandler:
restore_snapshot_import_export_config = ImportExportConfig( restore_snapshot_import_export_config = ImportExportConfig(
include_permission_data=True, reduce_disk_space_usage=False include_permission_data=True, reduce_disk_space_usage=False
) )
# Temporary set the workspace for the application so that the permissions can
# be correctly set during the import process.
application.workspace = workspace
exported_application = application_type.export_serialized( exported_application = application_type.export_serialized(
application, restore_snapshot_import_export_config, None, default_storage application, restore_snapshot_import_export_config, None, default_storage
) )

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.core.models import Snapshot from baserow.core.models import Snapshot
from baserow.core.object_scopes import ( from baserow.core.object_scopes import (
@ -15,9 +15,9 @@ class SnapshotObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("application") return object_scope_type_registry.get("application")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return self.get_base_queryset(include_trash).select_related(
"snapshot_from_application", "snapshot_from_application__workspace" "snapshot_from_application__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -1,6 +1,6 @@
from typing import Optional from typing import Optional
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.core.object_scopes import ( from baserow.core.object_scopes import (
ApplicationObjectScopeType, ApplicationObjectScopeType,
@ -17,9 +17,9 @@ class UserSourceObjectScopeType(ObjectScopeType):
def get_parent_scope(self) -> Optional["ObjectScopeType"]: def get_parent_scope(self) -> Optional["ObjectScopeType"]:
return object_scope_type_registry.get("application") return object_scope_type_registry.get("application")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related( return self.get_base_queryset(include_trash).select_related(
"application", "application__workspace" "application__workspace"
) )
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):

View file

@ -11,9 +11,9 @@ def test_can_create_a_snapshot_for_builder_application(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user) builder = data_fixture.create_builder_application(user)
snapshot = SnapshotHandler().create(builder.id, performed_by=user, name="test")[ snapshot = SnapshotHandler().start_create_job(
"snapshot" builder.id, performed_by=user, name="test"
] )["snapshot"]
assert snapshot is not None assert snapshot is not None
assert snapshot.name == "test" assert snapshot.name == "test"
@ -25,9 +25,9 @@ def test_can_delete_a_snapshot_for_builder_application(data_fixture):
user = data_fixture.create_user() user = data_fixture.create_user()
builder = data_fixture.create_builder_application(user) builder = data_fixture.create_builder_application(user)
snapshot = SnapshotHandler().create(builder.id, performed_by=user, name="test")[ snapshot = SnapshotHandler().start_create_job(
"snapshot" builder.id, performed_by=user, name="test"
] )["snapshot"]
SnapshotHandler().delete(snapshot.id, user) SnapshotHandler().delete(snapshot.id, user)

View file

@ -29,7 +29,9 @@ def test_create_snapshot_action_type(
application = data_fixture.create_database_application(workspace=workspace) application = data_fixture.create_database_application(workspace=workspace)
with django_capture_on_commit_callbacks(execute=True): with django_capture_on_commit_callbacks(execute=True):
result = SnapshotHandler().create(application.id, user, "test snapshot") result = SnapshotHandler().start_create_job(
application.id, user, "test snapshot"
)
snapshot = result["snapshot"] snapshot = result["snapshot"]
job = result["job"] job = result["job"]
@ -68,7 +70,7 @@ def test_restore_snapshot_action_type(
) )
with django_capture_on_commit_callbacks(execute=True): with django_capture_on_commit_callbacks(execute=True):
job = SnapshotHandler().restore(snapshot.id, user) job = SnapshotHandler().start_restore_job(snapshot.id, user)
job.refresh_from_db() job.refresh_from_db()
assert job.state == JOB_FINISHED assert job.state == JOB_FINISHED

View file

@ -0,0 +1,7 @@
{
"type": "bug",
"message": "Fix a bug causing every snapshot to perform additional queries to check permissions for RBAC.",
"issue_number": 2941,
"bullet_points": [],
"created_at": "2024-08-27"
}

View file

@ -273,7 +273,7 @@ domain than your Baserow, you need to make sure CORS is configured correctly.
| BASEROW\_DISABLE\_PUBLIC\_URL\_CHECK | When opening the Baserow login page a check is run to ensure the PUBLIC\_BACKEND\_URL/BASEROW\_PUBLIC\_URL variables are set correctly and your browser can correctly connect to the backend. If misconfigured an error is shown. If you wish to disable this check and warning set this to any non empty value. | | | BASEROW\_DISABLE\_PUBLIC\_URL\_CHECK | When opening the Baserow login page a check is run to ensure the PUBLIC\_BACKEND\_URL/BASEROW\_PUBLIC\_URL variables are set correctly and your browser can correctly connect to the backend. If misconfigured an error is shown. If you wish to disable this check and warning set this to any non empty value. | |
| ADDITIONAL\_MODULES | **Internal** A list of file paths to Nuxt module.js files to load as additional Nuxt modules into Baserow on startup. | | | ADDITIONAL\_MODULES | **Internal** A list of file paths to Nuxt module.js files to load as additional Nuxt modules into Baserow on startup. | |
| BASEROW\_DISABLE\_GOOGLE\_DOCS\_FILE\_PREVIEW | Set to \`true\` or \`1\` to disable Google docs file preview. | | | BASEROW\_DISABLE\_GOOGLE\_DOCS\_FILE\_PREVIEW | Set to \`true\` or \`1\` to disable Google docs file preview. | |
| BASEROW_MAX_SNAPSHOTS_PER_GROUP | Controls how many application snapshots can be created per workspace. | -1 (unlimited) | | BASEROW_MAX_SNAPSHOTS_PER_GROUP | Controls how many application snapshots can be created per workspace. | 50 |
| BASEROW\_USE\_PG\_FULLTEXT\_SEARCH | By default, Baserow will use Postgres full-text as its search backend. If the product is installed on a system with limited disk space, and less accurate results / degraded search performance is acceptable, then switch this setting off by setting it to false. | true | | BASEROW\_USE\_PG\_FULLTEXT\_SEARCH | By default, Baserow will use Postgres full-text as its search backend. If the product is installed on a system with limited disk space, and less accurate results / degraded search performance is acceptable, then switch this setting off by setting it to false. | true |
| BASEROW\_UNIQUE\_ROW\_VALUES\_SIZE\_LIMIT | Sets the limit for the automatic detection of multiselect options when converting a text field to a multiselect field. Increase the value to detect more options automatically, but consider performance implications. | 100 | | BASEROW\_UNIQUE\_ROW\_VALUES\_SIZE\_LIMIT | Sets the limit for the automatic detection of multiselect options when converting a text field to a multiselect field. Increase the value to detect more options automatically, but consider performance implications. | 100 |
| BASEROW\_BUILDER\_DOMAINS | A comma separated list of domain names that can be used as the domains to create sub domains in the application builder. | | | BASEROW\_BUILDER\_DOMAINS | A comma separated list of domain names that can be used as the domains to create sub domains in the application builder. | |

View file

@ -477,6 +477,12 @@ class RoleAssignmentHandler:
roles_per_scope_per_user = defaultdict(list) roles_per_scope_per_user = defaultdict(list)
for actor in actors: for actor in actors:
for key, value in roles_by_scope[actor.id].items(): for key, value in roles_by_scope[actor.id].items():
# scope_cache contains all the filtered scopes we need to check
# permissions for. Objects from snapshotted applications (table,
# applications, etc.) are filtered out as they're not accessible to the
# user, so we can safely ignore them here.
if key not in scope_cache:
continue
roles_per_scope_per_user[actor].append((scope_cache[key], value)) roles_per_scope_per_user[actor].append((scope_cache[key], value))
return roles_per_scope_per_user return roles_per_scope_per_user
@ -744,17 +750,23 @@ class RoleAssignmentHandler:
return self.assign_role_batch(workspace, new_role_assignments) return self.assign_role_batch(workspace, new_role_assignments)
def get_scopes(self, content_type_id, scope_ids) -> List[ScopeObject]: def get_scopes(self, content_type_id, scope_ids) -> QuerySet[ScopeObject]:
""" """
Helper method that returns the actual scope object given the scope ID and Helper method that returns the actual scope object given the scope ID and
the scope type. the scope type.
:param scope_id: The scope id. :param scope_id: The scope id.
:param content_type_id: The content_type id :param content_type_id: The content_type id
:return: A QuerySet of the scope objects matching the given scope IDs.
""" """
content_type = ContentType.objects.get_for_id(content_type_id) content_type = ContentType.objects.get_for_id(content_type_id)
return content_type.get_all_objects_for_this_type(id__in=scope_ids) object_scope = object_scope_type_registry.get_for_class(
content_type.model_class()
)
return object_scope.get_enhanced_queryset(include_trash=True).filter(
id__in=scope_ids
)
def get_role_assignments(self, workspace: Workspace, scope: Optional[ScopeObject]): def get_role_assignments(self, workspace: Workspace, scope: Optional[ScopeObject]):
""" """

View file

@ -1,4 +1,4 @@
from django.db.models import Q from django.db.models import Q, QuerySet
from baserow.core.object_scopes import WorkspaceObjectScopeType from baserow.core.object_scopes import WorkspaceObjectScopeType
from baserow.core.registries import ObjectScopeType, object_scope_type_registry from baserow.core.registries import ObjectScopeType, object_scope_type_registry
@ -12,8 +12,8 @@ class TeamObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("workspace") return object_scope_type_registry.get("workspace")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related("workspace") return self.get_base_queryset(include_trash).select_related("workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:
@ -29,8 +29,8 @@ class TeamSubjectObjectScopeType(ObjectScopeType):
def get_parent_scope(self): def get_parent_scope(self):
return object_scope_type_registry.get("team") return object_scope_type_registry.get("team")
def get_enhanced_queryset(self): def get_enhanced_queryset(self, include_trash: bool = False) -> QuerySet:
return self.get_base_queryset().prefetch_related("team", "team__workspace") return self.get_base_queryset(include_trash).select_related("team__workspace")
def get_filter_for_scope_type(self, scope_type, scopes): def get_filter_for_scope_type(self, scope_type, scopes):
if scope_type.type == WorkspaceObjectScopeType.type: if scope_type.type == WorkspaceObjectScopeType.type:

View file

@ -28,6 +28,7 @@ from baserow.contrib.database.table.operations import (
UpdateDatabaseTableOperationType, UpdateDatabaseTableOperationType,
) )
from baserow.core.exceptions import PermissionException from baserow.core.exceptions import PermissionException
from baserow.core.handler import CoreHandler
from baserow.core.models import Application from baserow.core.models import Application
from baserow.core.notifications.operations import ( from baserow.core.notifications.operations import (
ClearNotificationsOperationType, ClearNotificationsOperationType,
@ -47,7 +48,9 @@ from baserow.core.operations import (
UpdateWorkspaceOperationType, UpdateWorkspaceOperationType,
) )
from baserow.core.registries import operation_type_registry from baserow.core.registries import operation_type_registry
from baserow.core.snapshots.handler import SnapshotHandler
from baserow.core.types import PermissionCheck from baserow.core.types import PermissionCheck
from baserow.core.utils import Progress
from baserow_enterprise.role.default_roles import default_roles from baserow_enterprise.role.default_roles import default_roles
from baserow_enterprise.role.handler import RoleAssignmentHandler from baserow_enterprise.role.handler import RoleAssignmentHandler
from baserow_enterprise.role.models import Role from baserow_enterprise.role.models import Role
@ -1775,3 +1778,72 @@ def test_check_multiple_permissions_perf(
for q in captured.captured_queries: for q in captured.captured_queries:
print(q) print(q)
print(len(captured.captured_queries)) print(len(captured.captured_queries))
@pytest.mark.django_db
def test_fetching_permissions_does_not_extra_queries_per_snapshot(
data_fixture, enterprise_data_fixture, synced_roles
):
enterprise_data_fixture.enable_enterprise()
admin = data_fixture.create_user()
viewer = data_fixture.create_user()
workspace = data_fixture.create_workspace(
members=[admin, viewer],
)
database = data_fixture.create_database_application(workspace=workspace, order=1)
table = data_fixture.create_database_table(user=admin, database=database)
role_admin = Role.objects.get(uid="ADMIN")
role_viewer = Role.objects.get(uid="VIEWER")
RoleAssignmentHandler().assign_role(admin, workspace, role=role_admin)
RoleAssignmentHandler().assign_role(viewer, workspace, role=role_viewer)
RoleAssignmentHandler().assign_role(
admin, workspace, role=role_admin, scope=database.application_ptr
)
RoleAssignmentHandler().assign_role(
viewer, workspace, role=role_viewer, scope=database.application_ptr
)
# The first time it also fetches the settings and the content types
CoreHandler().get_permissions(viewer, workspace=workspace)
with CaptureQueriesContext(connection) as captured_1:
CoreHandler().get_permissions(viewer, workspace=workspace)
# Let's create a snapshot of the database
handler = SnapshotHandler()
snapshot = handler.create(database.id, admin, "Test snapshot")
handler.perform_create(snapshot, Progress(100))
with CaptureQueriesContext(connection) as captured_2:
CoreHandler().get_permissions(viewer, workspace=workspace)
assert len(captured_2.captured_queries) == len(captured_1.captured_queries)
# Another snapshot won't increase the number of queries
snapshot = handler.create(database.id, admin, "Test snapshot 2")
handler.perform_create(snapshot, Progress(100))
with CaptureQueriesContext(connection) as captured_3:
CoreHandler().get_permissions(viewer, workspace=workspace)
assert len(captured_3.captured_queries) == len(captured_2.captured_queries)
# The same should be valid for builder applications
builder = data_fixture.create_builder_application(user=admin, workspace=workspace)
page = data_fixture.create_builder_page(builder=builder)
with CaptureQueriesContext(connection) as captured_1:
CoreHandler().get_permissions(viewer, workspace=workspace)
# Let's create a snapshot of the builder app
handler = SnapshotHandler()
snapshot = handler.create(builder.id, admin, "Test snapshot")
handler.perform_create(snapshot, Progress(100))
with CaptureQueriesContext(connection) as captured_2:
CoreHandler().get_permissions(viewer, workspace=workspace)
assert len(captured_1.captured_queries) == len(captured_2.captured_queries)