Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion backend/src/baserow/api/admin/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from baserow.api.user.registries import member_data_registry
from baserow.api.user.schemas import authenticate_user_schema
from baserow.api.user.serializers import get_all_user_data_serialized
from baserow.core.action.registries import action_type_registry
from baserow.core.admin.users.actions import AdminDisableTwoFactorAuthActionType
from baserow.core.admin.users.exceptions import (
CannotDeactivateYourselfException,
Expand Down Expand Up @@ -274,7 +275,9 @@ def delete(self, request, user_id):
Removes the two-factor authentication of the specified user.
"""

AdminDisableTwoFactorAuthActionType.do(request.user, int(user_id))
action_type_registry.get_by_type(AdminDisableTwoFactorAuthActionType).do(
request.user, int(user_id)
)

return Response(status=204)

Expand Down
Empty file.
Empty file.
132 changes: 132 additions & 0 deletions backend/src/baserow/contrib/database/admin/views/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import dataclasses

from django.contrib.auth.models import AbstractUser
from django.utils.translation import gettext_lazy as _

from baserow.contrib.database.action.scopes import (
VIEW_ACTION_CONTEXT,
ViewActionScopeType,
)
from baserow.contrib.database.admin.views.handler import ViewsAdminHandler
from baserow.contrib.database.views.models import View
from baserow.core.action.registries import (
ActionScopeStr,
ActionType,
ActionTypeDescription,
)


class UpdateViewPublicAdminActionType(ActionType):
type = "admin_update_view_public"
description = ActionTypeDescription(
_("Admin changed view public sharing"),
_(
'Admin "%(user_email)s" (%(user_id)s) changed the publicly shared '
"state to %(public)s "
),
VIEW_ACTION_CONTEXT,
)
analytics_params = [
"view_id",
"table_id",
"database_id",
"public",
]

@dataclasses.dataclass
class Params:
user_id: int
user_email: str
view_id: int
view_name: str
table_id: int
table_name: str
database_id: int
database_name: str
public: bool
original_public: bool

@classmethod
def do(cls, user: AbstractUser, view: View, public: bool) -> View:
original_public = view.public

view = ViewsAdminHandler().update_view_public(user, view, public)

cls.register_action(
user=user,
params=cls.Params(
user.id,
user.email,
view.id,
view.name,
view.table.id,
view.table.name,
view.table.database.id,
view.table.database.name,
view.public,
original_public,
),
scope=cls.scope(view.id),
workspace=view.table.database.workspace,
)
return view

@classmethod
def scope(cls, view_id: int) -> ActionScopeStr:
return ViewActionScopeType.value(view_id)


class RotateViewSlugAdminActionType(ActionType):
type = "admin_rotate_view_slug"
description = ActionTypeDescription(
_("Admin rotated view slug"),
_('Admin "%(user_email)s" (%(user_id)s) rotated the public slug URL '),
VIEW_ACTION_CONTEXT,
)
analytics_params = [
"view_id",
"table_id",
"database_id",
]

@dataclasses.dataclass
class Params:
user_id: int
user_email: str
view_id: int
view_name: str
table_id: int
table_name: str
database_id: int
database_name: str
slug: str
original_slug: str

@classmethod
def do(cls, user: AbstractUser, view: View) -> View:
original_slug = view.slug

view = ViewsAdminHandler().rotate_view_slug(user, view)

cls.register_action(
user=user,
params=cls.Params(
user.id,
user.email,
view.id,
view.name,
view.table.id,
view.table.name,
view.table.database.id,
view.table.database.name,
view.slug,
original_slug,
),
scope=cls.scope(view.id),
workspace=view.table.database.workspace,
)
return view

@classmethod
def scope(cls, view_id: int) -> ActionScopeStr:
return ViewActionScopeType.value(view_id)
81 changes: 81 additions & 0 deletions backend/src/baserow/contrib/database/admin/views/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from copy import deepcopy

from django.contrib.auth.models import AbstractUser

from baserow.contrib.database.table.cache import invalidate_table_in_model_cache
from baserow.contrib.database.views.exceptions import CannotShareViewTypeError
from baserow.contrib.database.views.models import View
from baserow.contrib.database.views.registries import view_type_registry
from baserow.contrib.database.views.signals import view_updated
from baserow.core.exceptions import IsNotAdminError


class ViewsAdminHandler:
def update_view_public(
self, requesting_user: AbstractUser, view: View, public: bool
) -> View:
"""
Changes whether the provided view is publicly shared. This deliberately skips
the workspace permission checks because the requesting staff user is typically
not a member of the workspace. It's used by instance admins to intervene when a
publicly shared view is abused.

:param requesting_user: The staff user on whose behalf the view is updated.
:param view: The view that must be updated.
:param public: Whether the view must be publicly shared.
:raises IsNotAdminError: When the requesting user is not staff.
:raises CannotShareViewTypeError: When the view type doesn't support sharing.
:return: The updated view instance.
"""

self._raise_if_not_permitted(requesting_user)
self._raise_if_not_shareable(view)

view = view.specific
old_view = deepcopy(view)
view.public = public
view.save()

view_updated.send(self, view=view, user=requesting_user, old_view=old_view)

return view

def rotate_view_slug(self, requesting_user: AbstractUser, view: View) -> View:
"""
Rotates the slug of the provided view, permanently invalidating the current
public URL. This deliberately skips the workspace permission checks because
the requesting staff user is typically not a member of the workspace.

:param requesting_user: The staff user on whose behalf the view is updated.
:param view: The view whose slug must be rotated.
:raises IsNotAdminError: When the requesting user is not staff.
:raises CannotShareViewTypeError: When the view type doesn't support sharing.
:return: The updated view instance.
"""

self._raise_if_not_permitted(requesting_user)
self._raise_if_not_shareable(view)

view = view.specific
old_view = deepcopy(view)
view.rotate_slug()
view.save()

# Invalidate the model cache because fields could be depending on that specific
# model slug, like the edit row link field.
invalidate_table_in_model_cache(view.table_id)

view_updated.send(self, view=view, user=requesting_user, old_view=old_view)

return view

@staticmethod
def _raise_if_not_permitted(requesting_user: AbstractUser):
if not requesting_user.is_staff:
raise IsNotAdminError()

@staticmethod
def _raise_if_not_shareable(view: View):
view_type = view_type_registry.get_by_model(view.specific_class)
if not view_type.can_share:
raise CannotShareViewTypeError()
Empty file.
9 changes: 9 additions & 0 deletions backend/src/baserow/contrib/database/api/admin/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.urls import include, path

from .views import urls as views_urls

app_name = "baserow.contrib.database.api.admin"

urlpatterns = [
path("views/", include(views_urls, namespace="views")),
]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers

from baserow.contrib.database.views.models import View
from baserow.contrib.database.views.registries import view_type_registry


class AdminViewSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
table_id = serializers.IntegerField(read_only=True)
table_name = serializers.CharField(source="table.name", read_only=True)
database_id = serializers.IntegerField(source="table.database_id", read_only=True)
database_name = serializers.CharField(source="table.database.name", read_only=True)
workspace_id = serializers.IntegerField(
source="table.database.workspace_id", read_only=True
)
workspace_name = serializers.CharField(
source="table.database.workspace.name", read_only=True
)
public_view_has_password = serializers.BooleanField(read_only=True)
owned_by_id = serializers.IntegerField(read_only=True)
owned_by_username = serializers.SerializerMethodField()

class Meta:
model = View
fields = (
"id",
"name",
"slug",
"type",
"table_id",
"table_name",
"database_id",
"database_name",
"workspace_id",
"workspace_name",
"public",
"public_view_has_password",
"owned_by_id",
"owned_by_username",
"ownership_type",
"created_on",
)

@extend_schema_field(OpenApiTypes.STR)
def get_type(self, instance):
return view_type_registry.get_by_model(instance.specific_class).type

@extend_schema_field(OpenApiTypes.STR)
def get_owned_by_username(self, instance):
return instance.owned_by.username if instance.owned_by_id else None


class AdminViewUpdateSerializer(serializers.ModelSerializer):
public = serializers.BooleanField(
required=True,
help_text="Indicates whether the view must be publicly shared.",
)

class Meta:
model = View
fields = ("public",)
15 changes: 15 additions & 0 deletions backend/src/baserow/contrib/database/api/admin/views/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from django.urls import re_path

from .views import AdminViewRotateSlugView, AdminViewsView, AdminViewView

app_name = "baserow.contrib.database.api.admin.views"

urlpatterns = [
re_path(r"^$", AdminViewsView.as_view(), name="list"),
re_path(r"^(?P<view_id>[0-9]+)/$", AdminViewView.as_view(), name="edit"),
re_path(
r"^(?P<view_id>[0-9]+)/rotate-slug/$",
AdminViewRotateSlugView.as_view(),
name="rotate_slug",
),
]
Loading
Loading