feat: integrate MinIO-backed user filesystem
This commit is contained in:
212
authz-service/src/tests/test_minio_deprovisioning.py
Normal file
212
authz-service/src/tests/test_minio_deprovisioning.py
Normal file
@ -0,0 +1,212 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.minio_provisioning import (
|
||||
MinIOProvisioningConfig,
|
||||
deprovision_user_file_minio_resources,
|
||||
)
|
||||
from app.models import MinIOSettings
|
||||
|
||||
|
||||
class _FakeObject:
|
||||
def __init__(self, object_name: str) -> None:
|
||||
self.object_name = object_name
|
||||
|
||||
|
||||
class _FakeMinio:
|
||||
bucket_exists_value = True
|
||||
objects: list[str] = []
|
||||
removed_objects: list[str] = []
|
||||
|
||||
def __init__(self, **_kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
def bucket_exists(self, bucket: str) -> bool:
|
||||
return self.bucket_exists_value
|
||||
|
||||
def list_objects(self, bucket: str, *, prefix: str, recursive: bool) -> list[_FakeObject]:
|
||||
return [_FakeObject(name) for name in self.objects if name.startswith(prefix)]
|
||||
|
||||
def remove_objects(self, bucket: str, objects: list[Any]) -> list[Any]:
|
||||
self.removed_objects.extend(item.object_name for item in objects)
|
||||
return []
|
||||
|
||||
|
||||
class _FakeAdmin:
|
||||
calls: list[tuple[str, Any]] = []
|
||||
missing = False
|
||||
|
||||
def __init__(self, **_kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
def detach_policy(self, **kwargs: Any) -> None:
|
||||
self.calls.append(("detach_policy", kwargs))
|
||||
if self.missing:
|
||||
raise RuntimeError("policy not found")
|
||||
|
||||
def user_remove(self, access_key: str) -> None:
|
||||
self.calls.append(("user_remove", access_key))
|
||||
if self.missing:
|
||||
raise RuntimeError("user not found")
|
||||
|
||||
def policy_remove(self, policy_name: str) -> None:
|
||||
self.calls.append(("policy_remove", policy_name))
|
||||
if self.missing:
|
||||
raise RuntimeError("policy not found")
|
||||
|
||||
|
||||
class _FakeStaticProvider:
|
||||
def __init__(self, **_kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class _FakeDeleteObject:
|
||||
def __init__(self, object_name: str) -> None:
|
||||
self.object_name = object_name
|
||||
|
||||
|
||||
def _install_fake_minio(monkeypatch) -> None:
|
||||
minio_module = ModuleType("minio")
|
||||
minio_module.Minio = _FakeMinio
|
||||
minio_module.MinioAdmin = _FakeAdmin
|
||||
|
||||
credentials_module = ModuleType("minio.credentials")
|
||||
credentials_module.StaticProvider = _FakeStaticProvider
|
||||
|
||||
deleteobjects_module = ModuleType("minio.deleteobjects")
|
||||
deleteobjects_module.DeleteObject = _FakeDeleteObject
|
||||
|
||||
monkeypatch.setitem(sys.modules, "minio", minio_module)
|
||||
monkeypatch.setitem(sys.modules, "minio.credentials", credentials_module)
|
||||
monkeypatch.setitem(sys.modules, "minio.deleteobjects", deleteobjects_module)
|
||||
_FakeMinio.bucket_exists_value = True
|
||||
_FakeMinio.objects = []
|
||||
_FakeMinio.removed_objects = []
|
||||
_FakeAdmin.calls = []
|
||||
_FakeAdmin.missing = False
|
||||
|
||||
|
||||
def _config() -> MinIOProvisioningConfig:
|
||||
return MinIOProvisioningConfig(
|
||||
enabled=True,
|
||||
endpoint="minio.local:9000",
|
||||
public_endpoint="minio.local:9000",
|
||||
admin_access_key="admin",
|
||||
admin_secret_key="admin-secret",
|
||||
bucket="beaver-user-files",
|
||||
secure=False,
|
||||
region=None,
|
||||
)
|
||||
|
||||
|
||||
def _settings() -> MinIOSettings:
|
||||
return MinIOSettings(
|
||||
endpoint="minio.local:9000",
|
||||
access_key="beaver-alice",
|
||||
secret_key="alice-secret",
|
||||
bucket="beaver-user-files",
|
||||
namespace="users/alice",
|
||||
)
|
||||
|
||||
|
||||
def _client(tmp_path, monkeypatch) -> TestClient:
|
||||
monkeypatch.setenv("AUTHZ_DATA_DIR", str(tmp_path))
|
||||
monkeypatch.setenv("AUTHZ_PRIVATE_KEY_PATH", str(tmp_path / "signing_key.pem"))
|
||||
monkeypatch.setenv("AUTHZ_INTERNAL_TOKEN", "test-internal-token")
|
||||
monkeypatch.setenv("USER_FILES_MINIO_ENDPOINT", "minio.local:9000")
|
||||
monkeypatch.setenv("USER_FILES_MINIO_ADMIN_ACCESS_KEY", "admin")
|
||||
monkeypatch.setenv("USER_FILES_MINIO_ADMIN_SECRET_KEY", "admin-secret")
|
||||
monkeypatch.setenv("USER_FILES_MINIO_BUCKET", "beaver-user-files")
|
||||
import app.main as main
|
||||
|
||||
main = importlib.reload(main)
|
||||
return TestClient(main.app)
|
||||
|
||||
|
||||
def _register_backend(client: TestClient) -> None:
|
||||
response = client.post(
|
||||
"/backends/register",
|
||||
json={"backend_id": "alice", "name": "Alice", "base_url": "http://alice.local"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_deprovision_removes_namespace_resources_without_secrets(monkeypatch) -> None:
|
||||
_install_fake_minio(monkeypatch)
|
||||
_FakeMinio.objects = [
|
||||
"users/alice/uploads/a.txt",
|
||||
"users/alice/outputs/b.txt",
|
||||
"users/bob/uploads/c.txt",
|
||||
]
|
||||
|
||||
result = deprovision_user_file_minio_resources(
|
||||
backend_id="alice",
|
||||
existing=_settings(),
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["objects"] == {"status": "removed", "deleted": 2}
|
||||
assert _FakeMinio.removed_objects == ["users/alice/uploads/a.txt", "users/alice/outputs/b.txt"]
|
||||
assert ("user_remove", "beaver-alice") in _FakeAdmin.calls
|
||||
assert ("policy_remove", "beaver-user-files-alice") in _FakeAdmin.calls
|
||||
assert "secret" not in str(result).lower()
|
||||
|
||||
|
||||
def test_deprovision_is_idempotent_when_resources_are_absent(monkeypatch) -> None:
|
||||
_install_fake_minio(monkeypatch)
|
||||
_FakeMinio.bucket_exists_value = False
|
||||
_FakeAdmin.missing = True
|
||||
|
||||
result = deprovision_user_file_minio_resources(
|
||||
backend_id="alice",
|
||||
existing=_settings(),
|
||||
config=_config(),
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["bucket"] == "absent"
|
||||
assert result["objects"] == {"status": "absent", "deleted": 0}
|
||||
assert result["user"] == {"status": "absent"}
|
||||
assert result["policy"] == {"status": "absent"}
|
||||
|
||||
|
||||
def test_internal_user_file_deprovision_requires_internal_token(tmp_path, monkeypatch) -> None:
|
||||
with _client(tmp_path, monkeypatch) as client:
|
||||
unauthorized = client.delete("/internal/backends/alice/user-files")
|
||||
|
||||
assert unauthorized.status_code == 401
|
||||
|
||||
|
||||
def test_internal_user_file_deprovision_deletes_settings_without_returning_secret(tmp_path, monkeypatch) -> None:
|
||||
_install_fake_minio(monkeypatch)
|
||||
with _client(tmp_path, monkeypatch) as client:
|
||||
_register_backend(client)
|
||||
client.post(
|
||||
"/backends/alice/settings/minio",
|
||||
json={
|
||||
"endpoint": "minio.local:9000",
|
||||
"access_key": "beaver-alice",
|
||||
"secret_key": "alice-secret",
|
||||
"bucket": "beaver-user-files",
|
||||
"namespace": "users/alice",
|
||||
},
|
||||
)
|
||||
deleted = client.delete(
|
||||
"/internal/backends/alice/user-files",
|
||||
headers={"Authorization": "Bearer test-internal-token"},
|
||||
)
|
||||
after_delete = client.get("/backends/alice/settings/minio")
|
||||
|
||||
assert deleted.status_code == 200
|
||||
payload = deleted.json()
|
||||
assert payload["ok"] is True
|
||||
assert payload["settings"] == {"status": "removed"}
|
||||
assert "secret" not in str(payload).lower()
|
||||
assert after_delete.json() == {"configured": False}
|
||||
Reference in New Issue
Block a user