test(desktop): assert macOS device entitlements are inherited
Pin #37718: the inherit plist must grant audio-input, every device.* entitlement on the main app must also be inherited by the Helper/Setup processes, and both entitlement files must stay valid plists.
This commit is contained in:
76
tests/test_desktop_mac_entitlements.py
Normal file
76
tests/test_desktop_mac_entitlements.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Regression for #37718: macOS microphone entitlement must be inherited.
|
||||
|
||||
Hermes Desktop signs with ``hardenedRuntime: true`` and points electron-builder
|
||||
at two entitlement files (see ``apps/desktop/package.json``):
|
||||
|
||||
* ``entitlements`` → ``electron/entitlements.mac.plist`` (the main app), and
|
||||
* ``entitlementsInherit`` → ``electron/entitlements.mac.inherit.plist`` (the
|
||||
Electron Helper / Setup processes).
|
||||
|
||||
Under the hardened runtime, the process that actually opens the microphone is a
|
||||
Helper, which inherits the *inherit* plist. ``com.apple.security.device.audio-input``
|
||||
lived only in the main plist, so macOS' TCC layer refused the microphone with::
|
||||
|
||||
Prompting policy for hardened runtime; service: kTCCServiceMicrophone
|
||||
requires entitlement com.apple.security.device.audio-input but it is missing
|
||||
|
||||
and never showed the permission prompt. These tests pin that every device
|
||||
entitlement granted to the main app is also granted to the inherited helpers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import plistlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
ELECTRON_DIR = REPO_ROOT / "apps" / "desktop" / "electron"
|
||||
MAIN_PLIST = ELECTRON_DIR / "entitlements.mac.plist"
|
||||
INHERIT_PLIST = ELECTRON_DIR / "entitlements.mac.inherit.plist"
|
||||
|
||||
DEVICE_PREFIX = "com.apple.security.device."
|
||||
|
||||
|
||||
def _load(plist: Path) -> dict:
|
||||
assert plist.is_file(), f"missing entitlements file: {plist}"
|
||||
with plist.open("rb") as fh:
|
||||
return plistlib.load(fh)
|
||||
|
||||
|
||||
def test_inherit_plist_grants_microphone() -> None:
|
||||
"""The helper-inherited plist must grant audio-input (regression #37718)."""
|
||||
inherit = _load(INHERIT_PLIST)
|
||||
assert inherit.get("com.apple.security.device.audio-input") is True, (
|
||||
"entitlements.mac.inherit.plist must grant "
|
||||
"`com.apple.security.device.audio-input`; without it the hardened-runtime "
|
||||
"Helper process is denied the microphone and no TCC prompt appears (#37718)."
|
||||
)
|
||||
|
||||
|
||||
def test_device_entitlements_are_inherited() -> None:
|
||||
"""Every device.* entitlement on the main app must also be inherited."""
|
||||
main = _load(MAIN_PLIST)
|
||||
inherit = _load(INHERIT_PLIST)
|
||||
|
||||
main_device = {
|
||||
key: val
|
||||
for key, val in main.items()
|
||||
if key.startswith(DEVICE_PREFIX) and val is True
|
||||
}
|
||||
missing = [key for key in main_device if inherit.get(key) is not True]
|
||||
assert not missing, (
|
||||
"Device entitlements present in entitlements.mac.plist but missing from "
|
||||
f"entitlements.mac.inherit.plist: {missing}. Helper/Setup processes inherit "
|
||||
"the latter under hardenedRuntime, so any device access the app needs must "
|
||||
"be listed in both (#37718)."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("plist", [MAIN_PLIST, INHERIT_PLIST])
|
||||
def test_entitlement_files_are_valid_plists(plist: Path) -> None:
|
||||
"""Both entitlement files must remain well-formed plist dictionaries."""
|
||||
data = _load(plist)
|
||||
assert isinstance(data, dict) and data, f"{plist.name} should be a non-empty dict"
|
||||
Reference in New Issue
Block a user