fix: add _stored_to_legacy helper, load_auth compat, and old-format roundtrip test
This commit is contained in:
@@ -102,9 +102,10 @@ auth admin are implemented. The next work is polish, not new broad surfaces.
|
||||
- Completed: server startup policy moved to `wf_server.cli`; JSON-RPC HTTP
|
||||
remains in `wf_transport_rpc_http`:
|
||||
[`server CLI and transport boundary`](superpowers/specs/2026-06-10-server-cli-transport-boundary.md).
|
||||
- Next auth work: typed/discriminated auth records, source-owned auth binders
|
||||
(`McpAuthBinder` first), OAuth refresh-token support, and Google Drive MCP smoke
|
||||
through `https://drivemcp.googleapis.com/mcp/v1`. Production secret manager
|
||||
- Next auth work: typed/discriminated auth records and source-owned auth binders
|
||||
(`McpAuthBinder` first) are now completed. Remaining: OAuth refresh-token
|
||||
support and Google Drive MCP smoke through
|
||||
`https://drivemcp.googleapis.com/mcp/v1`. Production secret manager
|
||||
integration and encrypted-at-rest file format remain deferred.
|
||||
- Active specs:
|
||||
- [`workflow config targets and sources`](superpowers/specs/2026-06-03-workflow-config-targets-and-sources.md)
|
||||
|
||||
@@ -19,7 +19,8 @@ diagnostics and source registry apply summaries. Slice 3 exposes read-only auth
|
||||
admin summaries without secret payload values. Slice 4 adds local/dev file-backed
|
||||
auth save/delete through neutral admin, JSON-RPC, and CLI. Responses still
|
||||
expose only ids, schemes, metadata, and payload keys; secret payload values
|
||||
remain write-only.
|
||||
remain write-only. Slice 5 introduces typed stored auth records and MCP auth
|
||||
binding while preserving old `scheme + payload` compatibility input.
|
||||
|
||||
Next auth work should replace the stringly `scheme + payload` record with typed
|
||||
auth variants and source-owned auth binders. Google Drive's remote HTTP MCP
|
||||
|
||||
@@ -11,13 +11,41 @@ from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from wf_api.auth import AuthRecord as NeutralAuthRecord
|
||||
from wf_api.auth import StoredAuthRecord, validate_auth_id
|
||||
from wf_api.auth import (
|
||||
BearerAuth,
|
||||
EnvAuth,
|
||||
HeaderAuth,
|
||||
OAuthRefreshTokenAuth,
|
||||
OpaqueAuth,
|
||||
StoredAuthRecord,
|
||||
validate_auth_id,
|
||||
)
|
||||
from wf_sources_mcp.auth import AuthRecord, mcp_auth_from_neutral, neutral_auth_from_mcp
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from wf_sources_mcp.catalog.models import CatalogSnapshot
|
||||
|
||||
|
||||
def _stored_to_legacy(record: StoredAuthRecord) -> AuthRecord:
|
||||
"""Convert a typed StoredAuthRecord back to legacy AuthRecord for compat."""
|
||||
auth = record.auth
|
||||
if isinstance(auth, BearerAuth):
|
||||
return AuthRecord(record.id, "bearer", {"token": auth.access_token})
|
||||
if isinstance(auth, HeaderAuth):
|
||||
return AuthRecord(record.id, "headers", {"headers": dict(auth.headers)})
|
||||
if isinstance(auth, EnvAuth):
|
||||
return AuthRecord(record.id, "env", {"env": dict(auth.env)})
|
||||
if isinstance(auth, OAuthRefreshTokenAuth):
|
||||
return AuthRecord(
|
||||
record.id,
|
||||
"oauth_refresh_token",
|
||||
auth.model_dump(mode="json", exclude={"kind"}),
|
||||
)
|
||||
if isinstance(auth, OpaqueAuth):
|
||||
return AuthRecord(record.id, auth.scheme, dict(auth.payload))
|
||||
raise TypeError(f"unsupported auth variant {type(auth).__name__}")
|
||||
|
||||
|
||||
class AuthStore:
|
||||
def save_auth(self, record: AuthRecord) -> None:
|
||||
raise NotImplementedError
|
||||
@@ -95,6 +123,8 @@ class FileAuthStore(AuthStore):
|
||||
if not path.exists():
|
||||
return None
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if "kind" in data.get("auth", {}):
|
||||
return _stored_to_legacy(StoredAuthRecord.model_validate(data))
|
||||
return AuthRecord(**data)
|
||||
|
||||
def list_auth_refs(self) -> list[str]:
|
||||
|
||||
@@ -127,3 +127,47 @@ def test_file_auth_store_writes_new_stored_auth_record_shape(tmp_path: Path) ->
|
||||
assert data["auth"]["kind"] == "bearer"
|
||||
assert data["auth"]["access_token"] == "token"
|
||||
assert data["metadata"]["provider"] == "google"
|
||||
|
||||
|
||||
def test_file_auth_store_loads_old_format_through_load_auth_record(tmp_path: Path) -> None:
|
||||
store = FileAuthStore(tmp_path)
|
||||
path = tmp_path / "auth" / "github.work.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"connection_id": "github.work",
|
||||
"scheme": "bearer",
|
||||
"payload": {"token": "secret"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
record = store.load_auth_record("github.work")
|
||||
|
||||
assert isinstance(record, NeutralAuthRecord)
|
||||
assert record.id == "github.work"
|
||||
assert record.scheme == "bearer"
|
||||
assert record.payload["token"] == "secret"
|
||||
|
||||
|
||||
def test_file_auth_store_loads_new_format_through_load_auth(tmp_path: Path) -> None:
|
||||
store = FileAuthStore(tmp_path)
|
||||
path = tmp_path / "auth" / "google.drive.personal.json"
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"id": "google.drive.personal",
|
||||
"auth": {"kind": "bearer", "access_token": "token"},
|
||||
"metadata": {"provider": "google"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
legacy = store.load_auth("google.drive.personal")
|
||||
|
||||
assert legacy is not None
|
||||
assert legacy.connection_id == "google.drive.personal"
|
||||
assert legacy.scheme == "bearer"
|
||||
assert legacy.payload["token"] == "token"
|
||||
|
||||
Reference in New Issue
Block a user