arbiter-server 0.9.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ services:
2
+ arbiter:
3
+ image: ${ARBITER_IMAGE:-python:3.11-slim}
4
+ container_name: ${ARBITER_CONTAINER_NAME:-arbiter-staging}
5
+ user: ${ARBITER_CONTAINER_USER:-10001:10001}
6
+ restart: ${ARBITER_RESTART:-unless-stopped}
7
+ env_file:
8
+ - ${ARBITER_APP_ENV_FILE:-./conf/.env}
9
+ environment:
10
+ # Bind to all container interfaces so Docker's host-loopback port publish can
11
+ # reach the service. The service stays host-local because ports publish
12
+ # only on 127.0.0.1.
13
+ ARBITER_SERVER_HOST: 0.0.0.0
14
+ ARBITER_HOST_BIND: ${ARBITER_HOST_BIND:-127.0.0.1}
15
+ ARBITER_HOST_PORT: ${ARBITER_HOST_PORT:-18025}
16
+ ARBITER_CONTAINER_PORT: ${ARBITER_CONTAINER_PORT:-8025}
17
+ ARBITER_PUBLIC_SCHEME: ${ARBITER_PUBLIC_SCHEME:-http}
18
+ ARBITER_PUBLIC_BASE_URL: ${ARBITER_PUBLIC_BASE_URL:-}
19
+ ARBITER_CONFIG_NAME: ${ARBITER_CONFIG_NAME:-arbiter-server}
20
+ ARBITER_RUNTIME_VENV: ${ARBITER_RUNTIME_VENV:-/tmp/arbiter-venv}
21
+ HOME: ${ARBITER_CONTAINER_HOME:-/tmp/arbiter-home}
22
+ volumes:
23
+ - ${ARBITER_CONFIG_DIR:-./conf}:/config:ro
24
+ - ${ARBITER_REQUIREMENTS_FILE:-./requirements.txt}:/requirements.txt:ro
25
+ - ${ARBITER_WHEELS_DIR:-./wheels}:/wheels:ro
26
+ - ${ARBITER_PLUGIN_DATA_DIR:-./data/plugins}:/data/plugins
27
+ extra_hosts:
28
+ - "host.docker.internal:host-gateway"
29
+ ports:
30
+ - "${ARBITER_HOST_BIND:-127.0.0.1}:${ARBITER_HOST_PORT:-18025}:${ARBITER_CONTAINER_PORT:-8025}"
31
+ networks:
32
+ - arbiter
33
+ command: >
34
+ sh -lc
35
+ 'runtime_venv="$${ARBITER_RUNTIME_VENV:-/tmp/arbiter-venv}" &&
36
+ case "$$runtime_venv" in /tmp/arbiter-*) ;; *) echo "error: ARBITER_RUNTIME_VENV must be an Arbiter path under /tmp" >&2; exit 1;; esac &&
37
+ case "$$runtime_venv" in *..*) echo "error: ARBITER_RUNTIME_VENV must not contain .." >&2; exit 1;; esac &&
38
+ case "$$HOME" in /tmp/arbiter-*) ;; *) echo "error: HOME must be an Arbiter path under /tmp" >&2; exit 1;; esac &&
39
+ case "$$HOME" in *..*) echo "error: HOME must not contain .." >&2; exit 1;; esac &&
40
+ rm -rf "$$runtime_venv" &&
41
+ mkdir -p "$$HOME" &&
42
+ python -m venv "$$runtime_venv" &&
43
+ venv_python="$$runtime_venv/bin/python" &&
44
+ if grep -Eq "^[[:space:]]*/source/arbiter(/|$)" /requirements.txt; then
45
+ cp /requirements.txt /tmp/requirements.txt &&
46
+ awk "!/^[[:space:]]*(#|$)/ && !/^[[:space:]]*\\/source\\/arbiter(\\/|$)/ { print }" /tmp/requirements.txt > /tmp/requirements.pinned &&
47
+ awk "/^[[:space:]]*\\/source\\/arbiter(\\/|$)/ { sub(/^[[:space:]]*/, \"\"); sub(/[[:space:]]*$/, \"\"); print }" /tmp/requirements.txt > /tmp/requirements.editable &&
48
+ if [ -s /tmp/requirements.pinned ]; then
49
+ if [ -d /wheels ] && find /wheels -maxdepth 1 -name "*.whl" -print -quit | grep -q .; then
50
+ "$$venv_python" -m pip --disable-pip-version-check install --no-cache-dir --no-index --find-links /wheels -r /tmp/requirements.pinned;
51
+ else
52
+ "$$venv_python" -m pip --disable-pip-version-check install --no-cache-dir -r /tmp/requirements.pinned;
53
+ fi;
54
+ fi &&
55
+ rm -rf /tmp/arbiter-source &&
56
+ mkdir -p /tmp/arbiter-source &&
57
+ while IFS= read -r requirement; do
58
+ source_suffix="$${requirement#/source/arbiter}" &&
59
+ local_requirement="/tmp/arbiter-source$$source_suffix" &&
60
+ if [ -z "$$source_suffix" ]; then
61
+ cp -a /source/arbiter/. /tmp/arbiter-source || exit 1;
62
+ else
63
+ mkdir -p "$${local_requirement%/*}" &&
64
+ cp -a "$$requirement" "$$local_requirement" || exit 1;
65
+ fi;
66
+ done < /tmp/requirements.editable &&
67
+ find /tmp/arbiter-source -type d \( -name "*.egg-info" -o -name "__pycache__" \) -prune -exec rm -rf {} + &&
68
+ while IFS= read -r requirement; do
69
+ source_suffix="$${requirement#/source/arbiter}" &&
70
+ local_requirement="/tmp/arbiter-source$$source_suffix" &&
71
+ if [ -d /wheels ] && find /wheels -maxdepth 1 -name "*.whl" -print -quit | grep -q .; then
72
+ "$$venv_python" -m pip --disable-pip-version-check install --no-cache-dir --find-links /wheels -e "$$local_requirement" || exit 1;
73
+ else
74
+ "$$venv_python" -m pip --disable-pip-version-check install --no-cache-dir -e "$$local_requirement" || exit 1;
75
+ fi;
76
+ done < /tmp/requirements.editable;
77
+ elif [ -d /wheels ] && find /wheels -maxdepth 1 -name "*.whl" -print -quit | grep -q .; then
78
+ "$$venv_python" -m pip --disable-pip-version-check install --no-cache-dir --no-index --find-links /wheels -r /requirements.txt;
79
+ else
80
+ "$$venv_python" -m pip --disable-pip-version-check install --no-cache-dir -r /requirements.txt;
81
+ fi &&
82
+ public_host="$${ARBITER_HOST_BIND:-127.0.0.1}" &&
83
+ case "$$public_host" in 0.0.0.0) public_host=127.0.0.1;; *:*) public_host="[$$public_host]";; esac &&
84
+ set -- "arbiter.server.bind.host=$$ARBITER_SERVER_HOST" "arbiter.server.bind.port=$$ARBITER_CONTAINER_PORT" "arbiter.server.public.scheme=$${ARBITER_PUBLIC_SCHEME:-http}" "arbiter.server.public.host=$$public_host" "arbiter.server.public.port=$${ARBITER_HOST_PORT:-18025}" "arbiter.storage.plugin_data_dir=/data/plugins" "arbiter.deployment_scope=staged" &&
85
+ if [ -n "$${ARBITER_PUBLIC_BASE_URL:-}" ]; then set -- "$$@" "arbiter.server.public.base_url=$$ARBITER_PUBLIC_BASE_URL"; fi &&
86
+ if ! "$$runtime_venv/bin/arbiter-server" --config-dir /config --config-name "$$ARBITER_CONFIG_NAME" config check "$$@"; then
87
+ echo "fatal configuration error; stopping container without Docker restart" >&2;
88
+ exit 0;
89
+ fi &&
90
+ exec "$$runtime_venv/bin/arbiter-server" --config-dir /config --config-name "$$ARBITER_CONFIG_NAME" serve "$$@"'
91
+
92
+ networks:
93
+ arbiter:
94
+ name: ${ARBITER_DOCKER_NETWORK_NAME:-arbiter-staging}
95
+ driver: bridge
96
+ driver_opts:
97
+ com.docker.network.bridge.name: "${ARBITER_DOCKER_BRIDGE_NAME:-arbiter-stg0}"
98
+ ipam:
99
+ config:
100
+ # Override only if the default subnet conflicts with another host network.
101
+ - subnet: "${ARBITER_DOCKER_SUBNET:-172.31.251.0/24}"
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def ensure_runtime_config_permissions(
8
+ *,
9
+ config_dir: Path,
10
+ env_file: Path | None,
11
+ ) -> None:
12
+ if os.name == "nt":
13
+ from .windows import ensure_runtime_config_permissions as ensure_windows
14
+
15
+ ensure_windows(config_dir=config_dir, env_file=env_file)
16
+ return
17
+
18
+ from .posix import ensure_runtime_config_permissions as ensure_posix
19
+
20
+ ensure_posix(config_dir=config_dir, env_file=env_file)
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import stat
4
+ from pathlib import Path
5
+
6
+
7
+ def _other_read_write_bits(mode: int) -> int:
8
+ return mode & (stat.S_IROTH | stat.S_IWOTH)
9
+
10
+
11
+ def _group_or_other_write_bits(mode: int) -> int:
12
+ return mode & (stat.S_IWGRP | stat.S_IWOTH)
13
+
14
+
15
+ def _group_or_other_read_write_bits(mode: int) -> int:
16
+ return mode & (stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)
17
+
18
+
19
+ def _directory_group_or_other_write_execute_bits(mode: int) -> int:
20
+ group_bits = mode & stat.S_IWGRP if mode & stat.S_IXGRP else 0
21
+ other_bits = mode & stat.S_IWOTH if mode & stat.S_IXOTH else 0
22
+ return group_bits | other_bits
23
+
24
+
25
+ def ensure_runtime_config_permissions(
26
+ *,
27
+ config_dir: Path,
28
+ env_file: Path | None,
29
+ ) -> None:
30
+ for directory in sorted(
31
+ {config_dir, *(path.parent for path in config_dir.rglob("*.yaml"))}
32
+ ):
33
+ if not directory.is_dir():
34
+ continue
35
+ if _directory_group_or_other_write_execute_bits(directory.stat().st_mode):
36
+ raise ValueError(
37
+ "unsafe config directory permissions: "
38
+ f"{directory} must not be writable by group or others; "
39
+ f"run `chmod go-w {directory}`"
40
+ )
41
+
42
+ for config_file in sorted(config_dir.rglob("*.yaml")):
43
+ if not config_file.is_file():
44
+ continue
45
+ if _other_read_write_bits(
46
+ config_file.stat().st_mode
47
+ ) or _group_or_other_write_bits(config_file.stat().st_mode):
48
+ raise ValueError(
49
+ "unsafe config file permissions: "
50
+ f"{config_file} must not be writable by group or others, "
51
+ "or readable by others; "
52
+ f"run `chmod g-w,o-rw {config_file}`"
53
+ )
54
+
55
+ if env_file is None or not env_file.exists():
56
+ return
57
+ if env_file.parent.exists() and _directory_group_or_other_write_execute_bits(
58
+ env_file.parent.stat().st_mode
59
+ ):
60
+ raise ValueError(
61
+ "unsafe app env directory permissions: "
62
+ f"{env_file.parent} must not be writable by group or others; "
63
+ f"run `chmod go-w {env_file.parent}`"
64
+ )
65
+ if _group_or_other_read_write_bits(env_file.stat().st_mode):
66
+ raise ValueError(
67
+ "unsafe app env file permissions: "
68
+ f"{env_file} must not be readable or writable by group or others; "
69
+ f"run `chmod 600 {env_file}`"
70
+ )
@@ -0,0 +1,379 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+
10
+ _WINDOWS_PRINCIPAL_NAMES = {
11
+ "S-1-1-0": "Everyone",
12
+ "S-1-3-4": "Owner Rights",
13
+ "S-1-5-4": "Interactive Users",
14
+ "S-1-5-11": "Authenticated Users",
15
+ "S-1-5-18": "LocalSystem",
16
+ "S-1-5-32-544": "Builtin Administrators",
17
+ "S-1-5-32-545": "Builtin Users",
18
+ "S-1-5-32-546": "Builtin Guests",
19
+ }
20
+ _WINDOWS_ALLOWED_RUNTIME_PERMISSION_SIDS = {
21
+ "S-1-5-18", # LocalSystem
22
+ "S-1-5-32-544", # Builtin Administrators
23
+ }
24
+ _WINDOWS_DOMAIN_USER_RID_PATTERN = re.compile(r"^S-1-5-21-\d+-\d+-\d+-(513|514)$")
25
+ _WINDOWS_FILE_READ_WRITE_ACCESS_MASK = (
26
+ 0x00000001 # FILE_READ_DATA
27
+ | 0x00000002 # FILE_WRITE_DATA
28
+ | 0x00000004 # FILE_APPEND_DATA
29
+ | 0x00000008 # FILE_READ_EA
30
+ | 0x00000010 # FILE_WRITE_EA
31
+ | 0x00000080 # FILE_READ_ATTRIBUTES
32
+ | 0x00000100 # FILE_WRITE_ATTRIBUTES
33
+ | 0x00010000 # DELETE
34
+ | 0x00020000 # READ_CONTROL
35
+ | 0x00040000 # WRITE_DAC
36
+ | 0x00080000 # WRITE_OWNER
37
+ | 0x10000000 # GENERIC_ALL
38
+ | 0x40000000 # GENERIC_WRITE
39
+ | 0x80000000 # GENERIC_READ
40
+ )
41
+ _WINDOWS_DIRECTORY_MUTATING_ACCESS_MASK = (
42
+ 0x00000002 # FILE_ADD_FILE
43
+ | 0x00000004 # FILE_ADD_SUBDIRECTORY
44
+ | 0x00000040 # FILE_DELETE_CHILD
45
+ | 0x00010000 # DELETE
46
+ | 0x00040000 # WRITE_DAC
47
+ | 0x00080000 # WRITE_OWNER
48
+ | 0x10000000 # GENERIC_ALL
49
+ | 0x40000000 # GENERIC_WRITE
50
+ )
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class _WindowsAccessAce:
55
+ sid: str
56
+ mask: int
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class _WindowsFileSecurity:
61
+ owner_sid: str | None
62
+ access_aces: tuple[_WindowsAccessAce, ...]
63
+ has_null_dacl: bool
64
+
65
+
66
+ def _windows_principal_name(sid: str) -> str:
67
+ if sid in _WINDOWS_PRINCIPAL_NAMES:
68
+ return _WINDOWS_PRINCIPAL_NAMES[sid]
69
+ match = _WINDOWS_DOMAIN_USER_RID_PATTERN.fullmatch(sid)
70
+ if match is None:
71
+ return sid
72
+ rid = match.group(1)
73
+ if rid == "513":
74
+ return "Domain Users"
75
+ return "Domain Guests"
76
+
77
+
78
+ def _windows_unallowed_access_reason(
79
+ access_aces: Sequence[_WindowsAccessAce],
80
+ *,
81
+ owner_sid: str | None,
82
+ access_mask: int,
83
+ ) -> str | None:
84
+ allowed_sids = set(_WINDOWS_ALLOWED_RUNTIME_PERMISSION_SIDS)
85
+ if owner_sid is not None:
86
+ allowed_sids.add(owner_sid)
87
+ for ace in access_aces:
88
+ if not ace.mask & access_mask:
89
+ continue
90
+ if ace.sid in allowed_sids:
91
+ continue
92
+ principal_name = _windows_principal_name(ace.sid)
93
+ return f"{principal_name} ({ace.sid}) grants access outside the allowlist"
94
+ return None
95
+
96
+
97
+ def _windows_file_security(path: Path) -> _WindowsFileSecurity:
98
+ import ctypes
99
+ from ctypes import wintypes
100
+
101
+ if os.name != "nt":
102
+ raise OSError("Windows ACL inspection is only available on Windows")
103
+
104
+ class AclSizeInformation(ctypes.Structure):
105
+ _fields_ = [
106
+ ("AceCount", wintypes.DWORD),
107
+ ("AclBytesInUse", wintypes.DWORD),
108
+ ("AclBytesFree", wintypes.DWORD),
109
+ ]
110
+
111
+ class AceHeader(ctypes.Structure):
112
+ _fields_ = [
113
+ ("AceType", wintypes.BYTE),
114
+ ("AceFlags", wintypes.BYTE),
115
+ ("AceSize", wintypes.WORD),
116
+ ]
117
+
118
+ class AccessAllowedAce(ctypes.Structure):
119
+ _fields_ = [
120
+ ("Header", AceHeader),
121
+ ("Mask", wintypes.DWORD),
122
+ ("SidStart", wintypes.DWORD),
123
+ ]
124
+
125
+ se_file_object = 1
126
+ dacl_security_information = 0x00000004
127
+ owner_security_information = 0x00000001
128
+ acl_size_information = 2
129
+ access_allowed_ace_type = 0
130
+
131
+ psid = ctypes.c_void_p
132
+ win_dll = getattr(ctypes, "WinDLL", None)
133
+ if win_dll is None:
134
+ raise OSError("ctypes.WinDLL is not available")
135
+
136
+ def last_windows_error(message: str) -> OSError:
137
+ get_last_error = getattr(ctypes, "get_last_error", lambda: 0)
138
+ return OSError(get_last_error(), message)
139
+
140
+ advapi32 = win_dll("advapi32", use_last_error=True)
141
+ kernel32 = win_dll("kernel32", use_last_error=True)
142
+
143
+ get_named_security_info = advapi32.GetNamedSecurityInfoW
144
+ get_named_security_info.argtypes = [
145
+ wintypes.LPWSTR,
146
+ wintypes.DWORD,
147
+ wintypes.DWORD,
148
+ ctypes.POINTER(psid),
149
+ ctypes.POINTER(psid),
150
+ ctypes.POINTER(ctypes.c_void_p),
151
+ ctypes.POINTER(ctypes.c_void_p),
152
+ ctypes.POINTER(ctypes.c_void_p),
153
+ ]
154
+ get_named_security_info.restype = wintypes.DWORD
155
+
156
+ get_acl_information = advapi32.GetAclInformation
157
+ get_acl_information.argtypes = [
158
+ ctypes.c_void_p,
159
+ ctypes.c_void_p,
160
+ wintypes.DWORD,
161
+ wintypes.DWORD,
162
+ ]
163
+ get_acl_information.restype = wintypes.BOOL
164
+
165
+ get_ace = advapi32.GetAce
166
+ get_ace.argtypes = [
167
+ ctypes.c_void_p,
168
+ wintypes.DWORD,
169
+ ctypes.POINTER(ctypes.c_void_p),
170
+ ]
171
+ get_ace.restype = wintypes.BOOL
172
+
173
+ convert_sid_to_string_sid = advapi32.ConvertSidToStringSidW
174
+ convert_sid_to_string_sid.argtypes = [
175
+ psid,
176
+ ctypes.POINTER(wintypes.LPWSTR),
177
+ ]
178
+ convert_sid_to_string_sid.restype = wintypes.BOOL
179
+
180
+ local_free = kernel32.LocalFree
181
+ local_free.argtypes = [ctypes.c_void_p]
182
+ local_free.restype = ctypes.c_void_p
183
+
184
+ dacl = ctypes.c_void_p()
185
+ owner_sid = ctypes.c_void_p()
186
+ security_descriptor = ctypes.c_void_p()
187
+ result = get_named_security_info(
188
+ str(path),
189
+ se_file_object,
190
+ owner_security_information | dacl_security_information,
191
+ ctypes.byref(owner_sid),
192
+ None,
193
+ ctypes.byref(dacl),
194
+ None,
195
+ ctypes.byref(security_descriptor),
196
+ )
197
+ if result != 0:
198
+ raise OSError(result, f"GetNamedSecurityInfoW failed for {path}")
199
+ try:
200
+ owner_sid_string_value: str | None = None
201
+ if owner_sid:
202
+ owner_sid_string = wintypes.LPWSTR()
203
+ if not convert_sid_to_string_sid(owner_sid, ctypes.byref(owner_sid_string)):
204
+ raise last_windows_error("ConvertSidToStringSidW failed for owner")
205
+ try:
206
+ owner_sid_string_value = owner_sid_string.value
207
+ if owner_sid_string_value is None:
208
+ raise OSError("ConvertSidToStringSidW returned a null owner SID")
209
+ finally:
210
+ local_free(ctypes.cast(owner_sid_string, ctypes.c_void_p))
211
+
212
+ if not dacl:
213
+ return _WindowsFileSecurity(
214
+ owner_sid=owner_sid_string_value,
215
+ access_aces=(),
216
+ has_null_dacl=True,
217
+ )
218
+
219
+ acl_info = AclSizeInformation()
220
+ if not get_acl_information(
221
+ dacl,
222
+ ctypes.byref(acl_info),
223
+ ctypes.sizeof(acl_info),
224
+ acl_size_information,
225
+ ):
226
+ raise last_windows_error("GetAclInformation failed")
227
+
228
+ access_aces: list[_WindowsAccessAce] = []
229
+ for index in range(acl_info.AceCount):
230
+ ace_pointer = ctypes.c_void_p()
231
+ if not get_ace(dacl, index, ctypes.byref(ace_pointer)):
232
+ raise last_windows_error("GetAce failed")
233
+ if ace_pointer.value is None:
234
+ raise OSError("GetAce returned a null ACE pointer")
235
+ ace = ctypes.cast(
236
+ ace_pointer,
237
+ ctypes.POINTER(AccessAllowedAce),
238
+ ).contents
239
+ if ace.Header.AceType != access_allowed_ace_type:
240
+ continue
241
+ sid_pointer = ctypes.c_void_p(
242
+ ace_pointer.value + AccessAllowedAce.SidStart.offset
243
+ )
244
+ sid_string = wintypes.LPWSTR()
245
+ if not convert_sid_to_string_sid(sid_pointer, ctypes.byref(sid_string)):
246
+ raise last_windows_error("ConvertSidToStringSidW failed")
247
+ try:
248
+ if sid_string.value is None:
249
+ raise OSError("ConvertSidToStringSidW returned a null SID")
250
+ access_aces.append(
251
+ _WindowsAccessAce(sid=sid_string.value, mask=int(ace.Mask))
252
+ )
253
+ finally:
254
+ local_free(ctypes.cast(sid_string, ctypes.c_void_p))
255
+ return _WindowsFileSecurity(
256
+ owner_sid=owner_sid_string_value,
257
+ access_aces=tuple(access_aces),
258
+ has_null_dacl=False,
259
+ )
260
+ finally:
261
+ if security_descriptor:
262
+ local_free(security_descriptor)
263
+
264
+
265
+ def _windows_unallowed_permission_reason(path: Path, *, access_mask: int) -> str | None:
266
+ security = _windows_file_security(path)
267
+ if security.has_null_dacl:
268
+ return "file has a null DACL, which grants full access"
269
+ return _windows_unallowed_access_reason(
270
+ security.access_aces,
271
+ owner_sid=security.owner_sid,
272
+ access_mask=access_mask,
273
+ )
274
+
275
+
276
+ def _windows_icacls_remediation(path: Path) -> str:
277
+ return (
278
+ "repair ACLs from an elevated Command Prompt (cmd.exe) on the Arbiter "
279
+ f'host by running `takeown /F "{path}"`, then '
280
+ f'`icacls "{path}" /inheritance:r '
281
+ '/grant:r "%USERDOMAIN%\\%USERNAME%:F" '
282
+ '/grant:r "*S-1-5-18:F" /grant:r "*S-1-5-32-544:F"`'
283
+ )
284
+
285
+
286
+ def ensure_runtime_config_permissions(
287
+ *,
288
+ config_dir: Path,
289
+ env_file: Path | None,
290
+ ) -> None:
291
+ for directory in sorted(
292
+ {config_dir, *(path.parent for path in config_dir.rglob("*.yaml"))}
293
+ ):
294
+ if not directory.is_dir():
295
+ continue
296
+ try:
297
+ reason = _windows_unallowed_permission_reason(
298
+ directory,
299
+ access_mask=_WINDOWS_DIRECTORY_MUTATING_ACCESS_MASK,
300
+ )
301
+ except OSError as exc:
302
+ raise ValueError(
303
+ "unsafe config directory permissions: "
304
+ f"could not verify Windows ACLs for {directory}; "
305
+ "refusing to use runtime config with unverified permissions. "
306
+ f"{_windows_icacls_remediation(directory)}"
307
+ ) from exc
308
+ if reason is not None:
309
+ raise ValueError(
310
+ "unsafe config directory permissions: "
311
+ f"{directory} must not grant mutation access outside the owner, "
312
+ f"SYSTEM, or Administrators ({reason}); "
313
+ f"{_windows_icacls_remediation(directory)}"
314
+ )
315
+
316
+ for config_file in sorted(config_dir.rglob("*.yaml")):
317
+ if not config_file.is_file():
318
+ continue
319
+ try:
320
+ reason = _windows_unallowed_permission_reason(
321
+ config_file,
322
+ access_mask=_WINDOWS_FILE_READ_WRITE_ACCESS_MASK,
323
+ )
324
+ except OSError as exc:
325
+ raise ValueError(
326
+ "unsafe config file permissions: "
327
+ f"could not verify Windows ACLs for {config_file}; "
328
+ "refusing to use runtime config with unverified permissions. "
329
+ f"{_windows_icacls_remediation(config_file)}"
330
+ ) from exc
331
+ if reason is not None:
332
+ raise ValueError(
333
+ "unsafe config file permissions: "
334
+ f"{config_file} must not grant read/write access outside the "
335
+ f"owner, SYSTEM, or Administrators ({reason}); "
336
+ f"{_windows_icacls_remediation(config_file)}"
337
+ )
338
+
339
+ if env_file is None or not env_file.exists():
340
+ return
341
+ if env_file.parent.exists():
342
+ try:
343
+ reason = _windows_unallowed_permission_reason(
344
+ env_file.parent,
345
+ access_mask=_WINDOWS_DIRECTORY_MUTATING_ACCESS_MASK,
346
+ )
347
+ except OSError as exc:
348
+ raise ValueError(
349
+ "unsafe app env directory permissions: "
350
+ f"could not verify Windows ACLs for {env_file.parent}; "
351
+ "refusing to load runtime env file with unverified permissions. "
352
+ f"{_windows_icacls_remediation(env_file.parent)}"
353
+ ) from exc
354
+ if reason is not None:
355
+ raise ValueError(
356
+ "unsafe app env directory permissions: "
357
+ f"{env_file.parent} must not grant mutation access outside "
358
+ f"owner, SYSTEM, or Administrators ({reason}); "
359
+ f"{_windows_icacls_remediation(env_file.parent)}"
360
+ )
361
+ try:
362
+ reason = _windows_unallowed_permission_reason(
363
+ env_file,
364
+ access_mask=_WINDOWS_FILE_READ_WRITE_ACCESS_MASK,
365
+ )
366
+ except OSError as exc:
367
+ raise ValueError(
368
+ "unsafe app env file permissions: "
369
+ f"could not verify Windows ACLs for {env_file}; "
370
+ "refusing to load runtime env file with unverified permissions. "
371
+ f"{_windows_icacls_remediation(env_file)}"
372
+ ) from exc
373
+ if reason is not None:
374
+ raise ValueError(
375
+ "unsafe app env file permissions: "
376
+ f"{env_file} must not grant read/write access outside the "
377
+ f"owner, SYSTEM, or Administrators ({reason}); "
378
+ f"{_windows_icacls_remediation(env_file)}"
379
+ )