feat: add auth admin mutations

This commit is contained in:
lda
2026-06-06 16:49:34 +07:00 Verified
parent 56104c7849
commit f137b8fcb2
17 changed files with 627 additions and 15 deletions
+6 -2
View File
@@ -238,8 +238,12 @@ implementation state.
available through MCP-backed server admin, JSON-RPC, and CLI. Summaries show
ids, schemes, metadata, and payload keys only; secret payload values remain
hidden.
Not done: auth is still compatibility-grade. There is no auth mutation UI/CLI,
OAuth flow, production secret manager, provider-specific display model, or
Fourth implementation slice complete: local/dev auth records can be saved and
deleted through neutral admin, JSON-RPC, and `wf admin auth`. This is still not
a production secret manager or OAuth flow; payload values are accepted only as
write inputs and never returned.
Not done: auth is still compatibility-grade. There is no OAuth flow,
production secret manager, provider-specific display model, or
full removal of the legacy MCP auth record shape yet.
- Completed: `wf run watch` starts run progress UX with polling over existing
`inspect_run` and optional bounded `read_run_trace`. SSE/WebSocket/MCP
@@ -16,8 +16,11 @@ the architecture.
Slice 1 implements the neutral auth record/store protocol and MCP compatibility
bridge. Slice 2 surfaces missing explicit auth refs through live source
diagnostics and source registry apply summaries. Slice 3 exposes read-only auth
admin summaries without secret payload values. Auth mutation surfaces and
provider-specific auth unions are future slices.
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. OAuth, production secret managers, and provider-specific auth
variants remain future work.
This is not a complete auth product yet. The implemented runtime path only wires
existing MCP-compatible auth records into source calls, diagnostics, and
+15
View File
@@ -286,6 +286,21 @@ generate prose.
## Common Diagnostics
### Local/dev auth records
Auth payload values are write-only. `list`, `inspect`, `save`, and `delete`
responses show ids, schemes, metadata, and payload keys only.
```powershell
wf admin auth save drive.work --scheme bearer --payload-file drive-auth.json
wf admin auth list
wf admin auth inspect drive.work
wf admin auth delete drive.work --confirm
```
Use source `auth_ref` values to point sources at these records. Do not commit
payload files containing real secrets.
### `source_missing`
A required logical source is not available or not bound.
+30 -1
View File
@@ -4,6 +4,8 @@ from collections.abc import Mapping, Sequence
from dataclasses import asdict, is_dataclass
from typing import Any, Protocol
from wf_api.auth import AuthRecord
class WorkflowAdminConnectionProvider(Protocol):
"""Provides read-only connection inventory for admin frontends."""
@@ -20,12 +22,16 @@ class WorkflowAdminEventProvider(Protocol):
class WorkflowAdminAuthProvider(Protocol):
"""Provides read-only auth inventory without secret payload values."""
"""Provides auth inventory and local/dev auth mutation."""
def list_auth_records(self) -> Sequence[Mapping[str, Any] | object]: ...
def inspect_auth_record(self, auth_ref: str) -> Mapping[str, Any] | object: ...
def save_auth_record(self, record: AuthRecord) -> Mapping[str, Any] | object: ...
def delete_auth_record(self, auth_ref: str) -> Mapping[str, Any] | object: ...
class WorkflowAdminApi:
"""Protocol-neutral read-only broker/server admin operations.
@@ -79,6 +85,29 @@ class WorkflowAdminApi:
raise RuntimeError("auth admin is not available for this target")
return _payload(self.auth.inspect_auth_record(auth_ref))
async def save_auth_record(
self,
*,
auth_ref: str,
scheme: str,
payload: Mapping[str, object],
metadata: Mapping[str, object] | None = None,
) -> dict[str, Any]:
if self.auth is None:
raise RuntimeError("auth admin is not available for this target")
record = AuthRecord(
id=auth_ref,
scheme=scheme,
payload=dict(payload),
metadata=dict(metadata or {}),
)
return _payload(self.auth.save_auth_record(record))
async def delete_auth_record(self, auth_ref: str) -> dict[str, Any]:
if self.auth is None:
raise RuntimeError("auth admin is not available for this target")
return _payload(self.auth.delete_auth_record(auth_ref))
def _payload(value: Mapping[str, Any] | object) -> dict[str, Any]:
"""Normalize provider objects without depending on MCP event/config types."""
+12 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from typing import Any, Protocol
from wf_artifacts import ArtifactKind
@@ -229,6 +229,17 @@ class WorkflowAdminSurface(Protocol):
async def inspect_auth_record(self, auth_ref: str) -> dict[str, Any]: ...
async def save_auth_record(
self,
*,
auth_ref: str,
scheme: str,
payload: Mapping[str, object],
metadata: Mapping[str, object] | None = None,
) -> dict[str, Any]: ...
async def delete_auth_record(self, auth_ref: str) -> dict[str, Any]: ...
class WorkflowSourceRegistrySurface(Protocol):
"""Desired source registry methods exposed by platform frontends."""
+90 -1
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Annotated
import typer
@@ -11,11 +13,39 @@ from wf_cli.remote_errors import run_cli_operation
app = typer.Typer(
name="auth",
help="Read auth record status without exposing secret payload values.",
help="Manage local/dev auth records without exposing secret payload values.",
no_args_is_help=True,
)
def _read_json_object(
inline: str | None,
file_path: str | None,
flag_names: str,
) -> dict[str, object]:
if inline and file_path:
raise typer.BadParameter(f"provide exactly one of {flag_names}")
if inline:
try:
value = json.loads(inline)
except json.JSONDecodeError as exc:
raise typer.BadParameter(f"invalid JSON: {exc}") from exc
if not isinstance(value, dict):
raise typer.BadParameter(f"{flag_names} must be a JSON object")
return dict(value)
if file_path:
try:
value = json.loads(Path(file_path).read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise typer.BadParameter(f"file not found: {file_path}") from exc
except json.JSONDecodeError as exc:
raise typer.BadParameter(f"invalid JSON in file: {exc}") from exc
if not isinstance(value, dict):
raise typer.BadParameter(f"{flag_names} must be a JSON object")
return dict(value)
raise typer.BadParameter(f"{flag_names} is required")
@app.command("list")
def list_auth_records(
ctx: typer.Context,
@@ -47,3 +77,62 @@ def inspect_auth_record(
context.admin.inspect_auth_record(auth_ref),
)
emit_json(payload)
@app.command("save")
def save_auth_record(
ctx: typer.Context,
auth_ref: Annotated[str, typer.Argument(help="Auth record id/ref.")],
scheme: Annotated[str, typer.Option("--scheme", help="Auth scheme/kind.")],
payload_json: Annotated[
str | None,
typer.Option("--payload", help="Secret payload JSON object."),
] = None,
payload_file: Annotated[
str | None,
typer.Option("--payload-file", help="File containing secret payload JSON object."),
] = None,
metadata_json: Annotated[
str | None,
typer.Option("--metadata", help="Non-secret metadata JSON object."),
] = None,
metadata_file: Annotated[
str | None,
typer.Option("--metadata-file", help="File containing non-secret metadata JSON object."),
] = None,
) -> None:
"""Save or replace a local/dev auth record; response never includes payload values."""
payload = _read_json_object(payload_json, payload_file, "--payload/--payload-file")
metadata = (
_read_json_object(metadata_json, metadata_file, "--metadata/--metadata-file")
if metadata_json or metadata_file
else None
)
context = load_cli_context_from_typer(ctx)
result = run_cli_operation(
context,
context.admin.save_auth_record(
auth_ref=auth_ref,
scheme=scheme,
payload=payload,
metadata=metadata,
),
)
emit_json(result)
@app.command("delete")
def delete_auth_record(
ctx: typer.Context,
auth_ref: Annotated[str, typer.Argument(help="Auth record id/ref.")],
confirm: Annotated[
bool,
typer.Option("--confirm", help="Required to delete an auth record."),
] = False,
) -> None:
"""Delete a local/dev auth record."""
if not confirm:
raise typer.BadParameter("--confirm is required to delete an auth record")
context = load_cli_context_from_typer(ctx)
result = run_cli_operation(context, context.admin.delete_auth_record(auth_ref))
emit_json(result)
+12 -1
View File
@@ -4,13 +4,14 @@ from dataclasses import dataclass
from typing import Any
from wf_api import WorkflowAdminAuthProvider
from wf_api.auth import AuthRecord as NeutralAuthRecord
from ...storage import Store
@dataclass(frozen=True, slots=True)
class McpAuthAdminProvider(WorkflowAdminAuthProvider):
"""Read-only auth inventory for MCP-backed workflow servers.
"""Auth inventory and local/dev mutation for MCP-backed workflow servers.
Summaries intentionally expose payload keys, not payload values. Concrete
auth variants can provide richer safe display later.
@@ -35,5 +36,15 @@ class McpAuthAdminProvider(WorkflowAdminAuthProvider):
"payload_keys": sorted(str(key) for key in record.payload),
}
def save_auth_record(self, record: NeutralAuthRecord) -> dict[str, Any]:
self.store.save_auth_record(record)
return self.inspect_auth_record(record.id)
def delete_auth_record(self, auth_ref: str) -> dict[str, Any]:
deleted = self.store.delete_auth_record(auth_ref)
if not deleted:
raise KeyError(f"unknown auth record {auth_ref!r}")
return {"deleted": True, "id": auth_ref}
__all__ = ["McpAuthAdminProvider"]
+32 -3
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
import json
from pathlib import Path
from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_api.auth import AuthRecord as NeutralAuthRecord, validate_auth_id
from wf_mcp.capabilities import (
CatalogNodeEntry,
CatalogPromptEntry,
@@ -34,6 +34,12 @@ class Store:
def load_auth_record(self, auth_ref: str) -> NeutralAuthRecord | None:
raise NotImplementedError
def delete_auth(self, connection_id: str) -> bool:
raise NotImplementedError
def delete_auth_record(self, auth_ref: str) -> bool:
raise NotImplementedError
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
raise NotImplementedError
@@ -56,8 +62,20 @@ class FileStore(Store):
def catalog_dir(self) -> Path:
return self.root / "catalog"
def _auth_path(self, connection_id: str) -> Path:
return self._connection_path(self.auth_dir, connection_id)
def _auth_path(self, auth_ref: str) -> Path:
"""Map one auth ref to one file.
Auth refs used to be connection ids, but neutral auth refs now carry no
provider/account semantics. Keep catalog paths on connection-id
validation while auth storage accepts the wider auth-id contract.
"""
validate_auth_id(auth_ref)
root = self.auth_dir.resolve()
path = (self.auth_dir / f"{auth_ref}.json").resolve()
if path.parent != root:
raise ValueError(f"auth ref escapes store directory: {auth_ref!r}")
return path
def _catalog_path(self, connection_id: str) -> Path:
return self._connection_path(self.catalog_dir, connection_id)
@@ -112,6 +130,17 @@ class FileStore(Store):
return None
return neutral_auth_from_mcp(record)
def delete_auth(self, connection_id: str) -> bool:
path = self._auth_path(connection_id)
if not path.exists():
return False
path.unlink()
return True
def delete_auth_record(self, auth_ref: str) -> bool:
"""Delete neutral auth through the legacy MCP file shape."""
return self.delete_auth(auth_ref)
def save_catalog(self, snapshot: CatalogSnapshot) -> None:
self._catalog_path(snapshot.connection_id).write_text(
json.dumps(dump_catalog_snapshot(snapshot), indent=2),
+25
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
@@ -25,3 +26,27 @@ class RpcAdminClientMixin:
"workflow.admin.auth.inspect",
{"auth_ref": auth_ref},
)
async def save_auth_record(
self,
*,
auth_ref: str,
scheme: str,
payload: Mapping[str, object],
metadata: Mapping[str, object] | None = None,
) -> dict[str, Any]:
return await self._call(
"workflow.admin.auth.save",
{
"auth_ref": auth_ref,
"scheme": scheme,
"payload": dict(payload),
"metadata": dict(metadata or {}),
},
)
async def delete_auth_record(self, auth_ref: str) -> dict[str, Any]:
return await self._call(
"workflow.admin.auth.delete",
{"auth_ref": auth_ref},
)
+42 -1
View File
@@ -7,7 +7,7 @@ import fastapi_jsonrpc as jsonrpc
from wf_server import WorkflowServer
from .errors import WorkflowRpcError, raise_workflow_rpc_error
from .models import AdminEmptyParams, InspectAuthParams
from .models import AdminEmptyParams, DeleteAuthParams, InspectAuthParams, SaveAuthParams
from .params import RpcParams
@@ -88,3 +88,44 @@ def register_methods(
RuntimeError,
) as exc:
raise_workflow_rpc_error(exc)
@entrypoint.method(
name="workflow.admin.auth.save",
errors=[WorkflowRpcError],
)
async def workflow_admin_auth_save(
params: SaveAuthParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.admin.save_auth_record(
auth_ref=params.auth_ref,
scheme=params.scheme,
payload=params.payload,
metadata=params.metadata,
)
except (
ValueError,
KeyError,
LookupError,
FileNotFoundError,
RuntimeError,
) as exc:
raise_workflow_rpc_error(exc)
@entrypoint.method(
name="workflow.admin.auth.delete",
errors=[WorkflowRpcError],
)
async def workflow_admin_auth_delete(
params: DeleteAuthParams = RpcParams(),
) -> dict[str, Any]:
try:
return await server.admin.delete_auth_record(params.auth_ref)
except (
ValueError,
KeyError,
LookupError,
FileNotFoundError,
RuntimeError,
) as exc:
raise_workflow_rpc_error(exc)
+11
View File
@@ -209,3 +209,14 @@ class ApplyRegistryChangesParams(RpcParamsModel):
class InspectAuthParams(RpcParamsModel):
auth_ref: str = Field(min_length=1)
class SaveAuthParams(RpcParamsModel):
auth_ref: str = Field(min_length=1)
scheme: str = Field(min_length=1)
payload: dict[str, Any] = Field(default_factory=dict)
metadata: dict[str, Any] = Field(default_factory=dict)
class DeleteAuthParams(RpcParamsModel):
auth_ref: str = Field(min_length=1)
+84
View File
@@ -7,6 +7,7 @@ from typing import Any
import pytest
from wf_api import WorkflowAdminApi, WorkflowAdminSurface
from wf_api.auth import AuthRecord
@dataclass(frozen=True, slots=True)
@@ -159,3 +160,86 @@ def test_admin_auth_methods_report_unavailable_without_provider() -> None:
with pytest.raises(RuntimeError, match="auth admin is not available"):
asyncio.run(_api().inspect_auth_record("github.work"))
class MutableAuthProvider(AuthProvider):
def __init__(self) -> None:
self.records: dict[str, dict[str, Any]] = {}
def list_auth_records(self):
return list(self.records.values())
def inspect_auth_record(self, auth_ref: str):
try:
return self.records[auth_ref]
except KeyError as exc:
raise KeyError(auth_ref) from exc
def save_auth_record(self, record: AuthRecord):
self.records[record.id] = {
"id": record.id,
"scheme": record.scheme,
"metadata": dict(record.metadata),
"payload_keys": sorted(str(key) for key in record.payload),
}
return self.records[record.id]
def delete_auth_record(self, auth_ref: str):
if auth_ref not in self.records:
raise KeyError(auth_ref)
del self.records[auth_ref]
return {"deleted": True, "id": auth_ref}
def test_admin_saves_auth_record_without_payload_values() -> None:
provider = MutableAuthProvider()
api = _api(provider)
payload = asyncio.run(
api.save_auth_record(
auth_ref="drive.work",
scheme="bearer",
payload={"token": "secret"},
metadata={"owner": "test"},
)
)
assert payload == {
"id": "drive.work",
"scheme": "bearer",
"metadata": {"owner": "test"},
"payload_keys": ["token"],
}
assert "secret" not in str(payload)
def test_admin_deletes_auth_record() -> None:
provider = MutableAuthProvider()
api = _api(provider)
asyncio.run(
api.save_auth_record(
auth_ref="drive.work",
scheme="bearer",
payload={"token": "secret"},
)
)
payload = asyncio.run(api.delete_auth_record("drive.work"))
assert payload == {"deleted": True, "id": "drive.work"}
with pytest.raises(KeyError):
provider.inspect_auth_record("drive.work")
def test_admin_auth_mutations_report_unavailable_without_provider() -> None:
with pytest.raises(RuntimeError, match="auth admin is not available"):
asyncio.run(
_api().save_auth_record(
auth_ref="drive.work",
scheme="bearer",
payload={"token": "secret"},
)
)
with pytest.raises(RuntimeError, match="auth admin is not available"):
asyncio.run(_api().delete_auth_record("drive.work"))
+118 -1
View File
@@ -1,11 +1,17 @@
from __future__ import annotations
import json
from typer.testing import CliRunner
from wf_cli.app import app
class FakeAdmin:
def __init__(self) -> None:
self._saved: dict[str, dict] = {}
self._deleted: list[str] = []
async def list_auth_records(self):
return {
"auth_records": [
@@ -27,9 +33,28 @@ class FakeAdmin:
"payload_keys": ["token"],
}
async def save_auth_record(self, *, auth_ref, scheme, payload, metadata=None):
self._saved[auth_ref] = {
"auth_ref": auth_ref,
"scheme": scheme,
"payload": payload,
"metadata": metadata,
}
return {
"id": auth_ref,
"scheme": scheme,
"metadata": metadata or {},
"payload_keys": sorted(str(key) for key in payload),
}
async def delete_auth_record(self, auth_ref):
self._deleted.append(auth_ref)
return {"deleted": True, "id": auth_ref}
class FakeContext:
admin = FakeAdmin()
def __init__(self) -> None:
self.admin = FakeAdmin()
verbose = False
@@ -58,3 +83,95 @@ def test_wf_admin_auth_inspect(monkeypatch) -> None:
assert "github.work" in result.stdout
assert "payload_keys" in result.stdout
assert "secret" not in result.stdout
def test_wf_admin_auth_save(monkeypatch) -> None:
fake_ctx = FakeContext()
monkeypatch.setattr(
"wf_cli.commands.auth_admin.load_cli_context_from_typer",
lambda ctx: fake_ctx,
)
result = CliRunner().invoke(
app,
[
"admin",
"auth",
"save",
"drive.work",
"--scheme",
"bearer",
"--payload",
'{"token":"secret"}',
],
)
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["id"] == "drive.work"
assert payload["payload_keys"] == ["token"]
assert "secret" not in result.stdout
assert fake_ctx.admin._saved["drive.work"] == {
"auth_ref": "drive.work",
"scheme": "bearer",
"payload": {"token": "secret"},
"metadata": None,
}
def test_wf_admin_auth_save_reads_payload_file(tmp_path, monkeypatch) -> None:
fake_ctx = FakeContext()
payload_file = tmp_path / "auth.json"
payload_file.write_text('{"token":"secret"}', encoding="utf-8")
monkeypatch.setattr(
"wf_cli.commands.auth_admin.load_cli_context_from_typer",
lambda ctx: fake_ctx,
)
result = CliRunner().invoke(
app,
[
"admin",
"auth",
"save",
"drive.work",
"--scheme",
"bearer",
"--payload-file",
str(payload_file),
],
)
assert result.exit_code == 0
assert fake_ctx.admin._saved["drive.work"]["payload"] == {"token": "secret"}
def test_wf_admin_auth_delete_requires_confirm(monkeypatch) -> None:
fake_ctx = FakeContext()
monkeypatch.setattr(
"wf_cli.commands.auth_admin.load_cli_context_from_typer",
lambda ctx: fake_ctx,
)
result = CliRunner().invoke(app, ["admin", "auth", "delete", "drive.work"])
assert result.exit_code != 0
assert "confirm" in (result.stdout + result.output).lower()
assert fake_ctx.admin._deleted == []
def test_wf_admin_auth_delete(monkeypatch) -> None:
fake_ctx = FakeContext()
monkeypatch.setattr(
"wf_cli.commands.auth_admin.load_cli_context_from_typer",
lambda ctx: fake_ctx,
)
result = CliRunner().invoke(
app,
["admin", "auth", "delete", "drive.work", "--confirm"],
)
assert result.exit_code == 0
assert json.loads(result.stdout) == {"deleted": True, "id": "drive.work"}
assert fake_ctx.admin._deleted == ["drive.work"]
+46
View File
@@ -4,6 +4,7 @@ from pathlib import Path
import pytest
from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_mcp.broker.service.auth_admin import McpAuthAdminProvider
from wf_mcp.models import AuthRecord
from wf_mcp.storage import FileStore
@@ -73,3 +74,48 @@ def test_auth_admin_inspect_unknown_raises_key_error(tmp_path: Path) -> None:
with pytest.raises(KeyError, match="unknown auth record"):
provider.inspect_auth_record("missing.auth")
def test_auth_admin_provider_saves_auth_without_returning_payload(tmp_path) -> None:
store = FileStore(tmp_path / "store")
provider = McpAuthAdminProvider(store)
payload = provider.save_auth_record(
NeutralAuthRecord(
id="drive.work",
scheme="bearer",
payload={"token": "secret"},
metadata={"owner": "test"},
)
)
assert payload == {
"id": "drive.work",
"scheme": "bearer",
"metadata": {},
"payload_keys": ["token"],
}
assert "secret" not in str(payload)
assert store.load_auth("drive.work") == AuthRecord(
connection_id="drive.work",
scheme="bearer",
payload={"token": "secret"},
)
def test_auth_admin_provider_deletes_auth(tmp_path) -> None:
store = FileStore(tmp_path / "store")
provider = McpAuthAdminProvider(store)
store.save_auth(AuthRecord(connection_id="drive.work", scheme="bearer"))
payload = provider.delete_auth_record("drive.work")
assert payload == {"deleted": True, "id": "drive.work"}
assert store.load_auth("drive.work") is None
def test_auth_admin_provider_delete_unknown_auth_raises_key_error(tmp_path) -> None:
provider = McpAuthAdminProvider(FileStore(tmp_path / "store"))
with pytest.raises(KeyError, match="unknown auth record 'missing.auth'"):
provider.delete_auth_record("missing.auth")
+52 -2
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import pytest
from wf_api.auth import AuthRecord as NeutralAuthRecord
from wf_mcp.connections import parse_connection_id
from wf_mcp.models import AuthRecord, CatalogSnapshot
from wf_mcp.storage import FileStore
@@ -29,7 +30,7 @@ def test_parse_connection_id_rejects_path_traversal() -> None:
parse_connection_id(connection_id)
def test_file_store_rejects_auth_connection_id_path_traversal(tmp_path) -> None:
def test_file_store_rejects_auth_ref_path_traversal(tmp_path) -> None:
store = FileStore(tmp_path / "store")
record = AuthRecord(
connection_id="../outside",
@@ -37,7 +38,7 @@ def test_file_store_rejects_auth_connection_id_path_traversal(tmp_path) -> None:
payload={"token": "secret"},
)
with pytest.raises(ValueError, match="connection id"):
with pytest.raises(ValueError, match="auth id"):
store.save_auth(record)
assert not (tmp_path / "outside.json").exists()
@@ -58,3 +59,52 @@ def test_file_store_rejects_catalog_connection_id_path_traversal(tmp_path) -> No
store.save_catalog(snapshot)
assert not (tmp_path / "outside.json").exists()
def test_file_store_deletes_auth_record(tmp_path) -> None:
store = FileStore(tmp_path / "store")
record = AuthRecord(
connection_id="drive.work",
scheme="bearer",
payload={"token": "secret"},
)
store.save_auth(record)
assert store.load_auth("drive.work") == record
assert store.delete_auth("drive.work") is True
assert store.load_auth("drive.work") is None
assert store.delete_auth("drive.work") is False
def test_file_store_deletes_neutral_auth_record(tmp_path) -> None:
store = FileStore(tmp_path / "store")
store.save_auth_record(
NeutralAuthRecord(
id="drive.work",
scheme="bearer",
payload={"token": "secret"},
)
)
assert store.delete_auth_record("drive.work") is True
assert store.load_auth_record("drive.work") is None
def test_file_store_accepts_neutral_auth_ref_without_connection_shape(
tmp_path,
) -> None:
store = FileStore(tmp_path / "store")
record = NeutralAuthRecord(
id="api_ci-1",
scheme="bearer",
payload={"token": "secret"},
)
store.save_auth_record(record)
loaded = store.load_auth_record("api_ci-1")
assert loaded is not None
assert loaded.id == "api_ci-1"
assert loaded.scheme == "bearer"
assert loaded.payload == {"token": "secret"}
assert store.delete_auth_record("api_ci-1") is True
@@ -55,3 +55,50 @@ async def test_rpc_inspects_auth_record(tmp_path) -> None:
assert payload["id"] == "github.work"
assert payload["payload_keys"] == ["token"]
assert "payload" not in payload
async def test_rpc_saves_auth_record_without_returning_payload(tmp_path) -> None:
store = FileStore(tmp_path / "store")
config = BrokerConfig(store_root=store.root, connections=[])
server = build_workflow_server_from_config(config)
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport, base_url="http://test"
) as http_client:
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
payload = await client.save_auth_record(
auth_ref="drive.work",
scheme="bearer",
payload={"token": "secret"},
metadata={"owner": "test"},
)
assert payload["id"] == "drive.work"
assert payload["scheme"] == "bearer"
assert payload["payload_keys"] == ["token"]
assert "secret" not in str(payload)
assert FileStore(store.root).load_auth("drive.work") == AuthRecord(
connection_id="drive.work",
scheme="bearer",
payload={"token": "secret"},
)
async def test_rpc_deletes_auth_record(tmp_path) -> None:
store = FileStore(tmp_path / "store")
store.save_auth(AuthRecord(connection_id="drive.work", scheme="bearer"))
config = BrokerConfig(store_root=store.root, connections=[])
server = build_workflow_server_from_config(config)
app = create_rpc_app(server)
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(
transport=transport, base_url="http://test"
) as http_client:
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
payload = await client.delete_auth_record("drive.work")
assert payload == {"deleted": True, "id": "drive.work"}
assert FileStore(store.root).load_auth("drive.work") is None