diff --git a/apps/backend/app/extraction/support_matrix.py b/apps/backend/app/extraction/support_matrix.py index a5172f3f..757fc6d6 100644 --- a/apps/backend/app/extraction/support_matrix.py +++ b/apps/backend/app/extraction/support_matrix.py @@ -852,7 +852,7 @@ def _code_list(items: tuple[str, ...]) -> str: "authentication-isolation", "Authentication and owner isolation", PublicStatus.IMPLEMENTED, - "Email/password, Argon2, short-lived access tokens, rotating refresh tokens with reuse detection. Google and GitHub OAuth sign-in and account linking are implemented but inert until provider credentials are configured, and never create an account. Registration is gated by an admin-managed email allowlist in every environment except `development` (the default when running from source), where any address may register, and except the first account on an empty instance, which becomes its owner. Registration does not verify email ownership, so an approved address can be claimed by whoever registers it first; there is no admin role or in-app approval. Protected resources are owner-scoped; non-owner access returns 404.", + "Email/password, Argon2, short-lived access tokens, rotating refresh tokens with reuse detection. Google and GitHub OAuth sign-in and account linking are implemented but inert until provider credentials are configured, and can create an account for a verified new identity only through the same email-approval/bootstrap gate as password registration. Registration is gated by an admin-managed email allowlist in every environment except `development` (the default when running from source), where any address may register, and except the first account on an empty instance, which is permitted without prior approval and receives no administrator role. Registration does not verify email ownership, so an approved address can be claimed by whoever registers it first; there is no admin role or in-app approval. Protected resources are owner-scoped; non-owner access returns 404.", ("product.authentication-isolation",), ), PublicCapability( diff --git a/apps/backend/tests/test_oauth_service.py b/apps/backend/tests/test_oauth_service.py index 2fb302ee..2e8de2af 100644 --- a/apps/backend/tests/test_oauth_service.py +++ b/apps/backend/tests/test_oauth_service.py @@ -719,3 +719,24 @@ def test_lists_only_the_requested_users_identities(self, db): service = _make_service(db, {}) assert [i.provider for i in service.linked_identities(user_a.id)] == ["google"] assert [i.provider for i in service.linked_identities(user_b.id)] == ["github"] + + +@pytest.mark.parametrize("approved", [False, True]) +def test_public_oauth_capability_matches_verified_identity_creation_gate(db, approved): + import asyncio + from app.extraction.support_matrix import PUBLIC_CAPABILITIES + + capability = next(item for item in PUBLIC_CAPABILITIES if item.id == "authentication-isolation") + assert "can create an account for a verified new identity" in capability.statement + assert "same email-approval/bootstrap gate" in capability.statement + _create_user(db, "existing-contract@example.com") + email = "capability-newcomer@example.com" + if approved: + approve_email(db, email) + identity = OAuthIdentityInfo(subject="contract-identity", email=email, email_verified=True, display_name="Contract") + service = _make_service(db, {"google": FakeProviderClient(identity=identity)}) + url = service.start("google", intent="login", frontend_redirect_base="http://localhost:5173") + state = url.split("state=")[1].split("&")[0] + _, result = asyncio.run(service.complete_callback("google", state=state, code="c", provider_error=None)) + assert (result.kind == "session") is approved + assert db.query(User).filter(User.email == email).count() == int(approved) diff --git a/docs/CAPABILITIES.md b/docs/CAPABILITIES.md index 3d2341b7..fc12ca2b 100644 --- a/docs/CAPABILITIES.md +++ b/docs/CAPABILITIES.md @@ -31,7 +31,7 @@ release; `dev` carries active development. | --- | --- | --- | | Archive upload and public GitHub import | **Implemented** | ZIP/TAR-family archives and shallow public GitHub HTTPS clones; size and path-safety limits apply. Private GitHub cloning and other repository hosts are not supported. | | Repository explorer | **Implemented** | Owner-scoped file tree plus bounded text/image preview, binary detection, and truncation. | -| Authentication and owner isolation | **Implemented** | Email/password, Argon2, short-lived access tokens, rotating refresh tokens with reuse detection. Google and GitHub OAuth sign-in and account linking are implemented but inert until provider credentials are configured, and never create an account. Registration is gated by an admin-managed email allowlist in every environment except `development` (the default when running from source), where any address may register, and except the first account on an empty instance, which becomes its owner. Registration does not verify email ownership, so an approved address can be claimed by whoever registers it first; there is no admin role or in-app approval. Protected resources are owner-scoped; non-owner access returns 404. | +| Authentication and owner isolation | **Implemented** | Email/password, Argon2, short-lived access tokens, rotating refresh tokens with reuse detection. Google and GitHub OAuth sign-in and account linking are implemented but inert until provider credentials are configured, and can create an account for a verified new identity only through the same email-approval/bootstrap gate as password registration. Registration is gated by an admin-managed email allowlist in every environment except `development` (the default when running from source), where any address may register, and except the first account on an empty instance, which is permitted without prior approval and receives no administrator role. Registration does not verify email ownership, so an approved address can be claimed by whoever registers it first; there is no admin role or in-app approval. Protected resources are owner-scoped; non-owner access returns 404. | | Analysis lifecycle | **Implemented** | Database-backed, cancellable job with progress, bounded retry, lease renewal, and stale-worker recovery. | | Repository Intelligence | **Implemented with disclosed limits** | Immutable, revision-addressed `ri.v1` snapshots with normalized facts, evidence, query APIs, and a total canonical graph hash. Semantic extraction is strongest for supported Python and TypeScript/JavaScript constructs. | | Repository lineage | **Implemented with disclosed limits** | Repeated imports of the same repository and branch are grouped into a durable, owner-scoped lineage with duplicate-revision detection (RFC-0002). `GET /repositories/{id}/lineage` returns the ordered history and the repository detail page renders it. Refresh and cross-revision comparison on top of a lineage are not built. |