mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 22:35:36 +00:00
Optionally limit number of workspaces invites
This commit is contained in:
parent
2ac9de368f
commit
90e523efae
11 changed files with 143 additions and 1 deletions
backend
src/baserow
tests/baserow
changelog/entries/unreleased/feature
web-frontend
|
@ -10,3 +10,9 @@ ERROR_GROUP_INVITATION_EMAIL_MISMATCH = (
|
|||
HTTP_400_BAD_REQUEST,
|
||||
"Your email address does not match with the invitation.",
|
||||
)
|
||||
ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED = (
|
||||
"ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"The maximum number of pending invites for this workspace has been reached. "
|
||||
"Please wait for some invitees to accept the invite or cancel the existing ones.",
|
||||
)
|
||||
|
|
|
@ -26,6 +26,7 @@ from baserow.api.schemas import get_error_schema
|
|||
from baserow.api.workspaces.invitations.errors import (
|
||||
ERROR_GROUP_INVITATION_DOES_NOT_EXIST,
|
||||
ERROR_GROUP_INVITATION_EMAIL_MISMATCH,
|
||||
ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED,
|
||||
)
|
||||
from baserow.api.workspaces.serializers import WorkspaceUserWorkspaceSerializer
|
||||
from baserow.api.workspaces.users.errors import ERROR_GROUP_USER_ALREADY_EXISTS
|
||||
|
@ -39,6 +40,7 @@ from baserow.core.actions import (
|
|||
)
|
||||
from baserow.core.exceptions import (
|
||||
BaseURLHostnameNotAllowed,
|
||||
MaxNumberOfPendingWorkspaceInvitesReached,
|
||||
UserInvalidWorkspacePermissionsError,
|
||||
UserNotInWorkspace,
|
||||
WorkspaceDoesNotExist,
|
||||
|
@ -155,6 +157,7 @@ class WorkspaceInvitationsView(APIView, SortableViewMixin, SearchableViewMixin):
|
|||
"ERROR_USER_NOT_IN_GROUP",
|
||||
"ERROR_USER_INVALID_GROUP_PERMISSIONS",
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
"ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED",
|
||||
]
|
||||
),
|
||||
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
|
||||
|
@ -169,6 +172,7 @@ class WorkspaceInvitationsView(APIView, SortableViewMixin, SearchableViewMixin):
|
|||
UserInvalidWorkspacePermissionsError: ERROR_USER_INVALID_GROUP_PERMISSIONS,
|
||||
WorkspaceUserAlreadyExists: ERROR_GROUP_USER_ALREADY_EXISTS,
|
||||
BaseURLHostnameNotAllowed: ERROR_HOSTNAME_IS_NOT_ALLOWED,
|
||||
MaxNumberOfPendingWorkspaceInvitesReached: ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED,
|
||||
}
|
||||
)
|
||||
def post(self, request, data, workspace_id):
|
||||
|
|
|
@ -1058,6 +1058,11 @@ BASEROW_USER_LOG_ENTRY_CLEANUP_INTERVAL_MINUTES = int(
|
|||
BASEROW_USER_LOG_ENTRY_RETENTION_DAYS = int(
|
||||
os.getenv("BASEROW_USER_LOG_ENTRY_RETENTION_DAYS", 61)
|
||||
)
|
||||
# The maximum number of pending invites that a workspace can have. If `0` then
|
||||
# unlimited invites are allowed, which is the default value.
|
||||
BASEROW_MAX_PENDING_WORKSPACE_INVITES = int(
|
||||
os.getenv("BASEROW_MAX_PENDING_WORKSPACE_INVITES", 0)
|
||||
)
|
||||
|
||||
|
||||
PERMISSION_MANAGERS = [
|
||||
|
|
|
@ -76,6 +76,13 @@ class WorkspaceUserAlreadyExists(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class MaxNumberOfPendingWorkspaceInvitesReached(Exception):
|
||||
"""
|
||||
Raised when the maximum number of pending workspace invites has been reached.
|
||||
This value is configurable via the `BASEROW_MAX_PENDING_WORKSPACE_INVITES` setting.
|
||||
"""
|
||||
|
||||
|
||||
class WorkspaceUserIsLastAdmin(Exception):
|
||||
"""
|
||||
Raised when the last admin of the workspace tries to leave it. This will leave the
|
||||
|
|
|
@ -33,6 +33,7 @@ from .exceptions import (
|
|||
DuplicateApplicationMaxLocksExceededException,
|
||||
InvalidPermissionContext,
|
||||
LastAdminOfWorkspace,
|
||||
MaxNumberOfPendingWorkspaceInvitesReached,
|
||||
PermissionDenied,
|
||||
PermissionException,
|
||||
TemplateDoesNotExist,
|
||||
|
@ -1039,6 +1040,8 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
:raises ValueError: If the provided permissions are not allowed.
|
||||
:raises UserInvalidWorkspacePermissionsError: If the user does not belong to the
|
||||
workspace or doesn't have right permissions in the workspace.
|
||||
:raises MaxNumberOfPendingWorkspaceInvitesReached: When the maximum number of
|
||||
pending invites have been reached.
|
||||
:return: The created workspace invitation.
|
||||
:rtype: WorkspaceInvitation
|
||||
"""
|
||||
|
@ -1059,6 +1062,18 @@ class CoreHandler(metaclass=baserow_trace_methods(tracer)):
|
|||
f"The user {email} is already part of the workspace."
|
||||
)
|
||||
|
||||
max_invites = settings.BASEROW_MAX_PENDING_WORKSPACE_INVITES
|
||||
if max_invites > 0 and (
|
||||
WorkspaceInvitation.objects.filter(workspace=workspace)
|
||||
.exclude(email=email)
|
||||
.count()
|
||||
>= max_invites
|
||||
):
|
||||
raise MaxNumberOfPendingWorkspaceInvitesReached(
|
||||
f"The maximum number of pending workspaces invites {max_invites} has "
|
||||
f"been reached."
|
||||
)
|
||||
|
||||
invitation, created = WorkspaceInvitation.objects.update_or_create(
|
||||
workspace=workspace,
|
||||
email=email,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.shortcuts import reverse
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
@ -255,6 +256,48 @@ def test_create_workspace_invitation(api_client, data_fixture):
|
|||
assert response.json()["error"] == "ERROR_REQUEST_BODY_VALIDATION"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(BASEROW_MAX_PENDING_WORKSPACE_INVITES=1)
|
||||
def test_create_workspace_invitation_max_pending(api_client, data_fixture):
|
||||
user_1, token_1 = data_fixture.create_user_and_token(email="test1@test.nl")
|
||||
workspace_1 = data_fixture.create_workspace(user=user_1)
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:workspaces:invitations:list", kwargs={"workspace_id": workspace_1.id}
|
||||
),
|
||||
{
|
||||
"email": "test@test.nl",
|
||||
"permissions": "ADMIN",
|
||||
"message": "Test",
|
||||
"base_url": "http://localhost:3000/invite",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token_1}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
|
||||
response = api_client.post(
|
||||
reverse(
|
||||
"api:workspaces:invitations:list", kwargs={"workspace_id": workspace_1.id}
|
||||
),
|
||||
{
|
||||
"email": "test2@test.nl",
|
||||
"permissions": "ADMIN",
|
||||
"message": "Test",
|
||||
"base_url": "http://localhost:3000/invite",
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token_1}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert (
|
||||
response_json["error"]
|
||||
== "ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_workspace_invitation(api_client, data_fixture):
|
||||
user_1, token_1 = data_fixture.create_user_and_token(email="test1@test.nl")
|
||||
|
|
|
@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
|
|||
from django.conf import settings
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.db import OperationalError, transaction
|
||||
from django.test.utils import override_settings
|
||||
|
||||
import pytest
|
||||
from itsdangerous.exc import BadSignature
|
||||
|
@ -28,6 +29,7 @@ from baserow.core.exceptions import (
|
|||
DuplicateApplicationMaxLocksExceededException,
|
||||
IsNotAdminError,
|
||||
LastAdminOfWorkspace,
|
||||
MaxNumberOfPendingWorkspaceInvitesReached,
|
||||
TemplateDoesNotExist,
|
||||
TemplateFileDoesNotExist,
|
||||
UserInvalidWorkspacePermissionsError,
|
||||
|
@ -702,6 +704,47 @@ def test_create_workspace_invitation(mock_send_email, data_fixture):
|
|||
assert WorkspaceInvitation.objects.all().count() == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("baserow.core.handler.CoreHandler.send_workspace_invitation_email")
|
||||
@override_settings(BASEROW_MAX_PENDING_WORKSPACE_INVITES=1)
|
||||
def test_create_workspace_invitation_max_pending(mock_send_email, data_fixture):
|
||||
user_workspace = data_fixture.create_user_workspace()
|
||||
user = user_workspace.user
|
||||
workspace = user_workspace.workspace
|
||||
|
||||
handler = CoreHandler()
|
||||
|
||||
handler.create_workspace_invitation(
|
||||
user=user,
|
||||
workspace=workspace,
|
||||
email="test@test.nl",
|
||||
permissions="ADMIN",
|
||||
message="Test",
|
||||
base_url="http://localhost:3000/invite",
|
||||
)
|
||||
|
||||
with pytest.raises(MaxNumberOfPendingWorkspaceInvitesReached):
|
||||
handler.create_workspace_invitation(
|
||||
user=user,
|
||||
workspace=workspace,
|
||||
email="test2@test.nl",
|
||||
permissions="ADMIN",
|
||||
message="Test",
|
||||
base_url="http://localhost:3000/invite",
|
||||
)
|
||||
|
||||
# This email address already exists, so it should just update the invite without
|
||||
# failing.
|
||||
handler.create_workspace_invitation(
|
||||
user=user,
|
||||
workspace=workspace,
|
||||
email="test@test.nl",
|
||||
permissions="MEMBER",
|
||||
message="Test",
|
||||
base_url="http://localhost:3000/invite",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_workspace_invitation(data_fixture):
|
||||
workspace_invitation = data_fixture.create_workspace_invitation()
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Optionally limit the number of pending invites using the `BASEROW_MAX_PENDING_WORKSPACE_INVITES` environment variable.",
|
||||
"issue_number": null,
|
||||
"bullet_points": [],
|
||||
"created_at": "2024-05-27"
|
||||
}
|
|
@ -330,7 +330,9 @@
|
|||
"outputParserTitle": "Wrong output",
|
||||
"outputParserDescription": "The model responded with an incorrect output. Please try again.",
|
||||
"generateAIPromptTitle": "Prompt error",
|
||||
"generateAIPromptDescription": "Something was wrong with the constructed prompt."
|
||||
"generateAIPromptDescription": "Something was wrong with the constructed prompt.",
|
||||
"maxNumberOfPendingWorkspaceInvitesReachedTitle": "Max pending invites reached",
|
||||
"maxNumberOfPendingWorkspaceInvitesReachedDescription": "You've reached the maximum number of pending invites for this workspace. Please let invitees accept the invite or cancel existing ones to continue."
|
||||
},
|
||||
"importerType": {
|
||||
"csv": "Import a CSV file",
|
||||
|
|
|
@ -55,6 +55,7 @@ export default {
|
|||
methods: {
|
||||
async inviteSubmitted(values) {
|
||||
this.inviteLoading = true
|
||||
this.hideError()
|
||||
|
||||
try {
|
||||
// The public accept url is the page where the user can publicly navigate too,
|
||||
|
|
|
@ -163,6 +163,15 @@ export class ClientErrorMap {
|
|||
app.i18n.t('clientHandler.modelDoesNotBelongToTypeTitle'),
|
||||
app.i18n.t('clientHandler.modelDoesNotBelongToTypeDescription')
|
||||
),
|
||||
ERROR_MAX_NUMBER_OF_PENDING_WORKSPACE_INVITES_REACHED:
|
||||
new ResponseErrorMessage(
|
||||
app.i18n.t(
|
||||
'clientHandler.maxNumberOfPendingWorkspaceInvitesReachedTitle'
|
||||
),
|
||||
app.i18n.t(
|
||||
'clientHandler.maxNumberOfPendingWorkspaceInvitesReachedDescription'
|
||||
)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue