mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
fix: harden Horizon state boundaries
This commit is contained in:
parent
760bef1ab8
commit
86cbf38ed3
6 changed files with 147 additions and 11 deletions
|
|
@ -1,7 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
|
@ -11,6 +13,10 @@ import httpx2
|
|||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
from fastmcp.cli.deploy.configuration import (
|
||||
ConfigurationStore,
|
||||
HorizonConfiguration,
|
||||
)
|
||||
from fastmcp.cli.deploy.credentials import (
|
||||
AuthenticationRequiredError,
|
||||
CredentialStore,
|
||||
|
|
@ -63,6 +69,26 @@ def test_credential_store_restricts_an_existing_secret_file(tmp_path: Path) -> N
|
|||
assert store.path.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX directory fsync")
|
||||
def test_atomic_write_ignores_unsupported_directory_fsync(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
store = CredentialStore(tmp_path)
|
||||
original_fsync = os.fsync
|
||||
|
||||
def fsync(descriptor: int) -> None:
|
||||
if stat.S_ISDIR(os.fstat(descriptor).st_mode):
|
||||
raise OSError(errno.EINVAL, "directory sync is not supported")
|
||||
original_fsync(descriptor)
|
||||
|
||||
monkeypatch.setattr("fastmcp.cli.deploy.state.os.fsync", fsync)
|
||||
|
||||
store.save("fmcp_secret")
|
||||
|
||||
assert load_secret(store).get_secret_value() == "fmcp_secret"
|
||||
|
||||
|
||||
def test_atomic_write_preserves_previous_state_on_replace_failure(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
|
@ -132,6 +158,31 @@ async def test_interactive_credential_is_persisted(tmp_path: Path) -> None:
|
|||
assert load_secret(store).get_secret_value() == "fmcp_interactive"
|
||||
|
||||
|
||||
async def test_interactive_credential_rejects_an_origin_change(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
store = CredentialStore(tmp_path)
|
||||
ConfigurationStore(tmp_path).save(
|
||||
HorizonConfiguration(
|
||||
schemaVersion=1,
|
||||
apiOrigin="https://dev.horizon.prefect.io",
|
||||
)
|
||||
)
|
||||
|
||||
async def authorize() -> SecretStr:
|
||||
return SecretStr("fmcp_old_origin")
|
||||
|
||||
with pytest.raises(StateFileError, match="host changed"):
|
||||
await resolve_credential(
|
||||
store,
|
||||
environ={},
|
||||
authorize=authorize,
|
||||
expected_api_origin="https://horizon.prefect.io",
|
||||
)
|
||||
|
||||
assert store.load() is None
|
||||
|
||||
|
||||
async def test_missing_noninteractive_credential_is_explicit(tmp_path: Path) -> None:
|
||||
with pytest.raises(AuthenticationRequiredError):
|
||||
await resolve_credential(CredentialStore(tmp_path), environ={})
|
||||
|
|
|
|||
|
|
@ -249,6 +249,8 @@ async def test_invalid_responses_do_not_include_response_bodies() -> None:
|
|||
"https://user@example.com",
|
||||
"https://horizon.prefect.io/path",
|
||||
"https://horizon.prefect.io?query=value",
|
||||
"https://horizon.prefect.io:abc",
|
||||
"https://horizon.prefect.io:99999",
|
||||
],
|
||||
)
|
||||
def test_api_origin_rejects_values_that_are_not_origins(value: str) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue