소스 검색

Windows DACLs

Antti Aalto 9 년 전
부모
커밋
7188a72939
7개의 변경된 파일337개의 추가작업 그리고 13개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 27 8
      borg/archive.py
  3. 6 2
      borg/archiver.py
  4. 1 1
      borg/constants.py
  5. 2 0
      borg/platform.py
  6. 291 0
      borg/platform_windows.pyx
  7. 9 2
      setup.py

+ 1 - 0
.gitignore

@@ -11,6 +11,7 @@ crypto.c
 platform_darwin.c
 platform_freebsd.c
 platform_linux.c
+platform_windows.c
 *.egg-info
 *.pyc
 *.pyo

+ 27 - 8
borg/archive.py

@@ -25,6 +25,8 @@ from .helpers import Chunk, Error, uid2user, user2uid, gid2group, group2gid, \
     CompressionDecider1, CompressionDecider2, CompressionSpec
 from .repository import Repository
 from .platform import acl_get, acl_set
+if sys.platform == 'win32':
+    from .platform import get_owner, set_owner
 from .chunker import Chunker
 from .hashindex import ChunkIndex, ChunkIndexEntry
 from .cache import ChunkListEntry
@@ -423,6 +425,11 @@ Number of files: {0.stats.nfiles}'''.format(
                     os.lchown(path, uid, gid)
             except OSError:
                 pass
+        else:
+            try:
+                set_owner(path, item[b'user'], safe_decode(item[b'uid']))
+            except OSError:
+                pass
         if sys.platform != 'win32':
             if fd:
                 os.fchmod(fd, item[b'mode'])
@@ -501,14 +508,26 @@ Number of files: {0.stats.nfiles}'''.format(
         del self.manifest.archives[self.name]
 
     def stat_attrs(self, st, path):
-        item = {
-            b'mode': st.st_mode,
-            b'uid': st.st_uid, b'user': uid2user(st.st_uid),
-            b'gid': st.st_gid, b'group': gid2group(st.st_gid),
-            b'atime': int_to_bigint(st.st_atime_ns),
-            b'ctime': int_to_bigint(st.st_ctime_ns),
-            b'mtime': int_to_bigint(st.st_mtime_ns),
-        }
+        item = {}
+        if sys.platform == 'win32':
+            owner = get_owner(path)
+            item = {
+                b'mode': st.st_mode,
+                b'uid': owner[1], b'user': owner[0],
+                b'gid': st.st_gid, b'group': gid2group(st.st_gid),
+                b'atime': int_to_bigint(st.st_atime_ns),
+                b'ctime': int_to_bigint(st.st_ctime_ns),
+                b'mtime': int_to_bigint(st.st_mtime_ns),
+            }
+        else:
+            item = {
+                b'mode': st.st_mode,
+                b'uid': st.st_uid, b'user': uid2user(st.st_uid),
+                b'gid': st.st_gid, b'group': gid2group(st.st_gid),
+                b'atime': int_to_bigint(st.st_atime_ns),
+                b'ctime': int_to_bigint(st.st_ctime_ns),
+                b'mtime': int_to_bigint(st.st_mtime_ns),
+            }
         if self.numeric_owner:
             item[b'user'] = item[b'group'] = None
         xattrs = xattr.get_all(path, follow_symlinks=False)

+ 6 - 2
borg/archiver.py

@@ -735,7 +735,10 @@ class Archiver:
                 elif args.short:
                     format = "{path}{NL}"
                 else:
-                    format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}"
+                    if sys.platform == 'win32':
+                        format = "{user:15} {size:8} {isomtime} {path}{extra}{NL}"
+                    else:
+                        format = "{mode} {user:6} {group:6} {size:8} {isomtime} {path}{extra}{NL}"
                 formatter = ItemFormatter(archive, format)
 
                 if not hasattr(sys.stdout, 'buffer'):
@@ -2009,7 +2012,8 @@ class Archiver:
 
     def prerun_checks(self, logger):
         check_extension_modules()
-        selftest(logger)
+        if sys.platform != 'win32':
+            selftest(logger)
 
     def run(self, args):
         os.umask(args.umask)  # early, before opening files

+ 1 - 1
borg/constants.py

