unctools 0.1.0__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,586 @@
1
+ """
2
+ Windows security utilities for UNC paths and network drives.
3
+
4
+ This module provides functions for handling Windows security aspects of UNC paths,
5
+ including access permissions, sharing options, and security token management.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+ import subprocess
11
+ from typing import Dict, List, Optional, Tuple, Union, Any
12
+
13
+ # Set up module-level logger
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Check if we're running on Windows
17
+ IS_WINDOWS = os.name == 'nt'
18
+
19
+ # Try to import Windows-specific modules
20
+ if IS_WINDOWS:
21
+ try:
22
+ import win32security
23
+ import win32api
24
+ import win32con
25
+ import win32net
26
+ import ntsecuritycon
27
+ HAVE_WIN32SECURITY = True
28
+ except ImportError:
29
+ HAVE_WIN32SECURITY = False
30
+ logger.warning("win32security module not available. Security operations will use limited functionality.")
31
+ else:
32
+ HAVE_WIN32SECURITY = False
33
+
34
+ # Define or import constants for ACE types
35
+ if IS_WINDOWS and HAVE_WIN32SECURITY:
36
+ # Use constants from win32security when available
37
+ ACCESS_ALLOWED_ACE_TYPE = win32security.ACCESS_ALLOWED_ACE_TYPE
38
+ ACCESS_DENIED_ACE_TYPE = win32security.ACCESS_DENIED_ACE_TYPE
39
+ SYSTEM_AUDIT_ACE_TYPE = win32security.SYSTEM_AUDIT_ACE_TYPE
40
+
41
+ # SYSTEM_ALARM_ACE_TYPE may not be available in all versions
42
+ try:
43
+ SYSTEM_ALARM_ACE_TYPE = win32security.SYSTEM_ALARM_ACE_TYPE
44
+ except AttributeError:
45
+ # Define fallback value based on Windows SDK
46
+ SYSTEM_ALARM_ACE_TYPE = 3
47
+ logger.debug("win32security.SYSTEM_ALARM_ACE_TYPE not available, using fallback value")
48
+ else:
49
+ # Define fallback values for non-Windows or without win32security
50
+ ACCESS_ALLOWED_ACE_TYPE = 0
51
+ ACCESS_DENIED_ACE_TYPE = 1
52
+ SYSTEM_AUDIT_ACE_TYPE = 2
53
+ SYSTEM_ALARM_ACE_TYPE = 3
54
+
55
+ def get_file_security(path: str) -> Optional[Dict[str, Any]]:
56
+ """
57
+ Get security information for a file or directory.
58
+
59
+ Args:
60
+ path: The path to get security information for.
61
+
62
+ Returns:
63
+ A dictionary with security information, or None if not available.
64
+ """
65
+ if not IS_WINDOWS or not HAVE_WIN32SECURITY:
66
+ logger.warning("Security information is only available on Windows with win32security.")
67
+ return None
68
+
69
+ try:
70
+ # Get security descriptor
71
+ sd = win32security.GetFileSecurity(
72
+ path,
73
+ win32security.OWNER_SECURITY_INFORMATION |
74
+ win32security.GROUP_SECURITY_INFORMATION |
75
+ win32security.DACL_SECURITY_INFORMATION
76
+ )
77
+
78
+ # Get owner, group, and DACL information
79
+ owner_sid = sd.GetSecurityDescriptorOwner()
80
+ group_sid = sd.GetSecurityDescriptorGroup()
81
+ dacl = sd.GetSecurityDescriptorDacl()
82
+
83
+ # Convert SIDs to names
84
+ try:
85
+ owner_name, domain, _ = win32security.LookupAccountSid(None, owner_sid)
86
+ owner = f"{domain}\\{owner_name}"
87
+ except:
88
+ owner = str(owner_sid)
89
+
90
+ try:
91
+ group_name, domain, _ = win32security.LookupAccountSid(None, group_sid)
92
+ group = f"{domain}\\{group_name}"
93
+ except:
94
+ group = str(group_sid)
95
+
96
+ # Parse DACL
97
+ acl_entries = []
98
+ if dacl:
99
+ for i in range(dacl.GetAceCount()):
100
+ ace = dacl.GetAce(i)
101
+ try:
102
+ trustee_sid = ace[2]
103
+ trustee_name, domain, _ = win32security.LookupAccountSid(None, trustee_sid)
104
+ trustee = f"{domain}\\{trustee_name}"
105
+ except:
106
+ trustee = str(trustee_sid)
107
+
108
+ # Get access mask and type
109
+ ace_type = ace[0][0] # Type
110
+ ace_flags = ace[0][1] # Flags
111
+ ace_mask = ace[1] # Access mask
112
+
113
+ acl_entries.append({
114
+ 'trustee': trustee,
115
+ 'type': _get_ace_type_name(ace_type),
116
+ 'flags': _get_ace_flags(ace_flags),
117
+ 'permissions': _get_permission_names(ace_mask)
118
+ })
119
+
120
+ return {
121
+ 'owner': owner,
122
+ 'group': group,
123
+ 'acl': acl_entries
124
+ }
125
+
126
+ except Exception as e:
127
+ logger.error(f"Failed to get security information for {path}: {e}")
128
+ return None
129
+
130
+ def _get_ace_type_name(ace_type: int) -> str:
131
+ """
132
+ Get a string representation of the ACE type.
133
+
134
+ Args:
135
+ ace_type: The ACE type value.
136
+
137
+ Returns:
138
+ A string describing the ACE type.
139
+ """
140
+ ace_types = {
141
+ ACCESS_ALLOWED_ACE_TYPE: "Allow",
142
+ ACCESS_DENIED_ACE_TYPE: "Deny",
143
+ SYSTEM_AUDIT_ACE_TYPE: "Audit",
144
+ SYSTEM_ALARM_ACE_TYPE: "Alarm"
145
+ }
146
+ return ace_types.get(ace_type, f"Unknown ({ace_type})")
147
+
148
+ def _get_ace_flags(ace_flags: int) -> List[str]:
149
+ """
150
+ Get a list of string representations of the ACE flags.
151
+
152
+ Args:
153
+ ace_flags: The ACE flags value.
154
+
155
+ Returns:
156
+ A list of strings describing the ACE flags.
157
+ """
158
+ flags = []
159
+ if IS_WINDOWS and HAVE_WIN32SECURITY:
160
+ if ace_flags & win32security.OBJECT_INHERIT_ACE:
161
+ flags.append("Object Inherit")
162
+ if ace_flags & win32security.CONTAINER_INHERIT_ACE:
163
+ flags.append("Container Inherit")
164
+ if ace_flags & win32security.NO_PROPAGATE_INHERIT_ACE:
165
+ flags.append("No Propagate")
166
+ if ace_flags & win32security.INHERIT_ONLY_ACE:
167
+ flags.append("Inherit Only")
168
+ if ace_flags & win32security.INHERITED_ACE:
169
+ flags.append("Inherited")
170
+ return flags
171
+
172
+ def _get_permission_names(access_mask: int) -> List[str]:
173
+ """
174
+ Get a list of string representations of the permissions.
175
+
176
+ Args:
177
+ access_mask: The access mask value.
178
+
179
+ Returns:
180
+ A list of strings describing the permissions.
181
+ """
182
+ permissions = []
183
+
184
+ # Only try to decode permissions if we have the necessary module
185
+ if IS_WINDOWS and HAVE_WIN32SECURITY:
186
+ # File permissions
187
+ if access_mask & ntsecuritycon.FILE_READ_DATA:
188
+ permissions.append("Read")
189
+ if access_mask & ntsecuritycon.FILE_WRITE_DATA:
190
+ permissions.append("Write")
191
+ if access_mask & ntsecuritycon.FILE_APPEND_DATA:
192
+ permissions.append("Append")
193
+ if access_mask & ntsecuritycon.FILE_EXECUTE:
194
+ permissions.append("Execute")
195
+ if access_mask & ntsecuritycon.DELETE:
196
+ permissions.append("Delete")
197
+ if access_mask & ntsecuritycon.READ_CONTROL:
198
+ permissions.append("Read Permissions")
199
+ if access_mask & ntsecuritycon.WRITE_DAC:
200
+ permissions.append("Change Permissions")
201
+ if access_mask & ntsecuritycon.WRITE_OWNER:
202
+ permissions.append("Take Ownership")
203
+
204
+ # Generic permissions
205
+ if access_mask & ntsecuritycon.GENERIC_READ:
206
+ permissions.append("Generic Read")
207
+ if access_mask & ntsecuritycon.GENERIC_WRITE:
208
+ permissions.append("Generic Write")
209
+ if access_mask & ntsecuritycon.GENERIC_EXECUTE:
210
+ permissions.append("Generic Execute")
211
+ if access_mask & ntsecuritycon.GENERIC_ALL:
212
+ permissions.append("Full Control")
213
+
214
+ return permissions
215
+
216
+ def set_file_permissions(path: str, trustee: str, permissions: str,
217
+ allow: bool = True) -> bool:
218
+ """
219
+ Set permissions on a file or directory.
220
+
221
+ Args:
222
+ path: The path to set permissions on.
223
+ trustee: The user or group to set permissions for (e.g., "domain\\user").
224
+ permissions: The permission to set ("read", "write", "execute", "full").
225
+ allow: If True, grant the permissions; if False, deny them.
226
+
227
+ Returns:
228
+ True if successful, False otherwise.
229
+ """
230
+ if not IS_WINDOWS or not HAVE_WIN32SECURITY:
231
+ logger.warning("Setting permissions is only available on Windows with win32security.")
232
+ return False
233
+
234
+ try:
235
+ # Get security descriptor
236
+ sd = win32security.GetFileSecurity(
237
+ path,
238
+ win32security.DACL_SECURITY_INFORMATION
239
+ )
240
+
241
+ # Get existing DACL
242
+ dacl = sd.GetSecurityDescriptorDacl()
243
+ if dacl is None:
244
+ dacl = win32security.ACL()
245
+
246
+ # Determine access mask based on permissions
247
+ access_mask = 0
248
+ if permissions.lower() == "read":
249
+ access_mask = ntsecuritycon.FILE_GENERIC_READ
250
+ elif permissions.lower() == "write":
251
+ access_mask = ntsecuritycon.FILE_GENERIC_WRITE
252
+ elif permissions.lower() == "execute":
253
+ access_mask = ntsecuritycon.FILE_GENERIC_EXECUTE
254
+ elif permissions.lower() == "full":
255
+ access_mask = ntsecuritycon.FILE_ALL_ACCESS
256
+ else:
257
+ logger.error(f"Unknown permission: {permissions}")
258
+ return False
259
+
260
+ # Convert trustee name to SID
261
+ try:
262
+ if "\\" in trustee:
263
+ domain, user = trustee.split("\\", 1)
264
+ else:
265
+ domain, user = "", trustee
266
+
267
+ trustee_sid, _, _ = win32security.LookupAccountName(None, trustee)
268
+ except Exception as e:
269
+ logger.error(f"Failed to look up SID for {trustee}: {e}")
270
+ return False
271
+
272
+ # Add ACE to DACL
273
+ ace_type = ACCESS_ALLOWED_ACE_TYPE if allow else ACCESS_DENIED_ACE_TYPE
274
+ dacl.AddAccessAllowedAce(win32security.ACL_REVISION, access_mask, trustee_sid)
275
+
276
+ # Set new DACL
277
+ sd.SetSecurityDescriptorDacl(1, dacl, 0)
278
+ win32security.SetFileSecurity(
279
+ path,
280
+ win32security.DACL_SECURITY_INFORMATION,
281
+ sd
282
+ )
283
+
284
+ logger.info(f"Set {permissions} permissions for {trustee} on {path}")
285
+ return True
286
+
287
+ except Exception as e:
288
+ logger.error(f"Failed to set permissions on {path}: {e}")
289
+ return False
290
+
291
+ def take_ownership(path: str) -> bool:
292
+ """
293
+ Take ownership of a file or directory.
294
+
295
+ Args:
296
+ path: The path to take ownership of.
297
+
298
+ Returns:
299
+ True if successful, False otherwise.
300
+ """
301
+ if not IS_WINDOWS:
302
+ logger.warning("Taking ownership is only available on Windows.")
303
+ return False
304
+
305
+ # Try to use the Windows API if available
306
+ if HAVE_WIN32SECURITY:
307
+ try:
308
+ # Get current process token
309
+ token = win32security.OpenProcessToken(
310
+ win32api.GetCurrentProcess(),
311
+ win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY
312
+ )
313
+
314
+ # Enable SE_TAKE_OWNERSHIP_NAME privilege
315
+ privilege_id = win32security.LookupPrivilegeValue(
316
+ None, win32security.SE_TAKE_OWNERSHIP_NAME
317
+ )
318
+ win32security.AdjustTokenPrivileges(
319
+ token, 0, [(privilege_id, win32security.SE_PRIVILEGE_ENABLED)]
320
+ )
321
+
322
+ # Get current user SID
323
+ user_sid = win32security.GetTokenInformation(
324
+ token, win32security.TokenUser
325
+ )[0]
326
+
327
+ # Get security descriptor
328
+ sd = win32security.GetFileSecurity(
329
+ path, win32security.OWNER_SECURITY_INFORMATION
330
+ )
331
+
332
+ # Set new owner
333
+ sd.SetSecurityDescriptorOwner(user_sid, 0)
334
+ win32security.SetFileSecurity(
335
+ path, win32security.OWNER_SECURITY_INFORMATION, sd
336
+ )
337
+
338
+ logger.info(f"Successfully took ownership of {path}")
339
+ return True
340
+
341
+ except Exception as e:
342
+ logger.error(f"Failed to take ownership using API: {e}")
343
+ # Fall back to takeown command
344
+
345
+ # Fall back to using the takeown command
346
+ try:
347
+ result = subprocess.run(
348
+ ['takeown', '/f', path],
349
+ text=True, capture_output=True, check=False
350
+ )
351
+
352
+ if result.returncode == 0:
353
+ logger.info(f"Successfully took ownership of {path} using takeown")
354
+ return True
355
+ else:
356
+ logger.error(f"Failed to take ownership using takeown: {result.stderr}")
357
+ return False
358
+
359
+ except Exception as e:
360
+ logger.error(f"Failed to take ownership: {e}")
361
+ return False
362
+
363
+ def check_access_rights(path: str, desired_access: str = "read") -> bool:
364
+ """
365
+ Check if the current user has specific access rights to a path.
366
+
367
+ Args:
368
+ path: The path to check.
369
+ desired_access: The type of access to check for ("read", "write", "execute", "full").
370
+
371
+ Returns:
372
+ True if the user has the requested access, False otherwise.
373
+ """
374
+ if not IS_WINDOWS:
375
+ # On non-Windows platforms, use simpler checks
376
+ if desired_access.lower() == "read":
377
+ return os.access(path, os.R_OK)
378
+ elif desired_access.lower() == "write":
379
+ return os.access(path, os.W_OK)
380
+ elif desired_access.lower() == "execute":
381
+ return os.access(path, os.X_OK)
382
+ elif desired_access.lower() == "full":
383
+ return os.access(path, os.R_OK | os.W_OK | os.X_OK)
384
+ else:
385
+ logger.error(f"Unknown access type: {desired_access}")
386
+ return False
387
+
388
+ # Windows-specific checks
389
+ if not HAVE_WIN32SECURITY:
390
+ logger.warning("Detailed access checks require win32security.")
391
+ # Fall back to simpler checks
392
+ try:
393
+ if desired_access.lower() == "read":
394
+ # Try to open for reading
395
+ with open(path, 'r'):
396
+ pass
397
+ return True
398
+ elif desired_access.lower() == "write":
399
+ # Check if file exists and is writable, or if directory exists and we can create a temp file
400
+ if os.path.isfile(path):
401
+ return os.access(path, os.W_OK)
402
+ elif os.path.isdir(path):
403
+ try:
404
+ temp_file = os.path.join(path, "temp_access_check")
405
+ with open(temp_file, 'w'):
406
+ pass
407
+ os.remove(temp_file)
408
+ return True
409
+ except:
410
+ return False
411
+ return False
412
+ elif desired_access.lower() in ("execute", "full"):
413
+ # These require more complex checks
414
+ logger.warning("Detailed execute/full access check not available without win32security.")
415
+ return False
416
+ else:
417
+ logger.error(f"Unknown access type: {desired_access}")
418
+ return False
419
+ except:
420
+ return False
421
+
422
+ # Use win32security for detailed checks
423
+ try:
424
+ # Map desired access to access mask
425
+ if desired_access.lower() == "read":
426
+ access_mask = ntsecuritycon.FILE_GENERIC_READ
427
+ elif desired_access.lower() == "write":
428
+ access_mask = ntsecuritycon.FILE_GENERIC_WRITE
429
+ elif desired_access.lower() == "execute":
430
+ access_mask = ntsecuritycon.FILE_GENERIC_EXECUTE
431
+ elif desired_access.lower() == "full":
432
+ access_mask = ntsecuritycon.FILE_ALL_ACCESS
433
+ else:
434
+ logger.error(f"Unknown access type: {desired_access}")
435
+ return False
436
+
437
+ # Check access
438
+ if os.path.isdir(path):
439
+ # For directories, we need to check directory-specific access
440
+ handle = win32security.CreateFile(
441
+ path,
442
+ access_mask,
443
+ win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
444
+ None,
445
+ win32con.OPEN_EXISTING,
446
+ win32con.FILE_FLAG_BACKUP_SEMANTICS, # Required for directories
447
+ 0
448
+ )
449
+ else:
450
+ # For files, use standard file access
451
+ handle = win32security.CreateFile(
452
+ path,
453
+ access_mask,
454
+ win32con.FILE_SHARE_READ | win32con.FILE_SHARE_WRITE,
455
+ None,
456
+ win32con.OPEN_EXISTING,
457
+ 0,
458
+ 0
459
+ )
460
+
461
+ # If we got here, we have the requested access
462
+ win32api.CloseHandle(handle)
463
+ return True
464
+
465
+ except win32api.error as e:
466
+ # Access denied or other error
467
+ logger.debug(f"Access check failed for {path} with {desired_access}: {e}")
468
+ return False
469
+ except Exception as e:
470
+ logger.error(f"Error checking access rights: {e}")
471
+ return False
472
+
473
+ def get_unc_share_permissions(server: str, share: str) -> Optional[Dict[str, List[str]]]:
474
+ """
475
+ Get permissions for a network share.
476
+
477
+ Args:
478
+ server: The server name.
479
+ share: The share name.
480
+
481
+ Returns:
482
+ A dictionary mapping users/groups to permission lists, or None if not available.
483
+ """
484
+ if not IS_WINDOWS or not HAVE_WIN32SECURITY:
485
+ logger.warning("Share permissions are only available on Windows with win32security.")
486
+ return None
487
+
488
+ try:
489
+ # Get share information
490
+ server_unc = f"\\\\{server}"
491
+ share_info, _, _ = win32net.NetShareGetInfo(server, share, 502)
492
+
493
+ # Get share security descriptor
494
+ sd = share_info['security_descriptor']
495
+ if not sd:
496
+ logger.warning(f"No security descriptor available for {server}\\{share}")
497
+ return None
498
+
499
+ # Parse permissions
500
+ dacl = sd.GetSecurityDescriptorDacl()
501
+ if not dacl:
502
+ logger.warning(f"No DACL available for {server}\\{share}")
503
+ return None
504
+
505
+ permissions = {}
506
+ for i in range(dacl.GetAceCount()):
507
+ ace = dacl.GetAce(i)
508
+ try:
509
+ trustee_sid = ace[2]
510
+ trustee_name, domain, _ = win32security.LookupAccountSid(None, trustee_sid)
511
+ trustee = f"{domain}\\{trustee_name}"
512
+ except:
513
+ trustee = str(trustee_sid)
514
+
515
+ # Get access type and mask
516
+ ace_type = ace[0][0] # Type
517
+ ace_mask = ace[1] # Access mask
518
+
519
+ # Determine share permissions
520
+ perm_list = []
521
+ if ace_mask & win32netcon.ACCESS_READ:
522
+ perm_list.append("Read")
523
+ if ace_mask & win32netcon.ACCESS_WRITE:
524
+ perm_list.append("Write")
525
+ if ace_mask & win32netcon.ACCESS_CREATE:
526
+ perm_list.append("Create")
527
+ if ace_mask & win32netcon.ACCESS_EXEC:
528
+ perm_list.append("Execute")
529
+ if ace_mask & win32netcon.ACCESS_DELETE:
530
+ perm_list.append("Delete")
531
+ if ace_mask & win32netcon.ACCESS_ATRIB:
532
+ perm_list.append("Change Attributes")
533
+ if ace_mask & win32netcon.ACCESS_PERM:
534
+ perm_list.append("Change Permissions")
535
+ if ace_mask & win32netcon.ACCESS_ALL:
536
+ perm_list = ["Full Control"]
537
+
538
+ # Only include ALLOW aces (ignore DENY for now)
539
+ if ace_type == ACCESS_ALLOWED_ACE_TYPE:
540
+ permissions[trustee] = perm_list
541
+
542
+ return permissions
543
+
544
+ except Exception as e:
545
+ logger.error(f"Failed to get share permissions for {server}\\{share}: {e}")
546
+ return None
547
+
548
+ def bypass_security_dialog(enabled: bool = True) -> bool:
549
+ """
550
+ Enable or disable the security warning dialog for UNC paths.
551
+
552
+ This modifies the registry to suppress security warnings when accessing UNC paths.
553
+
554
+ Args:
555
+ enabled: True to suppress warnings, False to restore default behavior.
556
+
557
+ Returns:
558
+ True if successful, False otherwise.
559
+ """
560
+ if not IS_WINDOWS:
561
+ logger.warning("Registry modifications are only available on Windows.")
562
+ return False
563
+
564
+ try:
565
+ import winreg
566
+
567
+ # Registry key for UNC security
568
+ key_path = r"Software\Microsoft\Windows\CurrentVersion\Policies\Network"
569
+ value_name = "ClassicSharing"
570
+
571
+ # Open or create the key
572
+ try:
573
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_WRITE)
574
+ except:
575
+ key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, key_path)
576
+
577
+ # Set the value
578
+ winreg.SetValueEx(key, value_name, 0, winreg.REG_DWORD, 1 if enabled else 0)
579
+ winreg.CloseKey(key)
580
+
581
+ logger.info(f"{'Enabled' if enabled else 'Disabled'} UNC security bypass")
582
+ return True
583
+
584
+ except Exception as e:
585
+ logger.error(f"Failed to modify registry for UNC security bypass: {e}")
586
+ return False