Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions apps/api/app/services/auth/current_user_authentication_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@

from __future__ import annotations

import hashlib

from app.services.auth.api_key_authentication_service import (
APIKeyAuthenticationService,
)
from app.services.auth.dashboard_jwt_authentication_service import (
DashboardJWTAuthenticationService,
get_dashboard_jwt_authentication_service,
)
from loguru import logger
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as postgres_insert
from sqlalchemy.ext.asyncio import AsyncSession

from shared.core.database import get_db_context
from shared.core.exceptions.domain_exceptions import AuthException
from shared.models.database.user import User
from shared.utils.api_keys import is_api_key_token
Expand Down Expand Up @@ -79,13 +84,30 @@ async def _ensure_authenticated_user_exists(
if result.scalar_one_or_none() is not None:
return

raise AuthException(
user_message="Invalid authentication credentials",
internal_message=(
"Authenticated user id is not present in the user table: "
f"user_id={user_id}"
),
await _provision_dashboard_user_reference(user_id)


async def _provision_dashboard_user_reference(user_id: str) -> None:
async with get_db_context() as session:
await session.execute(
postgres_insert(User)
.values(
id=user_id,
name="Dashboard User",
email=_build_dashboard_user_reference_email(user_id),
)
.on_conflict_do_nothing(index_elements=[User.id])
)
await session.flush()
logger.info(
"Provisioned Dashboard-authenticated API user reference: user_id={}",
user_id,
)


def _build_dashboard_user_reference_email(user_id: str) -> str:
user_id_hash = hashlib.sha256(user_id.encode("utf-8")).hexdigest()
return f"dashboard-user-{user_id_hash}@reference.knowhere.local"


_current_user_authentication_service = CurrentUserAuthenticationService()
Expand Down
14 changes: 14 additions & 0 deletions apps/api/app/services/demo/official_library_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ def get_official_library_catalog(
source_payload_by_demo_source_id=source_payload_by_demo_source_id,
)
for source in _OFFICIAL_LIBRARY_SOURCES
if _is_publishable_ready_source(
source=source,
source_payload_by_demo_source_id=source_payload_by_demo_source_id,
)
],
}

Expand All @@ -337,6 +341,16 @@ def iter_official_library_categories() -> tuple[OfficialLibraryCategoryDefinitio
return _OFFICIAL_LIBRARY_CATEGORIES


def _is_publishable_ready_source(
*,
source: OfficialLibrarySourceDefinition,
source_payload_by_demo_source_id: dict[str, dict[str, Any]],
) -> bool:
if source.status != "ready" or source.demo_source_id is None:
return False
return source.demo_source_id in source_payload_by_demo_source_id


def _source_payload(
*,
source: OfficialLibrarySourceDefinition,
Expand Down
5 changes: 4 additions & 1 deletion apps/api/tests/contract/test_demo_documents_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ async def test_should_return_demo_catalog_with_resolvable_canonical_citations(
"demo-stem-transformers-tutorial"
)
assert library_sources_by_id["stem-transformers-tutorial"]["chunk_count"] == 519
assert library_sources_by_id["stem-information-theory"]["status"] == "planned"
assert "stem-information-theory" not in library_sources_by_id
assert {str(library_source["status"]) for library_source in library_sources} == {
"ready"
}

async with api_client_factory() as api_client:
chunks_response = await api_client.get(
Expand Down
56 changes: 46 additions & 10 deletions apps/api/tests/contract/test_job_creation_contract.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager
from datetime import datetime, timedelta, timezone
import hashlib
import json
import socket
from typing import cast
Expand Down Expand Up @@ -410,11 +411,11 @@ async def test_should_reject_a_malformed_authorization_header_when_creating_a_jo


@pytest.mark.asyncio
async def test_should_reject_authenticated_user_id_missing_from_user_table(
async def test_should_create_user_reference_for_dashboard_authenticated_user(
api_client_factory: Callable[[], AbstractAsyncContextManager[AsyncClient]],
monkeypatch: MonkeyPatch,
) -> None:
user_id = f"contract-missing-user-{uuid4().hex[:12]}"
user_id = f"contract-dashboard-user-{uuid4().hex[:12]}"
jwt_secret = f"contract-jwt-secret-{uuid4().hex[:12]}"
token = jwt.encode(
{
Expand All @@ -428,7 +429,7 @@ async def test_should_reject_authenticated_user_id_missing_from_user_table(
"namespace": "contract-jobs",
"source_type": "file",
"file_name": "contract-upload.pdf",
"data_id": "contract-job-missing-user",
"data_id": "contract-job-dashboard-user",
}

async with api_client_factory() as api_client:
Expand All @@ -445,17 +446,52 @@ async def test_should_reject_authenticated_user_id_missing_from_user_table(
api_client.headers.update({"Authorization": f"Bearer {token}"})
response = await api_client.post("/api/v1/jobs", json=payload)

assert response.status_code == 401
assert response.status_code == 200
assert response.headers["x-request-id"]

response_json: dict[str, object] = response.json()
error = cast(dict[str, object], response_json["error"])
job_id = cast(str, response_json["job_id"])

assert response_json["success"] is False
assert error["code"] == "UNAUTHENTICATED"
assert error["message"] == "Invalid authentication credentials"
assert "details" not in error
assert await _count_jobs() == 0
assert job_id.startswith("job_")
assert response_json["status"] == "waiting-file"
assert response_json["source_type"] == "file"
assert response_json["namespace"] == payload["namespace"]
assert response_json["data_id"] == payload["data_id"]

engine = await _create_contract_engine()
try:
async with engine.begin() as connection:
user_row = (
await connection.execute(
text(
"""
SELECT
id,
name,
email
FROM "user"
WHERE id = :user_id
"""
),
{"user_id": user_id},
)
).mappings().one()
finally:
await engine.dispose()

job_record = await _load_job_record(job_id)

assert dict(user_row) == {
"id": user_id,
"name": "Dashboard User",
"email": (
"dashboard-user-"
f"{hashlib.sha256(user_id.encode('utf-8')).hexdigest()}"
"@reference.knowhere.local"
),
}
assert job_record["user_id"] == user_id
assert await _count_jobs() == 1


@pytest.mark.asyncio
Expand Down
Loading