Skip to content

Commit 438a4b9

Browse files
silvestridCopilot
andauthored
fix: remove workspace invite messages and pending cap (baserow#5222)
* fix: remove workspace invite messages and pending cap Workspace invitations no longer accept custom messages, which removes the main spam payload from the flow. Drop the BASEROW_MAX_PENDING_WORKSPACE_INVITES limit because deleting and recreating invites made it ineffective. * Update backend/src/baserow/core/migrations/0114_alter_workspaceinvitation_message.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 0cd6824 commit 438a4b9

23 files changed

Lines changed: 59 additions & 298 deletions

File tree

backend/src/baserow/api/workspaces/invitations/errors.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,3 @@
1010
HTTP_400_BAD_REQUEST,
1111
"Your email address does not match with the invitation.",
1212
)
13-
ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED = (
14-
"ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED",
15-
HTTP_400_BAD_REQUEST,
16-
"The maximum number of pending invites for this workspace has been reached. "
17-
"Please wait for some invitees to accept the invite or cancel the existing ones.",
18-
)

backend/src/baserow/api/workspaces/invitations/serializers.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ class Meta:
1313
"workspace",
1414
"email",
1515
"permissions",
16-
"message",
1716
"created_on",
1817
)
1918
extra_kwargs = {"id": {"read_only": True}}
@@ -28,7 +27,7 @@ class CreateWorkspaceInvitationSerializer(serializers.ModelSerializer):
2827

2928
class Meta:
3029
model = WorkspaceInvitation
31-
fields = ("email", "permissions", "message", "base_url")
30+
fields = ("email", "permissions", "base_url")
3231

3332

3433
class UpdateWorkspaceInvitationSerializer(serializers.ModelSerializer):
@@ -54,13 +53,11 @@ class Meta:
5453
"invited_by",
5554
"workspace",
5655
"email",
57-
"message",
5856
"created_on",
5957
"email_exists",
6058
)
6159
extra_kwargs = {
6260
"id": {"read_only": True},
63-
"message": {"read_only": True},
6461
"created_on": {"read_only": True},
6562
}
6663

backend/src/baserow/api/workspaces/invitations/views.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from baserow.api.workspaces.invitations.errors import (
2727
ERROR_GROUP_INVITATION_DOES_NOT_EXIST,
2828
ERROR_GROUP_INVITATION_EMAIL_MISMATCH,
29-
ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED,
3029
)
3130
from baserow.api.workspaces.serializers import WorkspaceUserWorkspaceSerializer
3231
from baserow.api.workspaces.users.errors import ERROR_GROUP_USER_ALREADY_EXISTS
@@ -40,7 +39,6 @@
4039
)
4140
from baserow.core.exceptions import (
4241
BaseURLHostnameNotAllowed,
43-
MaxNumberOfPendingWorkspaceInvitesReached,
4442
UserInvalidWorkspacePermissionsError,
4543
UserNotInWorkspace,
4644
WorkspaceDoesNotExist,
@@ -68,10 +66,9 @@
6866

6967
class WorkspaceInvitationsView(APIView, SortableViewMixin, SearchableViewMixin):
7068
permission_classes = (IsAuthenticated,)
71-
search_fields = ["email", "message"]
69+
search_fields = ["email"]
7270
sort_field_mapping = {
7371
"email": "email",
74-
"message": "message",
7572
}
7673

7774
@extend_schema(
@@ -157,7 +154,6 @@ def get(self, request, workspace_id, query_params):
157154
"ERROR_USER_NOT_IN_GROUP",
158155
"ERROR_USER_INVALID_GROUP_PERMISSIONS",
159156
"ERROR_REQUEST_BODY_VALIDATION",
160-
"ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED",
161157
]
162158
),
163159
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
@@ -172,7 +168,6 @@ def get(self, request, workspace_id, query_params):
172168
UserInvalidWorkspacePermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS,
173169
WorkspaceUserAlreadyExists: ERROR_GROUP_USER_ALREADY_EXISTS,
174170
BaseURLHostnameNotAllowed: ERROR_HOSTNAME_IS_NOT_ALLOWED,
175-
MaxNumberOfPendingWorkspaceInvitesReached: ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED,
176171
}
177172
)
178173
def post(self, request, data, workspace_id):
@@ -187,7 +182,6 @@ def post(self, request, data, workspace_id):
187182
data["email"],
188183
data["permissions"],
189184
data["base_url"],
190-
data.get("message", ""),
191185
)
192186

193187
return Response(WorkspaceInvitationSerializer(workspace_invitation).data)

