Make API updates for member updates (#541)
* Add API endpoint that lists pending invites for all orgs (superuser-only) * Add API endpoint that lists pending invites for org * Add user emails to /api/orgs/<oid> response
This commit is contained in:
		
							parent
							
								
									9048d46c6c
								
							
						
					
					
						commit
						58aafc4191
					
				@ -189,6 +189,14 @@ class InviteOps:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def get_pending_invites(self, org=None):
 | 
				
			||||||
 | 
					        """return list of pending invites."""
 | 
				
			||||||
 | 
					        if org:
 | 
				
			||||||
 | 
					            invites = self.invites.find({"oid": org.id})
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            invites = self.invites.find()
 | 
				
			||||||
 | 
					        return [invite async for invite in invites]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def init_invites(mdb, email):
 | 
					def init_invites(mdb, email):
 | 
				
			||||||
    """init InviteOps"""
 | 
					    """init InviteOps"""
 | 
				
			||||||
 | 
				
			|||||||
@ -136,6 +136,7 @@ class Organization(BaseMongoModel):
 | 
				
			|||||||
                result["users"][id_] = {
 | 
					                result["users"][id_] = {
 | 
				
			||||||
                    "role": role,
 | 
					                    "role": role,
 | 
				
			||||||
                    "name": org_user.get("name", ""),
 | 
					                    "name": org_user.get("name", ""),
 | 
				
			||||||
 | 
					                    "email": org_user.get("email", ""),
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return result
 | 
					        return result
 | 
				
			||||||
@ -463,6 +464,11 @@ def init_orgs_api(app, mdb, user_manager, invites, user_dep: User):
 | 
				
			|||||||
        await user_manager.user_db.update(user)
 | 
					        await user_manager.user_db.update(user)
 | 
				
			||||||
        return {"added": True}
 | 
					        return {"added": True}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @router.get("/invites", tags=["invites"])
 | 
				
			||||||
 | 
					    async def get_pending_org_invites(org: Organization = Depends(org_owner_dep)):
 | 
				
			||||||
 | 
					        pending_invites = await user_manager.invites.get_pending_invites(org)
 | 
				
			||||||
 | 
					        return {"pending_invites": pending_invites}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @router.post("/remove", tags=["invites"])
 | 
					    @router.post("/remove", tags=["invites"])
 | 
				
			||||||
    async def remove_user_from_org(
 | 
					    async def remove_user_from_org(
 | 
				
			||||||
        remove: RemoveFromOrg, org: Organization = Depends(org_owner_dep)
 | 
					        remove: RemoveFromOrg, org: Organization = Depends(org_owner_dep)
 | 
				
			||||||
 | 
				
			|||||||
@ -151,7 +151,7 @@ class UserManager(BaseUserManager[UserCreate, UserDB]):
 | 
				
			|||||||
        """return list of user names for given ids"""
 | 
					        """return list of user names for given ids"""
 | 
				
			||||||
        user_ids = [UUID4(id_) for id_ in user_ids]
 | 
					        user_ids = [UUID4(id_) for id_ in user_ids]
 | 
				
			||||||
        cursor = self.user_db.collection.find(
 | 
					        cursor = self.user_db.collection.find(
 | 
				
			||||||
            {"id": {"$in": user_ids}}, projection=["id", "name"]
 | 
					            {"id": {"$in": user_ids}}, projection=["id", "name", "email"]
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return await cursor.to_list(length=1000)
 | 
					        return await cursor.to_list(length=1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -363,6 +363,7 @@ class BearerOrQueryTransport(BearerTransport):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ============================================================================
 | 
					# ============================================================================
 | 
				
			||||||
 | 
					# pylint: disable=too-many-locals
 | 
				
			||||||
def init_users_api(app, user_manager):
 | 
					def init_users_api(app, user_manager):
 | 
				
			||||||
    """init fastapi_users"""
 | 
					    """init fastapi_users"""
 | 
				
			||||||
    bearer_transport = BearerOrQueryTransport(tokenUrl="auth/jwt/login")
 | 
					    bearer_transport = BearerOrQueryTransport(tokenUrl="auth/jwt/login")
 | 
				
			||||||
@ -488,6 +489,14 @@ def init_users_api(app, user_manager):
 | 
				
			|||||||
        await user_manager.invites.remove_invite(token)
 | 
					        await user_manager.invites.remove_invite(token)
 | 
				
			||||||
        return {"removed": True}
 | 
					        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:
 | 
				
			||||||
 | 
					            raise HTTPException(status_code=403, detail="Not Allowed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pending_invites = await user_manager.invites.get_pending_invites()
 | 
				
			||||||
 | 
					        return {"pending_invites": pending_invites}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    app.include_router(users_router, prefix="/users", tags=["users"])
 | 
					    app.include_router(users_router, prefix="/users", tags=["users"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return fastapi_users
 | 
					    return fastapi_users
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,8 @@ CRAWLER_PW = "crawlerPASSWORD!"
 | 
				
			|||||||
_admin_config_id = None
 | 
					_admin_config_id = None
 | 
				
			||||||
_crawler_config_id = None
 | 
					_crawler_config_id = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					NON_DEFAULT_ORG_NAME = "Non-default org"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(scope="session")
 | 
					@pytest.fixture(scope="session")
 | 
				
			||||||
def admin_auth_headers():
 | 
					def admin_auth_headers():
 | 
				
			||||||
@ -52,6 +54,27 @@ def default_org_id(admin_auth_headers):
 | 
				
			|||||||
            time.sleep(5)
 | 
					            time.sleep(5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture(scope="session")
 | 
				
			||||||
 | 
					def non_default_org_id(admin_auth_headers):
 | 
				
			||||||
 | 
					    r = requests.post(
 | 
				
			||||||
 | 
					        f"{API_PREFIX}/orgs/create",
 | 
				
			||||||
 | 
					        headers=admin_auth_headers,
 | 
				
			||||||
 | 
					        json={"name": NON_DEFAULT_ORG_NAME},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        r = requests.get(f"{API_PREFIX}/orgs", headers=admin_auth_headers)
 | 
				
			||||||
 | 
					        data = r.json()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            for org in data["orgs"]:
 | 
				
			||||||
 | 
					                if org["name"] == NON_DEFAULT_ORG_NAME:
 | 
				
			||||||
 | 
					                    return org["id"]
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            print("Waiting for non-default org id")
 | 
				
			||||||
 | 
					            time.sleep(5)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(scope="session")
 | 
					@pytest.fixture(scope="session")
 | 
				
			||||||
def admin_crawl_id(admin_auth_headers, default_org_id):
 | 
					def admin_crawl_id(admin_auth_headers, default_org_id):
 | 
				
			||||||
    # Start crawl.
 | 
					    # Start crawl.
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										40
									
								
								backend/test/test_invites.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								backend/test/test_invites.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					import requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .conftest import API_PREFIX
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pending_invites(admin_auth_headers, default_org_id):
 | 
				
			||||||
 | 
					    r = requests.get(f"{API_PREFIX}/users/invites", headers=admin_auth_headers)
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    data = r.json()
 | 
				
			||||||
 | 
					    assert data["pending_invites"] == []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add a pending invite and check it's returned
 | 
				
			||||||
 | 
					    INVITE_EMAIL = "invite-pending@example.com"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    r = requests.post(
 | 
				
			||||||
 | 
					        f"{API_PREFIX}/users/invite",
 | 
				
			||||||
 | 
					        headers=admin_auth_headers,
 | 
				
			||||||
 | 
					        json={"email": INVITE_EMAIL},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    data = r.json()
 | 
				
			||||||
 | 
					    assert data["invited"] == "new_user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    r = requests.get(f"{API_PREFIX}/users/invites", headers=admin_auth_headers)
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    data = r.json()
 | 
				
			||||||
 | 
					    invites = data["pending_invites"]
 | 
				
			||||||
 | 
					    assert len(invites) == 1
 | 
				
			||||||
 | 
					    invite = invites[0]
 | 
				
			||||||
 | 
					    assert invite["_id"]
 | 
				
			||||||
 | 
					    assert invite["email"] == INVITE_EMAIL
 | 
				
			||||||
 | 
					    assert invite["oid"] == default_org_id
 | 
				
			||||||
 | 
					    assert invite["created"]
 | 
				
			||||||
 | 
					    assert invite["role"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_pending_invites_crawler(crawler_auth_headers, default_org_id):
 | 
				
			||||||
 | 
					    # Verify that only superusers can see pending invites
 | 
				
			||||||
 | 
					    r = requests.get(f"{API_PREFIX}/users/invites", headers=crawler_auth_headers)
 | 
				
			||||||
 | 
					    assert r.status_code == 403
 | 
				
			||||||
@ -16,6 +16,34 @@ def test_ensure_only_one_default_org(admin_auth_headers):
 | 
				
			|||||||
    assert len(orgs_with_same_name) == 1
 | 
					    assert len(orgs_with_same_name) == 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_get_org_admin(admin_auth_headers, default_org_id):
 | 
				
			||||||
 | 
					    """org owners should receive details on users."""
 | 
				
			||||||
 | 
					    r = requests.get(f"{API_PREFIX}/orgs/{default_org_id}", headers=admin_auth_headers)
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    data = r.json()
 | 
				
			||||||
 | 
					    assert data["id"] == default_org_id
 | 
				
			||||||
 | 
					    assert data["name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    users = data["users"]
 | 
				
			||||||
 | 
					    assert users
 | 
				
			||||||
 | 
					    for _, value in users.items():
 | 
				
			||||||
 | 
					        assert value["name"]
 | 
				
			||||||
 | 
					        assert value["email"]
 | 
				
			||||||
 | 
					        assert value["role"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_get_org_crawler(crawler_auth_headers, default_org_id):
 | 
				
			||||||
 | 
					    """non-owners should *not* receive details on users."""
 | 
				
			||||||
 | 
					    r = requests.get(
 | 
				
			||||||
 | 
					        f"{API_PREFIX}/orgs/{default_org_id}", headers=crawler_auth_headers
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    data = r.json()
 | 
				
			||||||
 | 
					    assert data["id"] == default_org_id
 | 
				
			||||||
 | 
					    assert data["name"]
 | 
				
			||||||
 | 
					    assert data.get("users") is None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_rename_org(admin_auth_headers, default_org_id):
 | 
					def test_rename_org(admin_auth_headers, default_org_id):
 | 
				
			||||||
    UPDATED_NAME = "updated org name"
 | 
					    UPDATED_NAME = "updated org name"
 | 
				
			||||||
    rename_data = {"name": UPDATED_NAME}
 | 
					    rename_data = {"name": UPDATED_NAME}
 | 
				
			||||||
@ -83,3 +111,44 @@ def test_remove_user_from_org(admin_auth_headers, default_org_id):
 | 
				
			|||||||
    assert r.status_code == 200
 | 
					    assert r.status_code == 200
 | 
				
			||||||
    data = r.json()
 | 
					    data = r.json()
 | 
				
			||||||
    assert data["removed"]
 | 
					    assert data["removed"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_get_pending_org_invites(
 | 
				
			||||||
 | 
					    admin_auth_headers, default_org_id, non_default_org_id
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    # Invite user to non-default org
 | 
				
			||||||
 | 
					    INVITE_EMAIL = "non-default-invite@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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Invite user to default org
 | 
				
			||||||
 | 
					    r = requests.post(
 | 
				
			||||||
 | 
					        f"{API_PREFIX}/orgs/{default_org_id}/invite",
 | 
				
			||||||
 | 
					        headers=admin_auth_headers,
 | 
				
			||||||
 | 
					        json={"email": "default-invite@example.com", "role": 10},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    assert r.status_code == 200
 | 
				
			||||||
 | 
					    data = r.json()
 | 
				
			||||||
 | 
					    assert data["invited"] == "new_user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check that only invite to non-default org is returned
 | 
				
			||||||
 | 
					    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 = data["pending_invites"]
 | 
				
			||||||
 | 
					    assert len(invites) == 1
 | 
				
			||||||
 | 
					    invite = invites[0]
 | 
				
			||||||
 | 
					    assert invite["_id"]
 | 
				
			||||||
 | 
					    assert invite["email"] == INVITE_EMAIL
 | 
				
			||||||
 | 
					    assert invite["oid"] == non_default_org_id
 | 
				
			||||||
 | 
					    assert invite["created"]
 | 
				
			||||||
 | 
					    assert invite["role"]
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user