The policy builder only offered "Matches everything" (the `true` scope node), which also targets servers. Authoring an estate-wide policy almost always means "every PC an operator sits at", and a reboot or script push against a server is a materially different event. Adds an `allpcs` scope node that matches every endpoint except those whose computer type is server-designated. Server designation reads the computer type's ICON slug, which is nominally a UI hint and is now load-bearing; it was chosen over matching the type NAME (a site can rename "Server") and over a new isserver column (needs a migration). It is isolated behind SERVER_ICON so promoting it to a real flag later is a one-place change. The `true` node is retained and still matches servers, so existing policies keep their exact meaning rather than silently re-scoping. It stays visible in the dropdown only when editing a policy that already uses it; new policies default to `allpcs`. Covered by tests/test_plugins/test_computers_policy_allpcs.py, verified against a planted canary so the server-exclusion assertions are known to bite.
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
"""Scope evaluation for the All PCs policy target.
|
|
|
|
These are pure unit tests: policyscope has no db or Flask dependency, so the
|
|
whole grammar can be exercised with plain dicts. build_endpoint_context needs
|
|
only duck-typed stand-ins for a Computer row.
|
|
"""
|
|
import pytest
|
|
|
|
from plugins.computers.services.policyresolver import build_endpoint_context
|
|
from plugins.computers.services.policyscope import (
|
|
ScopeError,
|
|
evaluate_scope,
|
|
validate_scope,
|
|
)
|
|
|
|
|
|
class FakeComputerType:
|
|
def __init__(self, icon):
|
|
self.icon = icon
|
|
|
|
|
|
class FakeComputer:
|
|
"""Minimal stand-in for a Computer row, no db required."""
|
|
|
|
def __init__(self, computertypeid=None, icon=None, installedapps=()):
|
|
self.computertypeid = computertypeid
|
|
self.computertype = FakeComputerType(icon) if icon is not None else None
|
|
self.installedapps = installedapps
|
|
|
|
|
|
def test_allpcs_is_a_valid_scope_node():
|
|
validate_scope({'type': 'allpcs'})
|
|
|
|
|
|
def test_allpcs_matches_an_ordinary_pc():
|
|
context = {'computertypeid': 1, 'appids': set(), 'isserver': False}
|
|
assert evaluate_scope({'type': 'allpcs'}, context) is True
|
|
|
|
|
|
def test_allpcs_does_not_match_a_server():
|
|
"""The whole point of the option: servers are excluded."""
|
|
context = {'computertypeid': 4, 'appids': set(), 'isserver': True}
|
|
assert evaluate_scope({'type': 'allpcs'}, context) is False
|
|
|
|
|
|
def test_allpcs_matches_an_endpoint_with_no_type_designation():
|
|
"""An untyped endpoint is an ordinary PC until something says otherwise."""
|
|
context = {'computertypeid': None, 'appids': set()}
|
|
assert evaluate_scope({'type': 'allpcs'}, context) is True
|
|
|
|
|
|
def test_true_still_matches_a_server():
|
|
"""'true' keeps its old meaning so existing policies do not silently shift."""
|
|
context = {'computertypeid': 4, 'appids': set(), 'isserver': True}
|
|
assert evaluate_scope({'type': 'true'}, context) is True
|
|
|
|
|
|
def test_allpcs_composes_with_other_nodes():
|
|
"""AND of allpcs and an app still excludes a server running that app."""
|
|
scope = {
|
|
'type': 'and',
|
|
'nodes': [{'type': 'allpcs'}, {'type': 'app', 'id': 7}],
|
|
}
|
|
pc = {'computertypeid': 1, 'appids': {7}, 'isserver': False}
|
|
server = {'computertypeid': 4, 'appids': {7}, 'isserver': True}
|
|
assert evaluate_scope(scope, pc) is True
|
|
assert evaluate_scope(scope, server) is False
|
|
|
|
|
|
def test_context_marks_a_server_icon_type_as_a_server():
|
|
computer = FakeComputer(computertypeid=4, icon='server')
|
|
assert build_endpoint_context(computer)['isserver'] is True
|
|
|
|
|
|
def test_context_does_not_mark_a_desktop_type_as_a_server():
|
|
computer = FakeComputer(computertypeid=1, icon='desktop')
|
|
assert build_endpoint_context(computer)['isserver'] is False
|
|
|
|
|
|
def test_context_handles_a_computer_with_no_type():
|
|
computer = FakeComputer()
|
|
assert build_endpoint_context(computer)['isserver'] is False
|
|
|
|
|
|
def test_an_unknown_node_type_is_still_rejected():
|
|
with pytest.raises(ScopeError):
|
|
validate_scope({'type': 'allpc'})
|