r"""Cross-platform best-effort file-permission hardening for auth state.Both the FNMT-certificate-backed authenticator and the Cl@ve Móvilprovider persist session-state JSON containing bearer-equivalentmaterial (storage-state cookies, OAuth tokens, refresh tokens). Thefiles must be restricted to the operator's user account.POSIX: ``chmod 0o600`` is sufficient. Windows: ``icacls.exe/inheritance:r /grant:r <user>:(F)`` strips inherited ACLs and grantsfull control to the operator only. The ``icacls`` call is best-effortand tries both ``DOMAIN\\user`` and ``user`` candidate names so it workson standalone machines and domain-joined hosts.The helper is shared between:mod:`~adapters.outbound.aeat.auth._authenticator` and:mod:`~adapters.outbound.aeat.auth._clave_movil` so the Windows-ACLdiscipline cannot diverge between the two session writers.This module is a compatibility/public hardening primitive for plaintext files;the active AEAT browser-session persistence backend stores session state throughsecure objects. The Windows branch reads ``SYSTEMROOT`` and ``USERDOMAIN`` as OSambient context only. It does not read AEAT-prefixed configuration or makepermission tightening an authorization decision.The public :func:`restrict_file_permissions` entry point accepts a:class:`~pathlib.Path` target and deliberately returns ``None`` even when thebest-effort hardening step cannot be applied.See Also: :func:`~core.file_permissions.restrict_file_permissions` Public entry point used by legacy/plaintext session writers. :mod:`~adapters.outbound.aeat.auth._authenticator` FNMT-backed browser-session writer that shares this hardening helper. :mod:`~adapters.outbound.aeat.auth._clave_movil` Cl@ve Móvil provider whose active session persistence now uses secure objects instead of plaintext storage-state files. ``2026-06-05-secure-storage-production-hardening-w12-p26-s297-review-audit`` Review that fixed silent POSIX failure swallowing and bounded ACL calls."""from__future__importannotationsimportgetpassimportosimportsubprocessfromcollections.abcimportSequencefrompathlibimportPathfromtypingimportFinalfrom.loggingimportget_logger_log=get_logger(__name__)# Windows environment variable names used to locate icacls.exe and the# operator's domain-qualified username. Named constants so grep surfaces# every usage site rather than having bare strings spread across the code._SYSTEMROOT_ENV_VAR:Final[str]="SYSTEMROOT"_USERDOMAIN_ENV_VAR:Final[str]="USERDOMAIN"_ICACLS_TIMEOUT_SECONDS:Final[float]=10.0def_run_permission_command(args:Sequence[str],*,timeout:float=_ICACLS_TIMEOUT_SECONDS,)->subprocess.CompletedProcess[str]:returnsubprocess.run(list(args),capture_output=True,text=True,check=False,timeout=timeout,)def_restrict_posix_file_permissions(path:Path)->None:try:os.chmod(path,0o600)exceptOSError:_log.debug("restrict_file_permissions: chmod failed on %s",path,exc_info=True)
[docs]defrestrict_file_permissions(path:Path)->None:r"""Best-effort restrict ``path`` to the operator's user account. POSIX: calls :func:`~os.chmod` with mode ``0o600``. Windows: shells out to ``icacls.exe`` to strip inherited ACLs and grant ``F`` (Full control) to the operator's account only. Tries both ``DOMAIN\\user`` and bare ``user`` candidates because standalone machines have no ``USERDOMAIN`` and domain-joined machines may need the qualified form. The subprocess is time-bounded so a wedged ``icacls.exe`` cannot block the auth-state writer indefinitely. Logs at ``WARNING`` when every candidate fails — the file stays on disk with whatever ACL it inherited from its parent directory. Best-effort: every error is swallowed so the auth flow never aborts on a hardening side-effect. Worst case is a slightly more permissive ACL than intended, surfaced via the warning log. Callers that need a confidentiality boundary should use the secure-object storage path rather than relying on this post-write permission adjustment. Args: path: Path to the file whose permissions must be tightened. """ifos.name=="nt":# pragma: no cover - Windows-specific# Wrap every Windows-only call in a single try/except so the# docstring's "best-effort, never raises" contract holds even# when icacls.exe is missing from PATH (FileNotFoundError),# getpass.getuser() raises (no operator name available), or# the subprocess.run hits an unexpected OSError.try:username=getpass.getuser()# os.environ.get allowlist: SYSTEMROOT / USERDOMAIN are Windows OS-integration# variables, not AEAT-prefixed config. There is no Settings field for them# because they are OS-provided ambient context, not application configuration.# The single-surface invariant test scanner only flags AEAT_* keys; these are# intentionally read directly from the OS environment here.icacls_path=Path(os.environ.get(_SYSTEMROOT_ENV_VAR,r"C:\\Windows"))/"System32"/"icacls.exe"candidates=[username]userdomain=os.environ.get(_USERDOMAIN_ENV_VAR)ifuserdomain:candidates.insert(0,f"{userdomain}\\{username}")result:subprocess.CompletedProcess[str]|None=Noneforcandidateincandidates:result=_run_permission_command([str(icacls_path),str(path),"/inheritance:r","/grant:r",f"{candidate}:(F)",])ifresult.returncode==0:return_log.warning("restrict_file_permissions: failed to harden Windows ACLs on %s: %s",path,result.stderr.strip()ifresultisnotNoneandresult.stderrelse"icacls returned non-zero",)exceptException:# Catch Exception (not just OSError) so the docstring's# "every error is swallowed" contract truly holds. The# candidates that have actually been observed are# ``OSError`` (icacls.exe missing → FileNotFoundError;# subprocess.run / icacls write to a path the operator# cannot ACL), but a future ``getpass`` / ``subprocess``# internal change could surface other exception types# here, and the auth flow must NOT abort because of a# best-effort hardening side-effect. ``KeyError`` is# not in the catch list because ``os.environ.get``# returns ``None`` rather than raising on a missing key._log.warning("restrict_file_permissions: best-effort hardening failed on %s",path,exc_info=True,)returnifos.name!="posix":return_restrict_posix_file_permissions(path)