198 lines
6.4 KiB
Python
198 lines
6.4 KiB
Python
"""Unit tests for control-plane space and grant use cases."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from ragcore.application.control import (
|
|
EffectiveSpaceResolver,
|
|
GrantService,
|
|
KnowledgeSpaceLifecycle,
|
|
)
|
|
from ragcore.domain.control import (
|
|
DataPolicy,
|
|
ProviderKind,
|
|
Role,
|
|
SpaceOperation,
|
|
Workspace,
|
|
)
|
|
|
|
|
|
def setup_control_plane() -> tuple[Workspace, object, object, object]:
|
|
workspace = Workspace(uuid4(), "Platform", "platform")
|
|
application = workspace.create_application(
|
|
id=uuid4(),
|
|
name="Client",
|
|
scopes=(),
|
|
default_knowledge_space_ids=frozenset(),
|
|
)
|
|
private = workspace.create_knowledge_space(
|
|
id=uuid4(), name="Private", slug="private", data_policy=DataPolicy.LOCAL_ONLY
|
|
)
|
|
shared = workspace.create_knowledge_space(
|
|
id=uuid4(), name="Shared", slug="shared", data_policy=DataPolicy.LOCAL_PREFERRED
|
|
)
|
|
return workspace, application, private, shared
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("policy", "provider", "operation", "allowed"),
|
|
[
|
|
(DataPolicy.LOCAL_ONLY, ProviderKind.LOCAL, SpaceOperation.ANSWER, True),
|
|
(DataPolicy.LOCAL_ONLY, ProviderKind.CLOUD, SpaceOperation.SEARCH, False),
|
|
(DataPolicy.LOCAL_PREFERRED, ProviderKind.CLOUD, SpaceOperation.ANSWER, True),
|
|
(DataPolicy.RETRIEVAL_ONLY, ProviderKind.LOCAL, SpaceOperation.SEARCH, True),
|
|
(DataPolicy.NO_GENERATION, ProviderKind.CLOUD, SpaceOperation.CONTEXT, True),
|
|
(DataPolicy.NO_GENERATION, ProviderKind.LOCAL, SpaceOperation.ANSWER, False),
|
|
],
|
|
)
|
|
def test_data_policy_excludes_incompatible_provider_operations(
|
|
policy: DataPolicy, provider: ProviderKind, operation: SpaceOperation, allowed: bool
|
|
) -> None:
|
|
workspace = Workspace(uuid4(), "Platform", "platform")
|
|
space = workspace.create_knowledge_space(
|
|
id=uuid4(), name="Policy", slug="policy", data_policy=policy
|
|
)
|
|
|
|
assert space.allows(operation=operation, provider=provider) is allowed
|
|
|
|
|
|
def test_effective_spaces_are_requested_grants_and_policy_intersection() -> None:
|
|
workspace, application, private, shared = setup_control_plane()
|
|
user_id = uuid4()
|
|
grants = GrantService()
|
|
app_grant = grants.application_grant(
|
|
workspace, id=uuid4(), application=application, space=private, role=Role.VIEWER
|
|
)
|
|
user_grant = grants.user_grant(
|
|
workspace, id=uuid4(), user_id=user_id, space=shared, role=Role.VIEWER
|
|
)
|
|
|
|
effective = EffectiveSpaceResolver().resolve(
|
|
workspace_id=workspace.id,
|
|
spaces=(private, shared),
|
|
grants=(app_grant,),
|
|
user_grants=(user_grant,),
|
|
application=application,
|
|
user_id=user_id,
|
|
requested_space_ids=(private.id, shared.id),
|
|
)
|
|
|
|
assert effective == frozenset({private.id, shared.id})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("role", "operation"),
|
|
[
|
|
(Role.VIEWER, SpaceOperation.CONTEXT),
|
|
(Role.VIEWER, SpaceOperation.ANSWER),
|
|
(Role.AUDITOR, SpaceOperation.SEARCH),
|
|
],
|
|
)
|
|
def test_grant_role_limits_effective_space_operations(
|
|
role: Role, operation: SpaceOperation
|
|
) -> None:
|
|
workspace, application, private, _ = setup_control_plane()
|
|
grant = GrantService().application_grant(
|
|
workspace, id=uuid4(), application=application, space=private, role=role
|
|
)
|
|
|
|
effective = EffectiveSpaceResolver().resolve(
|
|
workspace_id=workspace.id,
|
|
spaces=(private,),
|
|
grants=(grant,),
|
|
application=application,
|
|
requested_space_ids=(private.id,),
|
|
operation=operation,
|
|
)
|
|
|
|
assert effective == frozenset()
|
|
def test_default_spaces_never_expand_grants() -> None:
|
|
workspace, application, private, shared = setup_control_plane()
|
|
application = workspace.create_application(
|
|
id=application.id,
|
|
name=application.name,
|
|
scopes=(),
|
|
default_knowledge_space_ids=frozenset({private.id, shared.id}),
|
|
)
|
|
grant = GrantService().application_grant(
|
|
workspace, id=uuid4(), application=application, space=private, role=Role.VIEWER
|
|
)
|
|
|
|
effective = EffectiveSpaceResolver().resolve(
|
|
workspace_id=workspace.id,
|
|
spaces=(private, shared),
|
|
grants=(grant,),
|
|
application=application,
|
|
)
|
|
|
|
assert effective == frozenset({private.id})
|
|
|
|
|
|
def test_inactive_spaces_and_grants_are_excluded() -> None:
|
|
workspace, application, private, shared = setup_control_plane()
|
|
inactive_space = workspace.create_knowledge_space(
|
|
id=uuid4(), name="Inactive", slug="inactive", active=False
|
|
)
|
|
grants = GrantService()
|
|
private_grant = grants.application_grant(
|
|
workspace,
|
|
id=uuid4(),
|
|
application=application,
|
|
space=private,
|
|
role=Role.VIEWER,
|
|
active=False,
|
|
)
|
|
inactive_grant = grants.application_grant(
|
|
workspace, id=uuid4(), application=application, space=inactive_space, role=Role.VIEWER
|
|
)
|
|
|
|
effective = EffectiveSpaceResolver().resolve(
|
|
workspace_id=workspace.id,
|
|
spaces=(private, shared, inactive_space),
|
|
grants=(private_grant, inactive_grant),
|
|
application=application,
|
|
requested_space_ids=(private.id, inactive_space.id),
|
|
)
|
|
|
|
assert effective == frozenset()
|
|
|
|
|
|
def test_cross_workspace_spaces_and_grants_are_excluded() -> None:
|
|
workspace, application, private, _ = setup_control_plane()
|
|
other = Workspace(uuid4(), "Other", "other")
|
|
other_space = other.create_knowledge_space(id=uuid4(), name="Other", slug="other")
|
|
cross_grant = other.grant_user(
|
|
id=uuid4(), user_id=uuid4(), knowledge_space=other_space, role=Role.VIEWER
|
|
)
|
|
|
|
effective = EffectiveSpaceResolver().resolve(
|
|
workspace_id=workspace.id,
|
|
spaces=(private, other_space),
|
|
user_grants=(cross_grant,),
|
|
application=application,
|
|
user_id=cross_grant.user_id,
|
|
requested_space_ids=(private.id, other_space.id),
|
|
)
|
|
|
|
assert effective == frozenset()
|
|
|
|
|
|
def test_lifecycle_transitions_preserve_identity_without_mutating_source() -> None:
|
|
_, _, private, _ = setup_control_plane()
|
|
lifecycle = KnowledgeSpaceLifecycle()
|
|
|
|
inactive = lifecycle.deactivate(private)
|
|
cloud = lifecycle.set_data_policy(inactive, DataPolicy.CLOUD_ALLOWED)
|
|
active = lifecycle.activate(cloud)
|
|
|
|
assert private.active is True
|
|
assert inactive.active is False
|
|
assert cloud.data_policy is DataPolicy.CLOUD_ALLOWED
|
|
assert active.active is True
|
|
assert active.id == private.id
|
|
|
|
|