Skip to content

Commit 6873156

Browse files
authored
fix: improve response time on windows when using deadline config GUI (#540)
1 parent ca15ded commit 6873156

File tree

2 files changed

+128
-104
lines changed

2 files changed

+128
-104
lines changed

src/deadline/client/config/config_file.py

Lines changed: 43 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import getpass
1717
import os
1818
import platform
19-
import subprocess
2019
from configparser import ConfigParser
2120
from pathlib import Path
2221
from typing import Any, Dict, List, Optional
@@ -26,7 +25,6 @@
2625
from deadline.job_attachments.models import FileConflictResolution
2726

2827
from ..exceptions import DeadlineOperationError
29-
import re
3028

3129
# Default path where AWS Deadline Cloud's configuration lives
3230
CONFIG_FILE_PATH = os.path.join("~", ".deadline", "config")
@@ -182,80 +180,50 @@ def read_config() -> ConfigParser:
182180
return __config
183181

184182

185-
def _get_grant_args(principal: str, permissions: str) -> List[str]:
186-
return [
187-
"/grant",
188-
f"{principal}:{permissions}",
189-
# Apply recursively
190-
"/T",
191-
]
192-
193-
194-
RE_ICACLS_OUTPUT = re.compile(r"^(.+?(?=\\))?(?:\\)?(.+?(?=:)):(.*)$")
195-
196-
197-
def _reset_directory_permissions_windows(directory: Path, username: str, permissions: str) -> None:
183+
def _reset_directory_permissions_windows(directory: Path) -> None:
198184
if platform.system() != "Windows":
199185
return
186+
import win32security
187+
import ntsecuritycon
188+
189+
# We don't want to propagate existing permissions, so create a new DACL
190+
dacl = win32security.ACL()
191+
192+
# On Windows, both SYSTEM and the Administrators group normally
193+
# have Full Access to files in the user's home directory.
194+
# Use SIDs to represent the Administrators and SYSTEM to
195+
# support multi-language operating systems
196+
# Administrator(S-1-5-32-544), SYSTEM(S-1-5-18)
197+
# https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
198+
system_sid = win32security.ConvertStringSidToSid("S-1-5-18")
199+
admin_sid = win32security.ConvertStringSidToSid("S-1-5-32-544")
200+
201+
username = getpass.getuser()
202+
user_sid, _, _ = win32security.LookupAccountName(None, username)
203+
204+
for sid in [user_sid, admin_sid, system_sid]:
205+
dacl.AddAccessAllowedAceEx(
206+
win32security.ACL_REVISION,
207+
ntsecuritycon.OBJECT_INHERIT_ACE | ntsecuritycon.CONTAINER_INHERIT_ACE,
208+
ntsecuritycon.GENERIC_ALL,
209+
sid,
210+
)
200211

201-
result = subprocess.run(
202-
[
203-
"icacls",
204-
str(directory),
205-
],
206-
check=True,
207-
capture_output=True,
208-
text=True,
212+
# Get the security descriptor of the object
213+
sd = win32security.GetFileSecurity(
214+
str(directory.resolve()), win32security.DACL_SECURITY_INFORMATION
209215
)
210216

211-
icacls_output = result.stdout
212-
213-
principals_to_remove = []
214-
215-
for line in icacls_output.splitlines():
216-
if line.startswith(str(directory)):
217-
permission_line = line[len(str(directory)) :].strip()
218-
else:
219-
permission_line = line.strip()
220-
221-
permissions_match = RE_ICACLS_OUTPUT.match(permission_line)
222-
if permissions_match:
223-
ad_group = permissions_match.group(1)
224-
ad_user = permissions_match.group(2)
225-
principal = f"{ad_group}\\{ad_user}"
226-
if (
227-
ad_user != username
228-
and principal != "BUILTIN\\Administrators"
229-
and principal != "NT AUTHORITY\\SYSTEM"
230-
):
231-
principals_to_remove.append(ad_user)
232-
233-
for principal in principals_to_remove:
234-
subprocess.run(
235-
[
236-
"icacls",
237-
str(directory),
238-
"/remove",
239-
principal,
240-
],
241-
check=True,
242-
)
243-
244-
subprocess.run(
245-
[
246-
"icacls",
247-
str(directory),
248-
*_get_grant_args(username, permissions),
249-
# On Windows, both SYSTEM and the Administrators group normally
250-
# have Full Access to files in the user's home directory.
251-
# Use SIDs to represent the Administrators and SYSTEM to
252-
# support multi-language operating systems
253-
# Administrator(S-1-5-32-544), SYSTEM(S-1-5-18)
254-
*_get_grant_args("*S-1-5-32-544", permissions),
255-
*_get_grant_args("*S-1-5-18", permissions),
256-
],
257-
check=True,
258-
capture_output=True,
217+
# Set the security descriptor's DACL to the newly-created DACL
218+
# Arguments:
219+
# 1. bDaclPresent = 1: Indicates that the DACL is present in the security descriptor.
220+
# If set to 0, this method ignores the provided DACL and allows access to all principals.
221+
# 2. dacl: The discretionary access control list (DACL) to be set in the security descriptor.
222+
# 3. bDaclDefaulted = 0: Indicates the DACL was provided and not defaulted.
223+
# If set to 1, indicates the DACL was defaulted, as in the case of permissions inherited from a parent directory.
224+
sd.SetSecurityDescriptorDacl(1, dacl, 0)
225+
win32security.SetFileSecurity(
226+
str(directory.resolve()), win32security.DACL_SECURITY_INFORMATION, sd
259227
)
260228

261229

@@ -268,15 +236,10 @@ def write_config(config: ConfigParser) -> None:
268236
a modified value from what `read_config` returns.
269237
"""
270238
config_file_path = get_config_file_path()
271-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
272-
273-
if platform.system() == "Windows":
274-
username = getpass.getuser()
275-
config_file_parent_path = config_file_path.parent.absolute()
276-
# OI - Contained objects will inherit
277-
# CI - Sub-directories will inherit
278-
# F - Full control
279-
_reset_directory_permissions_windows(config_file_parent_path, username, "(OI)(CI)(F)")
239+
if not config_file_path.parent.exists():
240+
config_file_path.parent.mkdir(parents=True, exist_ok=True)
241+
if platform.system() == "Windows":
242+
_reset_directory_permissions_windows(config_file_path.parent)
280243

281244
# Using the config file path as the prefix ensures that the tmpfile and real file are
282245
# on the same filesystem. This is a requirement for os.replace to be atomic.

test/unit/deadline_client/config/test_config_file.py

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
import os
88
import platform
9-
import subprocess
9+
import getpass
10+
import tempfile
1011
from unittest.mock import patch, MagicMock
1112
from pathlib import Path
1213

@@ -273,32 +274,92 @@ def test_default_log_level():
273274
platform.system() != "Windows",
274275
reason="This test is for testing file permission changes in Windows.",
275276
)
276-
def test_windows_config_file_permissions(fresh_deadline_config) -> None:
277-
config_file_path = config_file.get_config_file_path()
278-
parent_dir = config_file_path.parent
279-
subprocess.run(
280-
[
281-
"icacls",
282-
str(parent_dir),
283-
"/grant",
284-
"Everyone:(OI)(CI)(F)",
285-
"/T",
286-
],
287-
check=True,
288-
)
277+
def test_reset_directory_permissions_windows() -> None:
278+
"""
279+
Asserts the _reset_directory_permissions_windows configures the provided
280+
folder with access only to the active user, the domain admin, and SYSTEM.
281+
"""
282+
# GIVEN
283+
import ntsecuritycon
284+
import win32security
285+
286+
path = Path(tempfile.gettempdir())
287+
system_sid = win32security.ConvertStringSidToSid("S-1-5-18")
288+
admin_sid = win32security.ConvertStringSidToSid("S-1-5-32-544")
289+
user_sid, _, _ = win32security.LookupAccountName(None, getpass.getuser())
290+
sids = [system_sid, admin_sid, user_sid]
291+
292+
# WHEN
293+
config_file._reset_directory_permissions_windows(path)
294+
295+
# THEN
296+
sd = win32security.GetFileSecurity(str(path.resolve()), win32security.DACL_SECURITY_INFORMATION)
297+
dacl = sd.GetSecurityDescriptorDacl()
298+
assert dacl.GetAceCount() == 3
299+
assert dacl.GetAclRevision() == win32security.ACL_REVISION
300+
for i in range(3):
301+
(acetype, aceflags), access, sid = dacl.GetAce(i)
302+
assert acetype == win32security.ACCESS_ALLOWED_ACE_TYPE
303+
assert aceflags == ntsecuritycon.OBJECT_INHERIT_ACE | ntsecuritycon.CONTAINER_INHERIT_ACE
304+
assert access == ntsecuritycon.FILE_ALL_ACCESS
305+
try:
306+
sids.remove(sid)
307+
except ValueError:
308+
assert False, f"Unexpected SID: {win32security.ConvertSidToStringSid(sid)}"
289309

290-
config_file.set_setting("defaults.aws_profile_name", "goodguyprofile")
291310

292-
result = subprocess.run(
293-
[
294-
"icacls",
295-
str(config_file_path),
296-
],
297-
check=True,
298-
capture_output=True,
299-
text=True,
311+
@pytest.mark.skipif(
312+
platform.system() != "Windows",
313+
reason="This test is for testing file permission changes in Windows.",
314+
)
315+
@patch.object(config_file, "get_config_file_path")
316+
def test_write_config_directory_permission_windows(
317+
mock_get_config_file_path,
318+
):
319+
"""
320+
Tests that the config directory permissions are not modified when writing to the config file
321+
"""
322+
# GIVEN
323+
path = Path(tempfile.gettempdir())
324+
config_path = path / "config"
325+
mock_get_config_file_path.return_value = config_path
326+
327+
# ----------------------------------------------------------------------------------------------
328+
# Sets up a directory with an added full access entry for domain guests. Since this is not a
329+
# typically expected entry, it can be used to validate existing permissions were not overwritten
330+
import win32security
331+
import ntsecuritycon
332+
333+
sd = win32security.GetFileSecurity(str(path.resolve()), win32security.DACL_SECURITY_INFORMATION)
334+
guest_sid = win32security.ConvertStringSidToSid("S-1-5-32-546") # Domain Guests
335+
dacl = sd.GetSecurityDescriptorDacl()
336+
dacl.AddAccessAllowedAceEx(
337+
win32security.ACL_REVISION,
338+
ntsecuritycon.OBJECT_INHERIT_ACE | ntsecuritycon.CONTAINER_INHERIT_ACE,
339+
ntsecuritycon.FILE_ALL_ACCESS,
340+
guest_sid,
300341
)
301-
assert "Everyone" not in result.stdout
342+
sd.SetSecurityDescriptorDacl(1, dacl, 0)
343+
win32security.SetFileSecurity(str(path.resolve()), win32security.DACL_SECURITY_INFORMATION, sd)
344+
# ----------------------------------------------------------------------------------------------
345+
346+
# WHEN
347+
config_file.write_config(MagicMock())
348+
349+
# THEN
350+
new_dacl = win32security.GetFileSecurity(
351+
str(path.resolve()), win32security.DACL_SECURITY_INFORMATION
352+
).GetSecurityDescriptorDacl()
353+
354+
assert new_dacl.GetAceCount() == dacl.GetAceCount()
355+
for i in range(dacl.GetAceCount()):
356+
# Assert the access control entries are identical
357+
(acetype, aceflags), access, sid = dacl.GetAce(i)
358+
(new_acetype, new_aceflags), new_access, new_sid = new_dacl.GetAce(i)
359+
assert acetype == new_acetype
360+
assert aceflags == new_aceflags
361+
assert access == new_access
362+
assert sid == new_sid
302363

303364

304365
@pytest.mark.skipif(

0 commit comments

Comments
 (0)