개선관리 요구사항과 Google 로그인을 완료
This commit is contained in:
parent
cc0a15b7c6
commit
2a39636163
112 changed files with 10166 additions and 527 deletions
|
|
@ -183,6 +183,7 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
config = await auth_routes.auth_config(request)
|
||||
|
||||
self.assertTrue(config.dev_login_enabled)
|
||||
self.assertEqual(config.allowed_email_domains, [])
|
||||
|
||||
async def test_learner_can_accept_and_withdraw_practice_consent(self) -> None:
|
||||
with patch.object(auth_sessions, "get_pool", side_effect=RuntimeError("no db")):
|
||||
|
|
@ -328,7 +329,9 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
|
||||
self.assertEqual(me.cohort_ids, ["e2e-hanshin"])
|
||||
|
||||
async def test_admin_created_external_domain_user_can_login(self) -> None:
|
||||
async def test_google_identity_accepts_any_verified_domain_and_auto_approves(
|
||||
self,
|
||||
) -> None:
|
||||
with (
|
||||
patched_settings(
|
||||
environment="dev",
|
||||
|
|
@ -354,29 +357,57 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
admin_routes.AdminUserCreate(
|
||||
email="forced.gmail@gmail.com",
|
||||
display_name="Forced Gmail",
|
||||
role="teacher",
|
||||
account_status="approved",
|
||||
role="learner",
|
||||
cohort_ids=["manual-cohort"],
|
||||
),
|
||||
super_admin,
|
||||
)
|
||||
|
||||
me = await auth_routes.dev_login(
|
||||
_request(path="/auth/dev-login"),
|
||||
auth_routes.DevLoginRequest(
|
||||
email="forced.gmail@gmail.com",
|
||||
role="learner",
|
||||
display_name="Provider Name",
|
||||
self.assertEqual(created.account_status, "pending")
|
||||
self.assertEqual(
|
||||
auth_routes.validate_google_identity(
|
||||
email="unmanaged@gmail.com",
|
||||
email_verified=True,
|
||||
),
|
||||
Response(),
|
||||
"unmanaged@gmail.com",
|
||||
)
|
||||
google_email = auth_routes.validate_google_identity(
|
||||
email="forced.gmail@gmail.com",
|
||||
email_verified=True,
|
||||
)
|
||||
google_managed = await auth_sessions.get_managed_user_by_email(google_email)
|
||||
self.assertEqual(google_email, created.email)
|
||||
self.assertIsNotNone(google_managed)
|
||||
self.assertEqual(google_managed.user_id, created.user_id)
|
||||
|
||||
self.assertEqual(me.user_id, created.user_id)
|
||||
self.assertEqual(me.email, "forced.gmail@gmail.com")
|
||||
self.assertEqual(me.role, "teacher")
|
||||
self.assertEqual(me.account_status, "approved")
|
||||
self.assertEqual(me.cohort_ids, ["manual-cohort"])
|
||||
_, google_user = await auth_sessions.create_session(
|
||||
email="forced.gmail@gmail.com",
|
||||
display_name="Provider Name",
|
||||
role="learner",
|
||||
external_id="google:external-participant",
|
||||
account_status="approved",
|
||||
)
|
||||
self.assertEqual(google_user.user_id, created.user_id)
|
||||
self.assertEqual(google_user.account_status, "approved")
|
||||
self.assertEqual(google_user.cohort_ids, ["manual-cohort"])
|
||||
|
||||
suspended = await admin_routes.patch_user(
|
||||
created.user_id,
|
||||
admin_routes.AdminUserPatch(account_status="suspended"),
|
||||
super_admin,
|
||||
)
|
||||
self.assertEqual(suspended.account_status, "suspended")
|
||||
_, suspended_login = await auth_sessions.create_session(
|
||||
email="forced.gmail@gmail.com",
|
||||
display_name="Provider Name",
|
||||
role="learner",
|
||||
external_id="google:external-participant",
|
||||
account_status="approved",
|
||||
)
|
||||
self.assertEqual(suspended_login.account_status, "suspended")
|
||||
|
||||
# Google is open to every verified account, while the local dev-login
|
||||
# fixture remains constrained to its explicit test domains.
|
||||
with self.assertRaises(auth_routes.HTTPException) as denied:
|
||||
await auth_routes.dev_login(
|
||||
_request(path="/auth/dev-login"),
|
||||
|
|
@ -388,6 +419,14 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
Response(),
|
||||
)
|
||||
self.assertEqual(denied.exception.status_code, 403)
|
||||
self.assertEqual(denied.exception.detail, "email domain is not allowed")
|
||||
|
||||
with self.assertRaises(auth_routes.HTTPException) as unverified:
|
||||
auth_routes.validate_google_identity(
|
||||
email="unmanaged@gmail.com",
|
||||
email_verified=False,
|
||||
)
|
||||
self.assertEqual(unverified.exception.status_code, 403)
|
||||
|
||||
async def test_super_admin_email_is_admin_and_auto_approved(self) -> None:
|
||||
with (
|
||||
|
|
@ -664,6 +703,62 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertIn("$7::boolean", conn.queries[0])
|
||||
self.assertIn("$9::boolean", conn.queries[1])
|
||||
|
||||
async def test_google_oauth_promotes_preregistered_pending_user_in_db(self) -> None:
|
||||
class RecordingConn:
|
||||
def __init__(self) -> None:
|
||||
self.query = ""
|
||||
self.args: tuple[Any, ...] = ()
|
||||
|
||||
async def fetchrow(self, query: str, *args: Any) -> dict[str, Any]:
|
||||
self.query = query
|
||||
self.args = args
|
||||
now = datetime.now(timezone.utc)
|
||||
return {
|
||||
"user_id": "00000000-0000-0000-0000-000000000606",
|
||||
"email": "preregistered@gmail.com",
|
||||
"display_name": "Preregistered",
|
||||
"role": "learner",
|
||||
"admin_access": False,
|
||||
"account_status": "approved",
|
||||
"cohort": "pilot",
|
||||
"affiliation": "",
|
||||
"created_at": now,
|
||||
"last_seen_at": now,
|
||||
}
|
||||
|
||||
class RecordingAcquire:
|
||||
def __init__(self, conn: RecordingConn) -> None:
|
||||
self.conn = conn
|
||||
|
||||
async def __aenter__(self) -> RecordingConn:
|
||||
return self.conn
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||
return None
|
||||
|
||||
class RecordingPool:
|
||||
def __init__(self, conn: RecordingConn) -> None:
|
||||
self.conn = conn
|
||||
|
||||
def acquire(self) -> RecordingAcquire:
|
||||
return RecordingAcquire(self.conn)
|
||||
|
||||
conn = RecordingConn()
|
||||
with patch.object(auth_sessions, "get_pool", return_value=RecordingPool(conn)):
|
||||
user = await auth_sessions.upsert_managed_user(
|
||||
auth_sessions.ManagedUserUpsertInput(
|
||||
email="preregistered@gmail.com",
|
||||
display_name="Preregistered",
|
||||
role="learner",
|
||||
external_id="google:preregistered-sub",
|
||||
account_status="approved",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(user.account_status, "approved")
|
||||
self.assertIn("WHEN $8::text = 'approved' THEN 'approved'", conn.query)
|
||||
self.assertEqual(conn.args[7], "approved")
|
||||
|
||||
async def test_auth_config_allows_dev_login_from_configured_tailnet_forwarded_host(self) -> None:
|
||||
request = _request(
|
||||
[
|
||||
|
|
@ -1040,6 +1135,82 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
self.assertIn("oauth=local_oauth_unavailable", location)
|
||||
self.assertFalse(auth_routes._oauth_states)
|
||||
|
||||
async def test_google_callback_accepts_unregistered_external_email(self) -> None:
|
||||
state = "external-email-state"
|
||||
auth_routes._oauth_states[state] = auth_routes.OAuthState(
|
||||
code_verifier="verifier",
|
||||
next_path="/learn",
|
||||
created_at=1_800_000_000.0,
|
||||
)
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload: dict[str, Any]) -> None:
|
||||
self.status_code = 200
|
||||
self._payload = payload
|
||||
|
||||
def json(self) -> dict[str, Any]:
|
||||
return self._payload
|
||||
|
||||
class FakeAsyncClient:
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
async def __aenter__(self) -> "FakeAsyncClient":
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: object, exc: object, tb: object) -> None:
|
||||
return None
|
||||
|
||||
async def post(self, url: str, **kwargs: Any) -> FakeResponse:
|
||||
return FakeResponse({"id_token": "id-token"})
|
||||
|
||||
async def get(self, url: str, **kwargs: Any) -> FakeResponse:
|
||||
return FakeResponse(
|
||||
{
|
||||
"aud": "google-client",
|
||||
"iss": "https://accounts.google.com",
|
||||
"email": "unregistered@gmail.com",
|
||||
"email_verified": "true",
|
||||
"name": "Unregistered External",
|
||||
"sub": "google-external-sub",
|
||||
}
|
||||
)
|
||||
|
||||
create_session_mock = AsyncMock(return_value=("opaque-session", object()))
|
||||
with (
|
||||
patched_settings(
|
||||
oauth_google_client_id="google-client",
|
||||
oauth_google_client_secret="google-secret",
|
||||
frontend_base_url="https://vignette.test",
|
||||
environment="prod",
|
||||
auth_allowed_email_domains=["hs.ac.kr"],
|
||||
),
|
||||
patch.object(auth_routes.httpx, "AsyncClient", FakeAsyncClient),
|
||||
patch.object(
|
||||
auth_routes,
|
||||
"get_managed_user_by_email",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch.object(auth_routes, "create_session", create_session_mock),
|
||||
):
|
||||
response = await auth_routes.callback(
|
||||
_request(),
|
||||
code="auth-code",
|
||||
state=state,
|
||||
oauth_state_cookie=state,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.headers["location"], "https://vignette.test/learn")
|
||||
create_session_mock.assert_awaited_once_with(
|
||||
email="unregistered@gmail.com",
|
||||
display_name="Unregistered External",
|
||||
role="learner",
|
||||
cohort_ids=[],
|
||||
external_id="google:google-external-sub",
|
||||
account_status="approved",
|
||||
)
|
||||
|
||||
async def test_google_callback_sets_opaque_cookie_without_browser_tokens(self) -> None:
|
||||
state = "state-token"
|
||||
auth_routes._oauth_states[state] = auth_routes.OAuthState(
|
||||
|
|
@ -1099,6 +1270,11 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
auth_email_cohort_map={"learner@hs.ac.kr": "pilot-a"},
|
||||
),
|
||||
patch.object(auth_routes.httpx, "AsyncClient", FakeAsyncClient),
|
||||
patch.object(
|
||||
auth_routes,
|
||||
"get_managed_user_by_email",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch.object(auth_routes, "create_session", create_session_mock),
|
||||
):
|
||||
response = await auth_routes.callback(
|
||||
|
|
@ -1116,6 +1292,7 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
role="learner",
|
||||
cohort_ids=["pilot-a", "hanshin-2026"],
|
||||
external_id="google:learner@hs.ac.kr",
|
||||
account_status="approved",
|
||||
)
|
||||
cookie_blob = "\n".join(
|
||||
value.decode("latin1")
|
||||
|
|
@ -1202,6 +1379,7 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
role="learner",
|
||||
cohort_ids=[],
|
||||
external_id="google:operator@hs.ac.kr",
|
||||
account_status="approved",
|
||||
)
|
||||
self.assertEqual(
|
||||
auth_routes._post_login_next_path(
|
||||
|
|
@ -1276,6 +1454,11 @@ class AuthProviderScaffoldTest(unittest.IsolatedAsyncioTestCase):
|
|||
session_secret="signed-oauth-state-secret",
|
||||
),
|
||||
patch.object(auth_routes.httpx, "AsyncClient", FakeAsyncClient),
|
||||
patch.object(
|
||||
auth_routes,
|
||||
"get_managed_user_by_email",
|
||||
AsyncMock(return_value=None),
|
||||
),
|
||||
patch.object(auth_routes, "create_session", AsyncMock(return_value=("opaque-session", object()))),
|
||||
):
|
||||
response = await auth_routes.callback(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue