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.
77 lines
3.0 KiB
Python
77 lines
3.0 KiB
Python
"""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"
|