Add org-specific delete invite endpoint (#575)

Adds POST /orgs/{oid}/invites/delete, which expects the invited
email address in the POST body.

This endpoint will also delete duplicate invites with the same
email/oid combination if env var ALLOW_DUPE_INVITES allows dupes.
This commit is contained in:
Tessa Walsh 2023-02-08 16:10:09 -05:00 committed by GitHub
parent 3261e7d666
commit a7a18b9db0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 77 additions and 5 deletions

View File

@ -118,6 +118,15 @@ class InviteOps:
"""remove invite from invite list"""
await self.invites.delete_one({"_id": invite_token})
async def remove_invite_by_email(self, email: str, oid: str = None):
"""remove invite from invite list by email"""
query = {"email": email}
if oid:
query["oid"] = oid
# Use delete_many rather than delete_one to clean up any duplicate
# invites as well.
return await self.invites.delete_many(query)
def accept_user_invite(self, user, invite_token: str):
"""remove invite from user, if valid token, throw if not"""
invite = user.invites.pop(invite_token, "")

View File

@ -3,6 +3,7 @@ Organization API handling
"""
import os
import time
import urllib.parse
import uuid
from typing import Dict, Union, Literal, Optional
@ -39,6 +40,11 @@ class RemoveFromOrg(InviteRequest):
"""Remove this user from org"""
# ============================================================================
class RemovePendingInvite(InviteRequest):
"""Delete pending invite to org by email"""
# ============================================================================
class RenameOrg(BaseModel):
"""Request to invite another user"""
@ -468,6 +474,20 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep: User):
pending_invites = await user_manager.invites.get_pending_invites(org)
return {"pending_invites": pending_invites}
@router.post("/invites/delete", tags=["invites"])
async def delete_invite(
invite: RemovePendingInvite, org: Organization = Depends(org_owner_dep)
):
# URL decode email just in case
email = urllib.parse.unquote(invite.email)
result = await user_manager.invites.remove_invite_by_email(email, org.id)
if result.deleted_count > 0:
return {
"removed": True,
"count": result.deleted_count,
}
raise HTTPException(status_code=404, detail="invite_not_found")
@router.post("/remove", tags=["invites"])
async def remove_user_from_org(
remove: RemoveFromOrg, org: Organization = Depends(org_owner_dep)

View File

@ -483,11 +483,6 @@ def init_users_api(app, user_manager):
return await user_manager.format_invite(invite)
@users_router.get("/invite-delete/{token}", tags=["invites"])
async def delete_invite(token: str):
await user_manager.invites.remove_invite(token)
return {"removed": True}
@users_router.get("/invites", tags=["invites"])
async def get_pending_invites(user: User = Depends(current_active_user)):
if not user.is_superuser:

View File

@ -1,4 +1,5 @@
import requests
import uuid
import pytest
@ -231,3 +232,50 @@ def test_send_and_accept_org_invite(
user for user in users.values() if user["email"] == expected_stored_email
]
assert len(users_with_invited_email) == 1
def test_delete_invite_by_email(admin_auth_headers, non_default_org_id):
# Invite user to non-default org
INVITE_EMAIL = "new-non-default-org-invite-by-email@example.com"
r = requests.post(
f"{API_PREFIX}/orgs/{non_default_org_id}/invite",
headers=admin_auth_headers,
json={"email": INVITE_EMAIL, "role": 20},
)
assert r.status_code == 200
data = r.json()
assert data["invited"] == "new_user"
# Delete invite by email
r = requests.post(
f"{API_PREFIX}/orgs/{non_default_org_id}/invites/delete",
headers=admin_auth_headers,
json={"email": INVITE_EMAIL},
)
assert r.status_code == 200
data = r.json()
assert data["removed"]
assert data["count"] == 1
# Verify invite no longer exists
r = requests.get(
f"{API_PREFIX}/orgs/{non_default_org_id}/invites",
headers=admin_auth_headers,
)
assert r.status_code == 200
data = r.json()
invites_matching_email = [
invite for invite in data["pending_invites"] if invite["email"] == INVITE_EMAIL
]
assert len(invites_matching_email) == 0
# Try to delete non-existent email and test we get 404
bad_token = str(uuid.uuid4())
r = requests.post(
f"{API_PREFIX}/orgs/{non_default_org_id}/invites/delete",
headers=admin_auth_headers,
json={"email": "not-a-valid-invite@example.com"},
)
assert r.status_code == 404
data = r.json()
assert data["detail"] == "invite_not_found"