@@ -1,7 +1,7 @@
 # this set must be kept complete, otherwise the RobustUnpacker might malfunction:
 ITEM_KEYS = set([b'path', b'source', b'rdev', b'chunks', b'hardlink_master',
                  b'mode', b'user', b'group', b'uid', b'gid', b'mtime', b'atime', b'ctime',
-                 b'xattrs', b'bsdflags', b'acl_nfs4', b'acl_access', b'acl_default', b'acl_extended', ])
+                 b'xattrs', b'bsdflags', b'acl_nfs4', b'acl_access', b'acl_default', b'acl_extended', b'win_dacl'])
 
 ARCHIVE_TEXT_KEYS = (b'name', b'comment', b'hostname', b'username', b'time', b'time_end')
 ITEM_TEXT_KEYS = (b'path', b'source', b'user', b'group')

+ 2 - 0
borg/platform.py

@@ -6,6 +6,8 @@ elif sys.platform.startswith('freebsd'):  # pragma: freebsd only
     from .platform_freebsd import acl_get, acl_set, API_VERSION
 elif sys.platform == 'darwin':  # pragma: darwin only
     from .platform_darwin import acl_get, acl_set, API_VERSION
+elif sys.platform == 'win32':  # pragma: windows only
+    from .platform_windows import acl_get, acl_set, API_VERSION, get_owner, set_owner
 else:  # pragma: unknown platform only
     API_VERSION = 2
 

+ 291 - 0
borg/platform_windows.pyx

