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:
		
							parent
							
								
									3261e7d666
								
							
						
					
					
						commit
						a7a18b9db0
					
				@ -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, "")
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user