test_oidc_unlink_last_link.py 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. """Removing a linked SSO account never removes a user's last way to sign in.
  2. A user Bambuddy created from an SSO login has no password, so their last
  3. link stays; a user with a password, or with another link, can remove it.
  4. """
  5. import pytest
  6. from httpx import AsyncClient
  7. from sqlalchemy import select
  8. from backend.app.core.auth import create_access_token, get_password_hash
  9. from backend.app.models.oidc_provider import OIDCProvider, UserOIDCLink
  10. from backend.app.models.user import User
  11. from backend.tests.integration.test_mfa_api import _setup_and_login
  12. async def _provider(db_session, name: str) -> int:
  13. provider = OIDCProvider(name=name, issuer_url=f"https://{name}.example.com", client_id="bambuddy")
  14. provider.client_secret = "provider-secret"
  15. db_session.add(provider)
  16. await db_session.commit()
  17. return provider.id
  18. async def _user(db_session, *, oidc_only: bool, providers: list[int]) -> tuple[User, dict]:
  19. user = User(
  20. username="sso-user",
  21. password_hash=None if oidc_only else get_password_hash("Whatever1!"),
  22. auth_source="oidc" if oidc_only else "local",
  23. role="user",
  24. is_active=True,
  25. )
  26. db_session.add(user)
  27. await db_session.commit()
  28. for i, provider_id in enumerate(providers):
  29. db_session.add(UserOIDCLink(user_id=user.id, provider_id=provider_id, provider_user_id=f"sub-{i}"))
  30. await db_session.commit()
  31. return user, {"Authorization": f"Bearer {create_access_token(data={'sub': user.username})}"}
  32. async def _links(db_session, user_id: int) -> int:
  33. db_session.expire_all()
  34. return len((await db_session.execute(select(UserOIDCLink).where(UserOIDCLink.user_id == user_id))).all())
  35. @pytest.mark.asyncio
  36. async def test_the_last_link_of_a_user_without_password_stays(async_client: AsyncClient, db_session):
  37. await _setup_and_login(async_client, "ssoadmin", "SsoAdmin12!")
  38. provider = await _provider(db_session, "alpha")
  39. user, headers = await _user(db_session, oidc_only=True, providers=[provider])
  40. response = await async_client.delete(f"/api/v1/auth/oidc/links/{provider}", headers=headers)
  41. assert response.status_code == 400
  42. assert await _links(db_session, user.id) == 1
  43. @pytest.mark.asyncio
  44. async def test_a_user_without_password_can_remove_one_of_two_links(async_client: AsyncClient, db_session):
  45. await _setup_and_login(async_client, "ssoadmin", "SsoAdmin12!")
  46. first, second = await _provider(db_session, "alpha"), await _provider(db_session, "beta")
  47. user, headers = await _user(db_session, oidc_only=True, providers=[first, second])
  48. response = await async_client.delete(f"/api/v1/auth/oidc/links/{first}", headers=headers)
  49. assert response.status_code == 200, response.text
  50. assert await _links(db_session, user.id) == 1
  51. @pytest.mark.asyncio
  52. async def test_a_user_with_a_password_can_remove_the_last_link(async_client: AsyncClient, db_session):
  53. await _setup_and_login(async_client, "ssoadmin", "SsoAdmin12!")
  54. provider = await _provider(db_session, "alpha")
  55. user, headers = await _user(db_session, oidc_only=False, providers=[provider])
  56. response = await async_client.delete(f"/api/v1/auth/oidc/links/{provider}", headers=headers)
  57. assert response.status_code == 200, response.text
  58. assert await _links(db_session, user.id) == 0