@@ -0,0 +1,291 @@
+import json
+from libc.stddef cimport wchar_t
+from libc.stdint cimport uint16_t, uint32_t, uint64_t
+cimport cpython.array
+import array
+
+import platform
+from .helpers import safe_decode, safe_encode
+
+API_VERSION = 2
+
+cdef extern from 'stdlib.h':
+    void free(void* ptr)
+    void* malloc(size_t)
+    void* calloc(size_t, size_t)
+
+cdef extern from 'Python.h':
+    wchar_t* PyUnicode_AsWideCharString(object, Py_ssize_t *)
+    object PyUnicode_FromWideChar(const wchar_t*, Py_ssize_t)
+    void* PyMem_Malloc(int)
+    void PyMem_Free(void*)
+
+cdef extern from 'windows.h':
+    ctypedef int HLOCAL
+    ctypedef wchar_t* LPCTSTR
+    ctypedef char BYTE
+    ctypedef int HLOCAL
+    ctypedef uint32_t DWORD
+    ctypedef DWORD* LPDWORD
+    ctypedef int BOOL
+    ctypedef BYTE* PSID
+    struct _ACL:
+        uint16_t AceCount
+
+    HLOCAL LocalFree(HLOCAL)
+    DWORD GetLastError();
+    void SetLastError(DWORD)
+
+    BOOL InitializeSecurityDescriptor(BYTE*, DWORD)
+
+    BOOL LookupAccountNameW(LPCTSTR, LPCTSTR, PSID, LPDWORD, LPCTSTR, LPDWORD, LPDWORD)
+    BOOL GetSecurityDescriptorDacl(PSID, BOOL*, _ACL**, BOOL*)
+
+    cdef extern int ERROR_INSUFFICIENT_BUFFER
+    cdef extern int ERROR_INVALID_SID
+
+    cdef extern int OWNER_SECURITY_INFORMATION
+    cdef extern int GROUP_SECURITY_INFORMATION
+    cdef extern int DACL_SECURITY_INFORMATION
+    cdef extern int SACL_SECURITY_INFORMATION
+    cdef extern int LABEL_SECURITY_INFORMATION
+    cdef extern int ATTRIBUTE_SECURITY_INFORMATION
+    cdef extern int SCOPE_SECURITY_INFORMATION
+    cdef extern int BACKUP_SECURITY_INFORMATION
+    cdef extern int UNPROTECTED_SACL_SECURITY_INFORMATION
+    cdef extern int UNPROTECTED_DACL_SECURITY_INFORMATION
+    cdef extern int PROTECTED_SACL_SECURITY_INFORMATION
+    cdef extern int PROTECTED_DACL_SECURITY_INFORMATION
+
+    cdef extern int SECURITY_DESCRIPTOR_MIN_LENGTH
+
+cdef extern from 'accctrl.h':
+    ctypedef enum _SE_OBJECT_TYPE:
+        SE_FILE_OBJECT
+    ctypedef _SE_OBJECT_TYPE SE_OBJECT_TYPE
+    struct _TRUSTEE_W:
+        uint16_t TrusteeForm
+        uint16_t TrusteeType
+        LPCTSTR ptstrName
+
+    struct _EXPLICIT_ACCESS_W:
+        DWORD grfAccessPermissions
+        uint16_t grfAccessMode
+        DWORD grfInheritance
+        _TRUSTEE_W Trustee
+
+    cdef extern uint16_t TRUSTEE_IS_SID
+    cdef extern uint16_t TRUSTEE_IS_NAME
+    cdef extern uint16_t TRUSTEE_BAD_FORM
+
+    DWORD GetExplicitEntriesFromAclW(_ACL*, uint32_t*, _EXPLICIT_ACCESS_W**)
+
+cdef extern from 'Sddl.h':
+    ctypedef int* LPBOOL
+
+    BOOL GetFileSecurityW(LPCTSTR, int, PSID, DWORD, LPDWORD)
+    BOOL GetSecurityDescriptorOwner(PSID, PSID*, LPBOOL)
+    BOOL LookupAccountSidW(LPCTSTR, PSID, LPCTSTR, LPDWORD, LPCTSTR, LPDWORD, uint16_t*)
+    BOOL ConvertSidToStringSidW(PSID, LPCTSTR*)
+    BOOL ConvertStringSidToSidW(LPCTSTR, PSID*)
+    BOOL ConvertSecurityDescriptorToStringSecurityDescriptorW(BYTE*, DWORD, int, LPCTSTR*, int*)
+
+    cdef extern int SDDL_REVISION_1
+
+cdef extern from 'Aclapi.h':
+    ctypedef void* PACL
+    DWORD GetNamedSecurityInfoW(LPCTSTR, SE_OBJECT_TYPE, DWORD, PSID*, PSID*, PACL*, PACL*, _ACL**)
+    DWORD SetNamedSecurityInfoW(LPCTSTR, int, int, PSID, PSID, PACL, PACL)
+    DWORD SetEntriesInAclW(unsigned int, _EXPLICIT_ACCESS_W*, PACL, _ACL**)
+
+cdef PSID _get_file_security(filename, int request):
+    cdef DWORD length = 0
+    # N.B. This query may fail with ERROR_INVALID_FUNCTION
+    # for some filesystems.
+    cdef wchar_t* wcharfilename = PyUnicode_AsWideCharString(filename, NULL)
+    if wcharfilename == NULL:
+        print 'filename is NULL'
+    GetFileSecurityW(wcharfilename, request, NULL, 0, &length)
+    if GetLastError() == ERROR_INSUFFICIENT_BUFFER:
+        SetLastError(0)
+    else:
+        print '_get_file_security failed. Windows error:', GetLastError()
+        return NULL
+    cdef BYTE* sd = <BYTE*>malloc((length) * sizeof(BYTE))
+    GetFileSecurityW(wcharfilename, request, sd, length, &length)
+    PyMem_Free(wcharfilename)
+    return sd
+
+cdef PSID _get_security_descriptor_owner(PSID sd):
+    cdef PSID sid
+    cdef BOOL sid_defaulted
+    GetSecurityDescriptorOwner(sd, &sid, &sid_defaulted)
+    return (sid)
+
+cdef _look_up_account_sid(PSID sid):
+    cdef int SIZE = 256
+    cdef wchar_t* name = <wchar_t*>malloc((SIZE) * sizeof(wchar_t))
+    cdef wchar_t* domain = <wchar_t*>malloc((SIZE) * sizeof(wchar_t))
+    cdef DWORD cch_name = SIZE
+    cdef DWORD cch_domain = SIZE
+    cdef uint16_t sid_type = <uint16_t>0
+
+    cdef BOOL ret = LookupAccountSidW(NULL, sid, name, &cch_name, domain, &cch_domain, &sid_type)
+    if ret == 0:
+        lasterror = GetLastError()
+        if lasterror == 1332:
+            # Unknown (removed?) user or file from another windows installation
+            free(name)
+            free(domain)
+            return 'unknown', 'unknown', 0
+        else:
+            print 'Windows error:', lasterror
+
+    pystrName = PyUnicode_FromWideChar(name, -1)
+    pystrDomain = PyUnicode_FromWideChar(domain, -1)
+
+    free(name)
+    free(domain)
+    return pystrName, pystrDomain, <unsigned int>sid_type
+
+cdef sid2string(PSID sid):
+    cdef wchar_t* sidstr
+    ConvertSidToStringSidW(sid, &sidstr)
+    ret = PyUnicode_FromWideChar(sidstr, -1)
+    LocalFree(<HLOCAL>sidstr)
+    return ret
+
+def get_owner(path):
+    cdef int request = OWNER_SECURITY_INFORMATION
+    cdef BYTE* sd = _get_file_security(path, request)
+    if sd == NULL:
+        return 'unknown', 'S-1-0-0'
+    cdef PSID sid = _get_security_descriptor_owner(sd)
+    if sid == NULL:
+        print 'sid is null'
+        return 'unknown', 'S-1-0-0'
+    name, domain, sid_type = _look_up_account_sid(sid)
+    free(sd)
+    if domain and domain.lower() != platform.node().lower() and domain != 'BUILTIN':
+        return '{0}\\{1}'.format(domain, name), sid2string(sid)
+    else:
+        return name, sid2string(sid)
+
+def set_owner(path, owner, sidstring = None):
+    cdef PSID newsid
+    cdef wchar_t* temp
+    cdef DWORD sid_type = 0
+    cdef DWORD length = 0
+    cdef DWORD domainlength = 0
+    if sidstring is not None:
+        temp = PyUnicode_AsWideCharString(sidstring, NULL)
+        ConvertStringSidToSidW(temp, &newsid)
+    if sidstring is None or GetLastError() == ERROR_INVALID_SID:
+        temp = PyUnicode_AsWideCharString(owner, NULL)
+
+        LookupAccountNameW(NULL, temp, NULL, &length, NULL, &domainlength, &sid_type)
+
+        newsid = <PSID>malloc((length) * sizeof(BYTE))
+        SetLastError(0)
+        domainlength = 0
+        LookupAccountNameW(NULL, temp, newsid, &length, NULL, &domainlength, &sid_type)
+        if GetLastError() != 0:
+            PyMem_Free(temp)
+            return
+
+    PyMem_Free(temp)
+
+    cdef wchar_t* cstrPath = PyUnicode_AsWideCharString(path, NULL)
+    SetNamedSecurityInfoW(cstrPath, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, newsid, NULL, NULL, NULL)
+    PyMem_Free(cstrPath)
+    if length == 0:
+        LocalFree(<HLOCAL>newsid)
+    else:
+        free(newsid)
+
+def acl_get(path, item, st, numeric_owner=False):
+    cdef int request = DACL_SECURITY_INFORMATION
+
+    cdef BYTE* SD = _get_file_security(path, request)
+    if SD == NULL:
+        return
+
+    cdef BOOL daclFound
+    cdef _ACL* DACL
+    cdef BOOL DACLDefaulted
+    GetSecurityDescriptorDacl(SD, &daclFound, &DACL, &DACLDefaulted)
+
+    cdef uint32_t length
+    cdef _EXPLICIT_ACCESS_W* ACEs
+
+    GetExplicitEntriesFromAclW(DACL, &length, &ACEs)
+
+    pyDACL = []
+    cdef PSID newsid
+    cdef uint32_t domainlength
+    cdef uint32_t sid_type
+    for i in range(0, length):
+        permissions = None
+        name = ""
+        sidstr = ""
+        if ACEs[i].Trustee.TrusteeForm == TRUSTEE_IS_SID:
+            name, domain, type = _look_up_account_sid(<BYTE*>(ACEs[i].Trustee.ptstrName))
+            sidstr = sid2string(<PSID>(ACEs[i].Trustee.ptstrName))
+
+        elif ACEs[i].Trustee.TrusteeForm == TRUSTEE_IS_NAME:
+            sid_type = 0
+            domainlength = 0
+            LookupAccountNameW(NULL, ACEs[i].Trustee.ptstrName, NULL, &(length), NULL, &domainlength, &sid_type)
+
+            newsid = <PSID>malloc((length) * sizeof(BYTE))
+            domainlength = 0
+            LookupAccountNameW(NULL, ACEs[i].Trustee.ptstrName, newsid, &length, NULL, &domainlength, &sid_type)
+            trusteeName, domain, type = _look_up_account_sid(newsid)
+
+            name = trusteeName
+            sidstr = sid2string(newsid)
+            free(newsid)
+
+        elif ACEs[i].Trustee.TrusteeForm == TRUSTEE_BAD_FORM:
+            continue
+        permissions = {'user': {'name': name, 'sid': sidstr}, 'permissions': (ACEs[i].grfAccessPermissions, ACEs[i].grfAccessMode, ACEs[i].grfInheritance)}
+        pyDACL.append(permissions)
+    item[b'win_dacl'] = safe_encode(json.dumps(pyDACL))
+
+    free(SD)
+    LocalFree(<HLOCAL>ACEs)
+
+def acl_set(path, item, numeric_owner=False):
+    if b'dacl' not in item:
+        return
+
+    pyDACL = json.loads(safe_decode(item[b'win_dacl']))
+    cdef _EXPLICIT_ACCESS_W* ACEs = <_EXPLICIT_ACCESS_W*>calloc(sizeof(_EXPLICIT_ACCESS_W), len(pyDACL))
+    cdef wchar_t* temp
+    cdef PSID newsid
+    for i in range(0, len(pyDACL)):
+        if pyDACL[i]['user']['name'] == '' or numeric_owner:
+            ACEs[i].Trustee.TrusteeForm = TRUSTEE_IS_SID
+            temp = PyUnicode_AsWideCharString(pyDACL[i]['user']['sid'], NULL)
+            ConvertStringSidToSidW(temp, &newsid)
+            ACEs[i].Trustee.ptstrName = <LPCTSTR>newsid
+            PyMem_Free(temp)
+        else:
+            ACEs[i].Trustee.TrusteeForm = TRUSTEE_IS_NAME
+            ACEs[i].Trustee.ptstrName = PyUnicode_AsWideCharString(pyDACL[i]['user']['name'], NULL)
+        ACEs[i].grfAccessPermissions = pyDACL[i]['permissions'][0]
+        ACEs[i].grfAccessMode = pyDACL[i]['permissions'][1]
+        ACEs[i].grfInheritance = pyDACL[i]['permissions'][2]
+    cdef _ACL* newDACL
+    SetEntriesInAclW(len(pyDACL), ACEs, NULL, &newDACL)
+    cdef wchar_t* cstrPath = PyUnicode_AsWideCharString(path, NULL)
+    SetNamedSecurityInfoW(cstrPath, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, newDACL, NULL)
+
+    for i in range(0, len(pyDACL)):
+        if pyDACL[i]['user']['name'] == '' or numeric_owner:
+            LocalFree(<HLOCAL>ACEs[i].Trustee.ptstrName)
+        else:
+            PyMem_Free(ACEs[i].Trustee.ptstrName)
+    free(ACEs)
+    PyMem_Free(cstrPath)
+    LocalFree(<HLOCAL>newDACL)