backend/src/baserow/config/settings/base.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,12 +1205,6 @@ def __setitem__(self, key, value):
12051205
BASEROW_USER_LOG_ENTRY_RETENTION_DAYS = int(
12061206
os.getenv("BASEROW_USER_LOG_ENTRY_RETENTION_DAYS", 61)
12071207
)
1208-
# The maximum number of pending invites that a workspace can have. If `0` then
1209-
# unlimited invites are allowed, which is the default value.
1210-
BASEROW_MAX_PENDING_WORKSPACE_INVITES = int(
1211-
os.getenv("BASEROW_MAX_PENDING_WORKSPACE_INVITES", 0)
1212-
)
1213-
12141208
BASEROW_IMPORT_EXPORT_RESOURCE_CLEANUP_INTERVAL_MINUTES = int(
12151209
os.getenv("BASEROW_IMPORT_EXPORT_RESOURCE_CLEANUP_INTERVAL_MINUTES", 5)
12161210
)

backend/src/baserow/core/actions.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,6 @@ def do(
817817
email: str,
818818
permissions: str,
819819
base_url: str,
820-
message: str = "",
821820
) -> WorkspaceInvitation:
822821
"""
823822
Creates a new workspace invitation for the given email address and sends out an
@@ -829,7 +828,7 @@ def do(
829828
"""
830829

831830
workspace_invitation = CoreHandler().create_workspace_invitation(
832-
user, workspace, email, permissions, base_url, message
831+
user, workspace, email, permissions, base_url
833832
)
834833

835834
cls.register_action(

backend/src/baserow/core/exceptions.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,6 @@ class WorkspaceUserAlreadyExists(Exception):
7676
"""
7777

7878

79-
class MaxNumberOfPendingWorkspaceInvitesReached(Exception):
80-
"""
81-
Raised when the maximum number of pending workspace invites has been reached.
82-
This value is configurable via the `BASEROW_MAX_PENDING_WORKSPACE_INVITES` setting.
83-
"""
84-
85-
8679
class WorkspaceUserIsLastAdmin(Exception):
8780
"""
8881
Raised when the last admin of the workspace tries to leave it. This will leave the

backend/src/baserow/core/handler.py

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
DuplicateApplicationMaxLocksExceededException,
4040
InvalidPermissionContext,
4141
LastAdminOfWorkspace,
42-
MaxNumberOfPendingWorkspaceInvitesReached,
4342
PermissionDenied,
4443
PermissionException,
4544
TemplateDoesNotExist,
@@ -1089,35 +1088,30 @@ def get_workspace_invitation(self, workspace_invitation_id, base_queryset=None):
10891088
return workspace_invitation
10901089

10911090
def create_workspace_invitation(
1092-
self, user, workspace, email, permissions, base_url, message=""
1093-
):
1091+
self,
1092+
user: AbstractUser,
1093+
workspace: Workspace,
1094+
email: str,
1095+
permissions: str,
1096+
base_url: str,
1097+
) -> WorkspaceInvitation:
10941098
"""
10951099
Creates a new workspace invitation for the given email address and sends out an
10961100
email containing the invitation.
10971101
10981102
:param user: The user on whose behalf the invitation is created.
1099-
:type user: User
11001103
:param workspace: The workspace for which the user is invited.
1101-
:type workspace: Workspace
11021104
:param email: The email address of the person that is invited to the workspace.
11031105
Can be an existing or not existing user.
1104-
:type email: str
11051106
:param permissions: The workspace permissions that the user will get once they
11061107
have accepted the invitation.
1107-
:type permissions: str
11081108
:param base_url: The base url of the frontend, where the user can accept his
11091109
invitation. The signed invitation id is appended to the URL (base_url +
11101110
'/TOKEN'). Only the PUBLIC_WEB_FRONTEND_HOSTNAME is allowed as domain name.
1111-
:type base_url: str
1112-
:param message: A custom message that will be included in the invitation email.
1113-
:type message: Optional[str]
11141111
:raises ValueError: If the provided permissions are not allowed.
11151112
:raises UserInvalidWorkspacePermissionsError: If the user does not belong to the
11161113
workspace or doesn't have right permissions in the workspace.
1117-
:raises MaxNumberOfPendingWorkspaceInvitesReached: When the maximum number of
1118-
pending invites have been reached.
11191114
:return: The created workspace invitation.
1120-
:rtype: WorkspaceInvitation
11211115
"""
11221116

