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
412 lines
16 KiB
Python
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
|