+ 9 - 2
setup.py

@@ -44,6 +44,7 @@ hashindex_source = 'borg/hashindex.pyx'
 platform_linux_source = 'borg/platform_linux.pyx'
 platform_darwin_source = 'borg/platform_darwin.pyx'
 platform_freebsd_source = 'borg/platform_freebsd.pyx'
+platform_windows_source = 'borg/platform_windows.pyx'
 
 try:
     from Cython.Distutils import build_ext
@@ -52,7 +53,9 @@ try:
     class Sdist(sdist):
         def __init__(self, *args, **kwargs):
             for src in glob('borg/*.pyx'):
-                cython_compiler.compile(src, cython_compiler.default_options)
+                options = cython_compiler.default_options
+                options['language_level'] = 3
+                cython_compiler.compile(src, options)
             super().__init__(*args, **kwargs)
 
         def make_distribution(self):
@@ -64,6 +67,7 @@ try:
                 'borg/platform_linux.c',
                 'borg/platform_freebsd.c',
                 'borg/platform_darwin.c',
+                'borg/platform_windows.c',
             ])
             super().make_distribution()
 
@@ -79,10 +83,11 @@ except ImportError:
     platform_linux_source = platform_linux_source.replace('.pyx', '.c')
     platform_freebsd_source = platform_freebsd_source.replace('.pyx', '.c')
     platform_darwin_source = platform_darwin_source.replace('.pyx', '.c')
+    platform_windows_source = platform_windows_source.replace('.pyx', '.c')
     from distutils.command.build_ext import build_ext
     if not on_rtd and not all(os.path.exists(path) for path in [
         compress_source, crypto_source, chunker_source, hashindex_source,
-        platform_linux_source, platform_freebsd_source]):
+        platform_linux_source, platform_freebsd_source, platform_windows_source]):
         raise ImportError('The GIT version of Borg needs Cython. Install Cython or use a released version.')
 
 
@@ -312,6 +317,8 @@ if not on_rtd:
         ext_modules.append(Extension('borg.platform_freebsd', [platform_freebsd_source]))
     elif sys.platform == 'darwin':
         ext_modules.append(Extension('borg.platform_darwin', [platform_darwin_source]))
+    elif sys.platform == 'win32':
+        ext_modules.append(Extension('borg.platform_windows', [platform_windows_source]))
 
 
 def parse(root, describe_command=None):