11231117
CoreHandler().check_permissions(
@@ -1136,23 +1130,10 @@ def create_workspace_invitation(
11361130
f"The user {email} is already part of the workspace."
11371131
)
11381132

1139-
max_invites = settings.BASEROW_MAX_PENDING_WORKSPACE_INVITES
1140-
if max_invites > 0 and (
1141-
WorkspaceInvitation.objects.filter(workspace=workspace)
1142-
.exclude(email=email)
1143-
.count()
1144-
>= max_invites
1145-
):
1146-
raise MaxNumberOfPendingWorkspaceInvitesReached(
1147-
f"The maximum number of pending workspaces invites {max_invites} has "
1148-
f"been reached."
1149-
)
1150-
11511133
invitation, created = WorkspaceInvitation.objects.update_or_create(
11521134
workspace=workspace,
11531135
email=email,
11541136
defaults={
1155-
"message": message,
11561137
"permissions": permissions,
11571138
"invited_by": user,
11581139
},
@@ -1176,23 +1157,21 @@ def create_workspace_invitation(
11761157

11771158
return invitation
11781159

1179-
def update_workspace_invitation(self, user, invitation, permissions):
1160+
def update_workspace_invitation(
1161+
self, user: AbstractUser, invitation: WorkspaceInvitation, permissions: str
1162+
) -> WorkspaceInvitation:
11801163
"""
11811164
Updates the permissions of an existing invitation if the user has ADMIN
11821165
permissions to the related workspace.
11831166
11841167
:param user: The user on whose behalf the invitation is updated.
1185-
:type user: User
11861168
:param invitation: The invitation that must be updated.
1187-
:type invitation: WorkspaceInvitation
11881169
:param permissions: The new permissions of the invitation that the user must
11891170
has after accepting.
1190-
:type permissions: str
11911171
:raises ValueError: If the provided permissions is not allowed.
11921172
:raises UserInvalidWorkspacePermissionsError: If the user does not belong to the
11931173
workspace or doesn't have right permissions in the workspace.
11941174
:return: The updated workspace permissions instance.
1195-
:rtype: WorkspaceInvitation
11961175
"""
11971176

11981177
CoreHandler().check_permissions(
@@ -1207,15 +1186,15 @@ def update_workspace_invitation(self, user, invitation, permissions):
12071186

12081187
return invitation
12091188

1210-
def delete_workspace_invitation(self, user, invitation):
1189+
def delete_workspace_invitation(
1190+
self, user: AbstractUser, invitation: WorkspaceInvitation
1191+
) -> None:
12111192
"""
12121193
Deletes an existing workspace invitation if the user has ADMIN permissions to
12131194
the related workspace.
12141195
12151196
:param user: The user on whose behalf the invitation is deleted.
1216-
:type user: User
12171197
:param invitation: The invitation that must be deleted.
1218-
:type invitation: WorkspaceInvitation
12191198
:raises UserInvalidWorkspacePermissionsError: If the user does not belong to the
12201199
workspace or doesn't have right permissions in the workspace.
12211200
"""
@@ -1415,10 +1394,8 @@ def filter_specific_applications(
14151394
] = None,
14161395
) -> QuerySet[Application]:
14171396
if per_content_type_queryset_hook is None:
1418-
per_content_type_queryset_hook = (
1419-
lambda model, qs: application_type_registry.get_by_model(
1420-
model
1421-
).enhance_queryset(qs)
1397+
per_content_type_queryset_hook = lambda model, qs: (
1398+
application_type_registry.get_by_model(model).enhance_queryset(qs)
14221399
)
14231400
return specific_queryset(queryset, per_content_type_queryset_hook)
14241401

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.13 on 2026-04-20 16:50
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('core', '0113_alter_notification_options_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='workspaceinvitation',
15+
name='message',
16+
field=models.TextField(default='', help_text='Deprecated legacy field retained for compatibility. This message is not exposed to invitation recipients.', max_length=250),
17+
),
18+
]

backend/src/baserow/core/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,11 +385,11 @@ class WorkspaceInvitation(
385385
help_text="The permissions that the user is going to get within the workspace "
386386
"after accepting the invitation.",
387387
)
388+
# TODO ZDM: Remove this field in a future migration (no longer used)
388389
message = models.TextField(
389-
blank=True,
390+
default="",
390391
max_length=250,
391-
help_text="An optional message that the invitor can provide. This will be "
392-
"visible to the receiver of the invitation.",
392+
help_text="Deprecated legacy field retained for compatibility. This message is not exposed to invitation recipients.",
393393
)
394394

395395
def get_parent(self):

backend/src/baserow/core/templates/baserow/core/workspace_invitation.html

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,6 @@
178178
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">{% blocktrans trimmed with invitation.invited_by.first_name as first_name and invitation.workspace.name as workspace_name %} <strong>{{ first_name }}</strong> has invited you to collaborate on <strong>{{ workspace_name }}</strong>. {% endblocktrans %}</div>
179179
</td>
180180
</tr>
181-
<!-- htmlmin:ignore -->{% if invitation.message %}<!-- htmlmin:ignore -->
182-
<tr>
183-
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
184-
<div style="font-family:Inter,sans-serif;font-size:13px;line-height:170%;text-align:left;color:#070810;">"{{ invitation.message }}"</div>
185-
</td>
186-
</tr>
187-
<!-- htmlmin:ignore -->{% endif %}<!-- htmlmin:ignore -->
188181
<tr>
189182
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
190183
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">

0 commit comments

Comments
 (0)