Files
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

412 lines
16 KiB
Python

"""Integration tests for multi-user data isolation.
Tests to ensure users can only access their own data and cannot access
other users' data unless explicitly shared.
"""
import os
from pathlib import Path
from typing import Any
import pytest
from fastapi.testclient import TestClient
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api_app import app
from invokeai.app.services.board_records.board_records_common import BoardRecordOrderBy
from invokeai.app.services.invoker import Invoker
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.users.users_common import UserCreateRequest
@pytest.fixture(autouse=True, scope="module")
def client(invokeai_root_dir: Path) -> TestClient:
"""Create a test client for the FastAPI app."""
os.environ["INVOKEAI_ROOT"] = invokeai_root_dir.as_posix()
return TestClient(app)
@pytest.fixture(autouse=True)
def enable_multiuser_for_auth_tests(mock_invoker: Invoker) -> None:
"""Enable multiuser mode for auth tests.
Auth tests need multiuser mode enabled since the login/setup endpoints
return 403 when multiuser is disabled.
"""
mock_invoker.services.configuration.multiuser = True
class MockApiDependencies(ApiDependencies):
"""Mock API dependencies for testing."""
invoker: Invoker
def __init__(self, invoker) -> None:
self.invoker = invoker
def create_user_and_login(
mock_invoker: Invoker, client: TestClient, monkeypatch: Any, email: str, password: str, is_admin: bool = False
) -> tuple[str, str]:
"""Helper to create a user, login, and return (user_id, token)."""
monkeypatch.setattr("invokeai.app.api.routers.auth.ApiDependencies", MockApiDependencies(mock_invoker))
monkeypatch.setattr("invokeai.app.api.auth_dependencies.ApiDependencies", MockApiDependencies(mock_invoker))
user_service = mock_invoker.services.users
user_data = UserCreateRequest(
email=email,
display_name=f"User {email}",
password=password,
is_admin=is_admin,
)
user = user_service.create(user_data)
# Login to get token
response = client.post(
"/api/v1/auth/login",
json={
"email": email,
"password": password,
"remember_me": False,
},
)
assert response.status_code == 200
token = response.json()["token"]
return user.user_id, token
class TestBoardDataIsolation:
"""Tests for board data isolation between users."""
def test_user_can_only_see_own_boards(self, monkeypatch: Any, mock_invoker: Invoker, client: TestClient):
"""Test that users can only see their own boards."""
monkeypatch.setattr("invokeai.app.api.routers.boards.ApiDependencies", MockApiDependencies(mock_invoker))
# Create two users
user1_id, user1_token = create_user_and_login(
mock_invoker, client, monkeypatch, "user1@example.com", "TestPass123"
)
user2_id, user2_token = create_user_and_login(
mock_invoker, client, monkeypatch, "user2@example.com", "TestPass123"
)
# Create board for user1
board_service = mock_invoker.services.boards
user1_board = board_service.create(board_name="User 1 Board", user_id=user1_id)
# Create board for user2
user2_board = board_service.create(board_name="User 2 Board", user_id=user2_id)
# User1 should only see their board
user1_boards = board_service.get_many(
user_id=user1_id,
is_admin=False,
order_by=BoardRecordOrderBy.CreatedAt,
direction=SQLiteDirection.Ascending,
)
user1_board_ids = [b.board_id for b in user1_boards.items]
assert user1_board.board_id in user1_board_ids
assert user2_board.board_id not in user1_board_ids
# User2 should only see their board
user2_boards = board_service.get_many(
user_id=user2_id,
is_admin=False,
order_by=BoardRecordOrderBy.CreatedAt,
direction=SQLiteDirection.Ascending,
)
user2_board_ids = [b.board_id for b in user2_boards.items]
assert user2_board.board_id in user2_board_ids
assert user1_board.board_id not in user2_board_ids
def test_user_cannot_access_other_user_board_directly(self, mock_invoker: Invoker):
"""Test that users cannot access other users' boards by ID."""
board_service = mock_invoker.services.boards
user_service = mock_invoker.services.users
# Create two users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user1 = user_service.create(user1_data)
user2_data = UserCreateRequest(
email="user2@example.com", display_name="User 2", password="TestPass123", is_admin=False
)
user2 = user_service.create(user2_data)
# User1 creates a board
user1_board = board_service.create(board_name="User 1 Private Board", user_id=user1.user_id)
# User2 tries to access user1's board
# The get method should check ownership
try:
retrieved_board = board_service.get(board_id=user1_board.board_id, user_id=user2.user_id)
# If get doesn't check ownership, this test needs to be updated
# or the implementation needs to be fixed
if retrieved_board is not None:
# Board was retrieved - check if it's because of missing authorization check
# This would be a security issue that needs fixing
pytest.fail("User was able to access another user's board without authorization")
except Exception:
# Expected - user2 should not be able to access user1's board
pass
def test_admin_can_see_all_boards(self, mock_invoker: Invoker):
"""Test that admin users can see all boards."""
board_service = mock_invoker.services.boards
user_service = mock_invoker.services.users
# Create admin user
admin_data = UserCreateRequest(
email="admin@example.com", display_name="Admin", password="AdminPass123", is_admin=True
)
admin = user_service.create(admin_data)
# Create regular user
user_data = UserCreateRequest(
email="user@example.com", display_name="User", password="TestPass123", is_admin=False
)
user = user_service.create(user_data)
# User creates a board
board_service.create(board_name="User Board", user_id=user.user_id)
# Admin creates a board
board_service.create(board_name="Admin Board", user_id=admin.user_id)
# Admin should be able to get all boards (implementation dependent)
# Note: Current implementation may not have admin override for board listing
# This test documents expected behavior
class TestImageDataIsolation:
"""Tests for image data isolation between users."""
def test_user_images_isolated_from_other_users(self, mock_invoker: Invoker):
"""Test that users cannot see other users' images."""
user_service = mock_invoker.services.users
# Create two users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user_service.create(user1_data)
user2_data = UserCreateRequest(
email="user2@example.com", display_name="User 2", password="TestPass123", is_admin=False
)
user_service.create(user2_data)
# Note: Image service tests would require actual image creation
# which is beyond the scope of basic security testing
# This test documents expected behavior:
# - Images should have user_id field
# - Image queries should filter by user_id
# - Users should not be able to access images by knowing the image_name
class TestWorkflowDataIsolation:
"""Tests for workflow data isolation between users."""
def test_user_workflows_isolated_from_other_users(self, mock_invoker: Invoker):
"""Test that users cannot see other users' private workflows."""
user_service = mock_invoker.services.users
# Create two users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user_service.create(user1_data)
user2_data = UserCreateRequest(
email="user2@example.com", display_name="User 2", password="TestPass123", is_admin=False
)
user_service.create(user2_data)
# Note: Workflow service tests would require workflow creation
# This test documents expected behavior:
# - Workflows should have user_id and is_public fields
# - Private workflows should only be visible to owner
# - Public workflows should be visible to all users
class TestQueueDataIsolation:
"""Tests for session queue data isolation between users."""
def test_user_queue_items_isolated_from_other_users(self, mock_invoker: Invoker):
"""Test that users cannot see other users' queue items."""
user_service = mock_invoker.services.users
# Create two users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user_service.create(user1_data)
user2_data = UserCreateRequest(
email="user2@example.com", display_name="User 2", password="TestPass123", is_admin=False
)
user_service.create(user2_data)
# Note: Queue service tests would require session creation
# This test documents expected behavior:
# - Queue items should have user_id field
# - Users should only see their own queue items
# - Admin should see all queue items
class TestSharedBoardAccess:
"""Tests for shared board functionality."""
@pytest.mark.skip(reason="Shared board functionality not yet fully implemented")
def test_shared_board_access(self, mock_invoker: Invoker):
"""Test that users can access boards shared with them."""
board_service = mock_invoker.services.boards
user_service = mock_invoker.services.users
# Create two users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user1 = user_service.create(user1_data)
user2_data = UserCreateRequest(
email="user2@example.com", display_name="User 2", password="TestPass123", is_admin=False
)
user_service.create(user2_data)
# User1 creates a board
board_service.create(board_name="Shared Board", user_id=user1.user_id)
# User1 shares the board with user2
# (This functionality is not yet implemented)
# User2 should be able to see the shared board
# Expected behavior documented for future implementation
class TestAdminAuthorization:
"""Tests for admin-only functionality."""
def test_regular_user_cannot_create_admin(self, mock_invoker: Invoker):
"""Test that regular users cannot create admin accounts."""
user_service = mock_invoker.services.users
# Create first admin
admin_data = UserCreateRequest(
email="admin@example.com", display_name="Admin", password="AdminPass123", is_admin=True
)
user_service.create(admin_data)
# Try to create another admin (should fail)
with pytest.raises(ValueError, match="already exists"):
another_admin_data = UserCreateRequest(
email="another@example.com", display_name="Another Admin", password="AdminPass123"
)
user_service.create_admin(another_admin_data)
def test_regular_user_cannot_list_all_users(self, mock_invoker: Invoker):
"""Test that regular users cannot list all users.
Note: This depends on API endpoint implementation.
At the service level, list_users is available to all callers.
Authorization should be enforced at the API level.
"""
user_service = mock_invoker.services.users
# Create users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user_service.create(user1_data)
# Service level does not enforce authorization
# API level should check if caller is admin before allowing user listing
user_service.list_users()
# This will succeed at service level - API must enforce auth
class TestDataIntegrity:
"""Tests for data integrity in multi-user scenarios."""
def test_user_deletion_cascades_to_owned_data(self, mock_invoker: Invoker):
"""Test that deleting a user also deletes their owned data."""
user_service = mock_invoker.services.users
board_service = mock_invoker.services.boards
# Create user
user_data = UserCreateRequest(
email="deleteme@example.com", display_name="Delete Me", password="TestPass123", is_admin=False
)
user = user_service.create(user_data)
# User creates a board
board = board_service.create(board_name="My Board", user_id=user.user_id)
# Delete user
user_service.delete(user.user_id)
# Board should be deleted too (CASCADE in database)
# Note: get_dto doesn't take user_id parameter, it gets the board by ID only
# We'll check that it raises an exception or returns None after cascade delete
try:
board_service.get_dto(board_id=board.board_id)
# If we get here, the board wasn't deleted - this is a failure
raise AssertionError("Board should have been deleted by CASCADE")
except Exception:
# Expected - board was deleted by CASCADE
pass
def test_concurrent_user_operations_maintain_isolation(self, mock_invoker: Invoker):
"""Test that concurrent operations from different users maintain data isolation.
This is a basic test - comprehensive concurrency testing would require
multiple threads/processes and more complex scenarios.
"""
user_service = mock_invoker.services.users
board_service = mock_invoker.services.boards
# Create two users
user1_data = UserCreateRequest(
email="user1@example.com", display_name="User 1", password="TestPass123", is_admin=False
)
user1 = user_service.create(user1_data)
user2_data = UserCreateRequest(
email="user2@example.com", display_name="User 2", password="TestPass123", is_admin=False
)
user2 = user_service.create(user2_data)
# Both users create boards
user1_board = board_service.create(board_name="User 1 Board", user_id=user1.user_id)
user2_board = board_service.create(board_name="User 2 Board", user_id=user2.user_id)
# Verify isolation is maintained
user1_boards = board_service.get_many(
user_id=user1.user_id,
is_admin=False,
order_by=BoardRecordOrderBy.CreatedAt,
direction=SQLiteDirection.Ascending,
)
user2_boards = board_service.get_many(
user_id=user2.user_id,
is_admin=False,
order_by=BoardRecordOrderBy.CreatedAt,
direction=SQLiteDirection.Ascending,
)
user1_board_ids = [b.board_id for b in user1_boards.items]
user2_board_ids = [b.board_id for b in user2_boards.items]
# Each user should only see their own board
assert user1_board.board_id in user1_board_ids
assert user2_board.board_id not in user1_board_ids
assert user2_board.board_id in user2_board_ids
assert user1_board.board_id not in user2_board_ids