diff --git a/backend/src/baserow/contrib/database/api/views/gallery/views.py b/backend/src/baserow/contrib/database/api/views/gallery/views.py index b5dffb910d..02baed3c31 100644 --- a/backend/src/baserow/contrib/database/api/views/gallery/views.py +++ b/backend/src/baserow/contrib/database/api/views/gallery/views.py @@ -67,10 +67,11 @@ from baserow.contrib.database.views.filters import AdHocFilters from baserow.contrib.database.views.handler import ViewHandler from baserow.contrib.database.views.models import GalleryView +from baserow.contrib.database.views.operations import ListViewRowsOperationType from baserow.contrib.database.views.registries import view_type_registry from baserow.contrib.database.views.signals import view_loaded +from baserow.contrib.database.views.utils import check_permissions_with_view_fallback from baserow.core.exceptions import UserNotInWorkspace -from baserow.core.handler import CoreHandler from .errors import ERROR_GALLERY_DOES_NOT_EXIST from .pagination import GalleryLimitOffsetPagination @@ -207,12 +208,12 @@ def get( ) view_type = view_type_registry.get_by_model(view) - workspace = view.table.database.workspace - CoreHandler().check_permissions( - request.user, + check_permissions_with_view_fallback( ListRowsDatabaseTableOperationType.type, - workspace=workspace, - context=view.table, + ListViewRowsOperationType.type, + request.user, + view.table, + view, ) search = query_params.get("search") diff --git a/backend/src/baserow/contrib/database/api/views/grid/views.py b/backend/src/baserow/contrib/database/api/views/grid/views.py index a53e16915f..6e9fbf4428 100644 --- a/backend/src/baserow/contrib/database/api/views/grid/views.py +++ b/backend/src/baserow/contrib/database/api/views/grid/views.py @@ -86,13 +86,14 @@ from baserow.contrib.database.views.filters import AdHocFilters from baserow.contrib.database.views.handler import ViewHandler from baserow.contrib.database.views.models import GridView +from baserow.contrib.database.views.operations import ListViewRowsOperationType from baserow.contrib.database.views.registries import ( view_aggregation_type_registry, view_type_registry, ) from baserow.contrib.database.views.signals import view_loaded +from baserow.contrib.database.views.utils import check_permissions_with_view_fallback from baserow.core.exceptions import UserNotInWorkspace -from baserow.core.handler import CoreHandler from baserow.core.utils import split_comma_separated_string from .errors import ERROR_GRID_DOES_NOT_EXIST @@ -237,13 +238,14 @@ def get(self, request, view_id, field_options, row_metadata, query_params): ) view_type = view_type_registry.get_by_model(view) - workspace = view.table.database.workspace - CoreHandler().check_permissions( - request.user, + check_permissions_with_view_fallback( ListRowsDatabaseTableOperationType.type, - workspace=workspace, - context=view.table, + ListViewRowsOperationType.type, + request.user, + view.table, + view, ) + field_ids = get_include_exclude_field_ids( view.table, include_fields, exclude_fields ) @@ -347,11 +349,12 @@ def post(self, request, view_id, data): """ view = ViewHandler().get_view_as_user(request.user, view_id, GridView) - CoreHandler().check_permissions( - request.user, + check_permissions_with_view_fallback( ListRowsDatabaseTableOperationType.type, - workspace=view.table.database.workspace, - context=view.table, + ListViewRowsOperationType.type, + request.user, + view.table, + view, ) hidden_field_ids = get_hidden_field_ids_for_view_user(request.user, view) diff --git a/backend/src/baserow/contrib/database/apps.py b/backend/src/baserow/contrib/database/apps.py index 9b80adc638..96af0d5829 100755 --- a/backend/src/baserow/contrib/database/apps.py +++ b/backend/src/baserow/contrib/database/apps.py @@ -855,6 +855,7 @@ def ready(self): ListViewFieldsOperationType, ListViewFilterOperationType, ListViewGroupByOperationType, + ListViewRowsOperationType, ListViewsOperationType, ListViewSortOperationType, OrderViewsOperationType, @@ -892,6 +893,7 @@ def ready(self): operation_type_registry.register(ReadViewRowOperationType()) operation_type_registry.register(ReadAdjacentViewRowOperationType()) operation_type_registry.register(ListViewFieldsOperationType()) + operation_type_registry.register(ListViewRowsOperationType()) operation_type_registry.register(CreateViewRowOperationType()) operation_type_registry.register(UpdateViewRowOperationType()) operation_type_registry.register(DeleteViewRowOperationType()) diff --git a/backend/src/baserow/contrib/database/rows/handler.py b/backend/src/baserow/contrib/database/rows/handler.py index 89e265cc62..e09f6b8643 100644 --- a/backend/src/baserow/contrib/database/rows/handler.py +++ b/backend/src/baserow/contrib/database/rows/handler.py @@ -77,7 +77,6 @@ from baserow.core.exceptions import ( CannotCalculateIntermediateOrder, PermissionDenied, - PermissionException, ) from baserow.core.handler import CoreHandler from baserow.core.psycopg import is_unique_violation_error, sql @@ -741,59 +740,14 @@ def _check_permissions_with_view_fallback( :return: """ - table_check = PermissionCheck( - user, - table_operation, - context=table, - ) - view_check = PermissionCheck( - user, - view_operation, - context=view, + from baserow.contrib.database.views.utils import ( + check_permissions_with_view_fallback, ) - checks = [table_check] - if view is not None: - checks.append(view_check) - - # Check multiple permissions regardless because if a view is provided, we don't - # want to execute multiple queries in order to check if the permission check - # should fall back on the view. - check_results = CoreHandler().check_multiple_permissions( - checks, - workspace=table.database.workspace, - return_permissions_exceptions=True, + check_permissions_with_view_fallback( + table_operation, view_operation, user, table, view, row_ids ) - if check_results[table_check] is True: - return - - if ( - view is not None - # Because the user wants to change rows in a specific table, we must make - # sure that the provided view belongs to that table. Otherwise, it would - # result in a security bug. - and view.table_id == table.id - # The view ownership type should also allow modifying rows directly in - # the view. The rows are provided because some additional permission - # checks might need to be done in order to make sure that the user is - # allowed to modify the provided rows. - and view_ownership_type_registry.get(view.ownership_type).can_modify_rows( - view, - row_ids, - ) - and check_results[view_check] is True - ): - return - - if isinstance(check_results[table_check], PermissionException): - raise check_results[table_check] - - if isinstance(check_results[view_check], PermissionException): - raise check_results[view_check] - - raise PermissionDenied(actor=user) - def create_row( self, user: AbstractUser, diff --git a/backend/src/baserow/contrib/database/views/operations.py b/backend/src/baserow/contrib/database/views/operations.py index 46a5a31422..d1131cc9c3 100644 --- a/backend/src/baserow/contrib/database/views/operations.py +++ b/backend/src/baserow/contrib/database/views/operations.py @@ -32,6 +32,10 @@ class ViewRowOperationType(OperationType, abc.ABC): context_scope_name = DatabaseViewObjectScopeType.type +class ListViewRowsOperationType(ViewRowOperationType): + type = "database.table.view.list_rows" + + class ReadViewRowOperationType(ViewRowOperationType): type = "database.table.view.read_row" diff --git a/backend/src/baserow/contrib/database/views/utils.py b/backend/src/baserow/contrib/database/views/utils.py index 2ef76458bb..4995ab5b06 100644 --- a/backend/src/baserow/contrib/database/views/utils.py +++ b/backend/src/baserow/contrib/database/views/utils.py @@ -1,7 +1,17 @@ -from typing import Any, Dict, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple +from django.contrib.auth.models import AbstractUser from django.db.models.aggregates import Aggregate, Count +from baserow.core.exceptions import PermissionDenied, PermissionException +from baserow.core.handler import CoreHandler +from baserow.core.registries import OperationType +from baserow.core.types import PermissionCheck + +if TYPE_CHECKING: + from baserow.contrib.database.table.models import Table + from baserow.contrib.database.views.models import View + class AnnotatedAggregation: """ @@ -77,3 +87,86 @@ def serialize_row_for_action(row, model) -> Tuple[Dict[str, Any], Dict[str, Any] if not f["type"].read_only } return serialized_values, fields_metadata + + +def check_permissions_with_view_fallback( + table_operation: OperationType, + view_operation: OperationType, + user: AbstractUser, + table: "Table", + view: Optional["View"], + row_ids: Optional[List[int]] = None, +): + """ + Checks if the user has permission to the provided table object. If not, it will + fall back to the view permissions, if the view ownership type allows it, then + check if the user has permissions to the view. + + :param table_operation: The permission on table level to check. If this check + passes, then no exception will be raised. + :param view_operation: The permission on view level to check. If both this check + succeeds and the view ownership type allows it, then no exception will be + raised. + :param user: The user on whose behalf the permissions are checked. + :param table: The table where to check the permissions for. + :param view: Optionally provide the view where to check permissions for as + fallback. + :param row_ids: Optionally the row ids that are modified. + :raises PermissionDenied: If the user does not have access to both the table + and view. + """ + + from baserow.contrib.database.views.registries import view_ownership_type_registry + + table_check = PermissionCheck( + user, + table_operation, + context=table, + ) + view_check = PermissionCheck( + user, + view_operation, + context=view, + ) + + checks = [table_check] + if view is not None: + checks.append(view_check) + + # Check multiple permissions regardless because if a view is provided, we don't + # want to execute multiple queries in order to check if the permission check + # should fall back on the view. + check_results = CoreHandler().check_multiple_permissions( + checks, + workspace=table.database.workspace, + return_permissions_exceptions=True, + ) + + if check_results[table_check] is True: + return + + if ( + view is not None + # Because the user wants to access rows in a specific table, we must make + # sure that the provided view belongs to that table. Otherwise, it would + # result in a security bug. + and view.table_id == table.id + # The view ownership type should also allow accessing rows directly in + # the view. The rows are provided because some additional permission + # checks might need to be done in order to make sure that the user is + # allowed to access the provided rows. + and view_ownership_type_registry.get(view.ownership_type).can_modify_rows( + view, + row_ids, + ) + and check_results[view_check] is True + ): + return + + if isinstance(check_results[table_check], PermissionException): + raise check_results[table_check] + + if view is not None and isinstance(check_results[view_check], PermissionException): + raise check_results[view_check] + + raise PermissionDenied(actor=user) diff --git a/backend/src/baserow/core/apps.py b/backend/src/baserow/core/apps.py index bfdf2e9cff..e26ee03cbb 100755 --- a/backend/src/baserow/core/apps.py +++ b/backend/src/baserow/core/apps.py @@ -204,6 +204,7 @@ def ready(self): DeleteWorkspaceOperationType, DeleteWorkspaceUserOperationType, DuplicateApplicationOperationType, + ExportWorkspaceOperationType, ListApplicationsWorkspaceOperationType, ListInvitationsWorkspaceOperationType, ListWorkspacesOperationType, @@ -247,6 +248,7 @@ def ready(self): operation_type_registry.register(ListWorkspacesOperationType()) operation_type_registry.register(UpdateWorkspaceInvitationType()) operation_type_registry.register(ReadWorkspaceOperationType()) + operation_type_registry.register(ExportWorkspaceOperationType()) operation_type_registry.register(UpdateWorkspaceOperationType()) operation_type_registry.register(ListWorkspaceUsersWorkspaceOperationType()) operation_type_registry.register(OrderApplicationsOperationType()) diff --git a/backend/src/baserow/core/import_export/handler.py b/backend/src/baserow/core/import_export/handler.py index f4a303c44e..15d4a258df 100644 --- a/backend/src/baserow/core/import_export/handler.py +++ b/backend/src/baserow/core/import_export/handler.py @@ -49,7 +49,7 @@ ImportExportTrustedSource, Workspace, ) -from baserow.core.operations import ReadWorkspaceOperationType +from baserow.core.operations import ExportWorkspaceOperationType from baserow.core.registries import ImportExportConfig, application_type_registry from baserow.core.signals import application_created, application_imported from baserow.core.storage import ( @@ -99,7 +99,7 @@ def get_workspace_or_raise(self, user: AbstractUser, workspace_id: int): core_handler.check_permissions( user, - ReadWorkspaceOperationType.type, + ExportWorkspaceOperationType.type, workspace=workspace, context=workspace, ) diff --git a/backend/src/baserow/core/job_types.py b/backend/src/baserow/core/job_types.py index c8928652b7..5f1bb8dc52 100755 --- a/backend/src/baserow/core/job_types.py +++ b/backend/src/baserow/core/job_types.py @@ -66,8 +66,8 @@ ) from baserow.core.operations import ( CreateApplicationsWorkspaceOperationType, + ExportWorkspaceOperationType, ListApplicationsWorkspaceOperationType, - ReadWorkspaceOperationType, ) from baserow.core.service import CoreService from baserow.core.utils import Progress @@ -300,7 +300,7 @@ def fetch_applications(self, user, workspace, application_ids): CoreHandler().check_permissions( user, - ReadWorkspaceOperationType.type, + ExportWorkspaceOperationType.type, workspace=workspace, context=workspace, ) diff --git a/backend/src/baserow/core/operations.py b/backend/src/baserow/core/operations.py index 8876afae0a..f10d3f1a46 100644 --- a/backend/src/baserow/core/operations.py +++ b/backend/src/baserow/core/operations.py @@ -36,6 +36,10 @@ class DeleteWorkspaceOperationType(WorkspaceCoreOperationType): type = "workspace.delete" +class ExportWorkspaceOperationType(WorkspaceCoreOperationType): + type = "workspace.export" + + class RestoreWorkspaceOperationType(WorkspaceCoreOperationType): type = "workspace.restore" diff --git a/enterprise/backend/src/baserow_enterprise/role/default_roles.py b/enterprise/backend/src/baserow_enterprise/role/default_roles.py index b059966c65..c654b09443 100755 --- a/enterprise/backend/src/baserow_enterprise/role/default_roles.py +++ b/enterprise/backend/src/baserow_enterprise/role/default_roles.py @@ -168,6 +168,7 @@ ListViewFieldsOperationType, ListViewFilterOperationType, ListViewGroupByOperationType, + ListViewRowsOperationType, ListViewsOperationType, ListViewSortOperationType, OrderViewsOperationType, @@ -226,6 +227,7 @@ DeleteWorkspaceOperationType, DeleteWorkspaceUserOperationType, DuplicateApplicationOperationType, + ExportWorkspaceOperationType, ListApplicationsWorkspaceOperationType, ListInvitationsWorkspaceOperationType, ListWorkspaceUsersWorkspaceOperationType, @@ -342,40 +344,20 @@ # Note that the read only role can automatically be assigned to the user if they have a # role assigned on a higher scope. If the user for example has `NO_ACCESS` to a # database, but has been given `EDITOR` role to the table, then they will automatically -# get the viewer role of the database. The individual endpoints or filter queryset -# rules must prevent accidental data exposure. +# get the read only role of the database. The individual endpoints or filter queryset +# rules must prevent accidental data exposure. Only add operations related to +# endpoints that filter items that the user does not have access to like listing +# workspaces, listing applications, etc. default_roles[READ_ONLY_ROLE_UID].extend( default_roles[NO_ACCESS_ROLE_UID] + [ - ReadWorkspaceOperationType, - ReadTeamOperationType, ListTeamsOperationType, ListApplicationsWorkspaceOperationType, ListTablesDatabaseTableOperationType, + ListViewsOperationType, + ReadWorkspaceOperationType, ReadApplicationOperationType, ReadDatabaseTableOperationType, - ReadViewOperationType, - ListViewSortOperationType, - ReadViewFieldOptionsOperationType, - ReadViewDecorationOperationType, - ListViewDecorationOperationType, - ListViewFilterOperationType, - ListViewsOperationType, - ListAggregationsViewOperationType, - ReadAggregationsViewOperationType, - ReadViewFilterOperationType, - ReadViewsOrderOperationType, - ReadViewSortOperationType, - ListViewGroupByOperationType, - ReadViewGroupByOperationType, - ListBuilderWorkflowActionsPageOperationType, - ReadBuilderWorkflowActionOperationType, - ReadViewFilterGroupOperationType, - ReadWidgetOperationType, - ListWidgetsOperationType, - ListDashboardDataSourcesOperationType, - ReadDashboardDataSourceOperationType, - ListRowsDatabaseTableOperationType, ] ) default_roles[VIEWER_ROLE_UID].extend( @@ -399,6 +381,29 @@ ReadAdjacentRowDatabaseRowOperationType, ReadAdjacentViewRowOperationType, ListRowNamesDatabaseTableOperationType, + ReadTeamOperationType, + ReadViewOperationType, + ReadViewFieldOptionsOperationType, + ReadViewDecorationOperationType, + ListViewSortOperationType, + ListViewDecorationOperationType, + ListViewFilterOperationType, + ListAggregationsViewOperationType, + ReadAggregationsViewOperationType, + ReadViewFilterOperationType, + ReadViewsOrderOperationType, + ReadViewSortOperationType, + ListViewGroupByOperationType, + ReadViewGroupByOperationType, + ListBuilderWorkflowActionsPageOperationType, + ReadBuilderWorkflowActionOperationType, + ReadViewFilterGroupOperationType, + ReadWidgetOperationType, + ListWidgetsOperationType, + ListDashboardDataSourcesOperationType, + ReadDashboardDataSourceOperationType, + ListRowsDatabaseTableOperationType, + ListViewRowsOperationType, ] ) default_roles[COMMENTER_ROLE_UID].extend( @@ -608,5 +613,6 @@ ListWorkspaceAuditLogEntriesOperationType, ReadRoleViewOperationType, UpdateRoleViewOperationType, + ExportWorkspaceOperationType, ] ) diff --git a/enterprise/backend/tests/baserow_enterprise_tests/views/test_restricted_view.py b/enterprise/backend/tests/baserow_enterprise_tests/views/test_restricted_view.py index e5f2f27a03..025913c3bf 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/views/test_restricted_view.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/views/test_restricted_view.py @@ -2398,3 +2398,71 @@ def test_default_values_get_view_editor_sees_visible_only( field_ids = {dv["field"] for dv in default_values} assert ctx["visible_field"].id in field_ids assert ctx["hidden_field"].id not in field_ids + + +@pytest.mark.django_db +@override_settings(DEBUG=True) +def test_editor_with_view_access_can_list_rows_for_all_view_types( + enterprise_data_fixture, + premium_data_fixture, + api_client, +): + """ + Tests that a user who has NO_ACCESS at workspace level but EDITOR on a + specific restricted view can still list rows through all view type + endpoints via the view-level permission fallback. + """ + + enterprise_data_fixture.enable_enterprise() + + user, token = enterprise_data_fixture.create_user_and_token() + user2, token2 = enterprise_data_fixture.create_user_and_token() + workspace = enterprise_data_fixture.create_workspace(user=user, members=[user2]) + database = enterprise_data_fixture.create_database_application(workspace=workspace) + table = enterprise_data_fixture.create_database_table(database=database) + text_field = enterprise_data_fixture.create_text_field(table=table, primary=True) + + editor_role = Role.objects.get(uid="EDITOR") + no_access_role = Role.objects.get(uid="NO_ACCESS") + RoleAssignmentHandler().assign_role( + user2, workspace, role=no_access_role, scope=workspace + ) + + RowHandler().create_row(user, table, values={f"field_{text_field.id}": "a"}) + + for view_type in view_type_registry.get_all(): + if view_type.type not in view_type_url_mapping: + continue + + view_path, fixture_create, response_path = view_type_url_mapping[view_type.type] + + view = getattr(premium_data_fixture, fixture_create)( + table=table, ownership_type=RestrictedViewOwnershipType.type + ) + + RoleAssignmentHandler().assign_role( + user2, + workspace, + role=editor_role, + scope=View.objects.get(id=view.id), + ) + + for field in table.field_set.all(): + if field.specific_class == DateField: + table.get_model().objects.all().update( + **{f"field_{field.id}": datetime(2021, 1, 1)} + ) + + query_param = "?from_timestamp=2021-01-01&to_timestamp=2021-02-01" + response = api_client.get( + reverse(view_path, kwargs={"view_id": view.id}) + query_param, + format="json", + HTTP_AUTHORIZATION=f"JWT {token2}", + ) + assert response.status_code == HTTP_200_OK, ( + f"Editor with view-level access should be able to list rows in " + f"{view_type.type}" + ) + response_json = response.json() + rows = get_value_at_path(response_json, response_path) + assert len(rows) == 1, f"Editor should see the row in {view_type.type}" diff --git a/premium/backend/src/baserow_premium/api/views/calendar/views.py b/premium/backend/src/baserow_premium/api/views/calendar/views.py index 5ad1366a14..3152877310 100644 --- a/premium/backend/src/baserow_premium/api/views/calendar/views.py +++ b/premium/backend/src/baserow_premium/api/views/calendar/views.py @@ -63,12 +63,13 @@ ) from baserow.contrib.database.views.filters import AdHocFilters from baserow.contrib.database.views.handler import ViewHandler +from baserow.contrib.database.views.operations import ListViewRowsOperationType from baserow.contrib.database.views.registries import view_type_registry from baserow.contrib.database.views.signals import view_loaded +from baserow.contrib.database.views.utils import check_permissions_with_view_fallback from baserow.core.action.registries import action_type_registry from baserow.core.db import specific_queryset from baserow.core.exceptions import UserNotInWorkspace -from baserow.core.handler import CoreHandler from baserow_premium.api.views.calendar.errors import ( ERROR_CALENDAR_VIEW_HAS_NO_DATE_FIELD, ) @@ -215,11 +216,12 @@ def get(self, request, view_id, field_options, row_metadata, query_params): PREMIUM, request.user, workspace ) - CoreHandler().check_permissions( - request.user, + check_permissions_with_view_fallback( ListRowsDatabaseTableOperationType.type, - workspace=workspace, - context=view.table, + ListViewRowsOperationType.type, + request.user, + view.table, + view, ) date_field = view.date_field diff --git a/premium/backend/src/baserow_premium/api/views/kanban/views.py b/premium/backend/src/baserow_premium/api/views/kanban/views.py index 1a837ad9e9..ccf174e2fe 100644 --- a/premium/backend/src/baserow_premium/api/views/kanban/views.py +++ b/premium/backend/src/baserow_premium/api/views/kanban/views.py @@ -44,10 +44,11 @@ ) from baserow.contrib.database.views.filters import AdHocFilters from baserow.contrib.database.views.handler import ViewHandler +from baserow.contrib.database.views.operations import ListViewRowsOperationType from baserow.contrib.database.views.registries import view_type_registry from baserow.contrib.database.views.signals import view_loaded +from baserow.contrib.database.views.utils import check_permissions_with_view_fallback from baserow.core.exceptions import UserNotInWorkspace -from baserow.core.handler import CoreHandler from baserow_premium.api.views.errors import ERROR_INVALID_SELECT_OPTION_PARAMETER from baserow_premium.api.views.exceptions import InvalidSelectOptionParameter from baserow_premium.license.features import PREMIUM @@ -177,11 +178,12 @@ def get(self, request, view_id, field_options, row_metadata): PREMIUM, request.user, workspace ) - CoreHandler().check_permissions( - request.user, + check_permissions_with_view_fallback( ListRowsDatabaseTableOperationType.type, - workspace=workspace, - context=view.table, + ListViewRowsOperationType.type, + request.user, + view.table, + view, ) single_select_option_field = view.single_select_field diff --git a/premium/backend/src/baserow_premium/api/views/timeline/views.py b/premium/backend/src/baserow_premium/api/views/timeline/views.py index ab66a98497..1d00ae1c64 100644 --- a/premium/backend/src/baserow_premium/api/views/timeline/views.py +++ b/premium/backend/src/baserow_premium/api/views/timeline/views.py @@ -68,9 +68,10 @@ ) from baserow.contrib.database.views.filters import AdHocFilters from baserow.contrib.database.views.handler import ViewHandler +from baserow.contrib.database.views.operations import ListViewRowsOperationType from baserow.contrib.database.views.signals import view_loaded +from baserow.contrib.database.views.utils import check_permissions_with_view_fallback from baserow.core.exceptions import UserNotInWorkspace -from baserow.core.handler import CoreHandler from baserow_premium.api.views.timeline.errors import ( ERROR_TIMELINE_VIEW_HAS_INVALID_DATE_SETTINGS, ) @@ -227,11 +228,12 @@ def get(self, request, view_id, field_options, row_metadata, query_params): PREMIUM, request.user, workspace ) - CoreHandler().check_permissions( - request.user, + check_permissions_with_view_fallback( ListRowsDatabaseTableOperationType.type, - workspace=workspace, - context=view.table, + ListViewRowsOperationType.type, + request.user, + view.table, + view, ) field_ids = get_include_exclude_field_ids( diff --git a/premium/backend/src/baserow_premium/permission_manager.py b/premium/backend/src/baserow_premium/permission_manager.py index 75e1fc2082..05c0fcff97 100644 --- a/premium/backend/src/baserow_premium/permission_manager.py +++ b/premium/backend/src/baserow_premium/permission_manager.py @@ -27,6 +27,7 @@ ListViewFieldsOperationType, ListViewFilterOperationType, ListViewGroupByOperationType, + ListViewRowsOperationType, ListViewsOperationType, ListViewSortOperationType, ReadAdjacentViewRowOperationType, @@ -130,6 +131,7 @@ def __init__(self): DeleteViewDecorationOperationType.type, ReadViewRowOperationType.type, ReadAdjacentViewRowOperationType.type, + ListViewRowsOperationType.type, CreateViewRowOperationType.type, UpdateViewRowOperationType.type, DeleteViewRowOperationType.type, diff --git a/web-frontend/modules/core/components/workspace/WorkspaceContext.vue b/web-frontend/modules/core/components/workspace/WorkspaceContext.vue index 5d89272970..ab83629d29 100644 --- a/web-frontend/modules/core/components/workspace/WorkspaceContext.vue +++ b/web-frontend/modules/core/components/workspace/WorkspaceContext.vue @@ -15,7 +15,7 @@ >