chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user