netinvent/windows_tools

View on GitHub
windows_tools/file_utils/__init__.py

Summary

Maintainability
B
6 hrs
Test Coverage
#! /usr/bin/env python
#  -*- coding: utf-8 -*-
#
# This file is part of windows_tools module

"""
Windows file_utils can be used to get / set ownership on files / directories,
set ACLs, and recursively set ownership or ALCsy
Functions have been tested on NTFS an ReFS (Win10 x64 2004)

From my post here: https://stackoverflow.com/a/61041460/2635443

Versioning semantics:
    Major version: backward compatibility breaking changes
    Minor version: New functionality
    Patch version: Backwards compatible bug fixes

"""

__intname__ = "windows_tools.file_utils"
__author__ = "Orsiris de Jong"
__copyright__ = "Copyright (C) 2020 Orsiris de Jong"
__description__ = "Windows NTFS & ReFS file ownership and ACL handling functions"
__licence__ = "BSD 3 Clause"
__version__ = "0.3.0"
__build__ = "2021021601"

import logging
from typing import Tuple, Union, List, Iterable

import ntsecuritycon
import pywintypes

# pywin32
import win32api
import win32security
from ofunctions.file_utils import get_paths_recursive

from windows_tools.users import get_pysid

logger = logging.getLogger(__intname__)


def get_ownership(path: str) -> Tuple[str, str, int]:
    """
    Returns owner from Path

    :param path: (str) path to directory / file to check
    :return: (tuple) ('owner', 'group', 'privilege')
    """
    try:
        sec_descriptor = win32security.GetFileSecurity(
            path, win32security.OWNER_SECURITY_INFORMATION
        )
        sid = sec_descriptor.GetSecurityDescriptorOwner()
        return win32security.LookupAccountSid(None, sid)
    except pywintypes.error as exc:
        # Let's raise OSError so we don't need to import pywintypes in parent module to catch the exception
        raise OSError("Cannot get owner of file: {0}. {1}".format(path, exc))


def take_ownership(path: str, owner=None, force: bool = True) -> bool:
    """
    Set owner on NTFS & ReFS files / directories, see
    https://stackoverflow.com/a/61009508/2635443

    :param path: (str) path
    :param owner: (PySID) object that represents the security identifier.
                  If not set, current security identifier will be used
    :param force: (bool) Shall we force take ownership
    :return:
    """
    try:
        hToken = win32security.OpenThreadToken(
            win32api.GetCurrentThread(), win32security.TOKEN_ALL_ACCESS, True
        )

    except win32security.error:
        hToken = win32security.OpenProcessToken(
            win32api.GetCurrentProcess(), win32security.TOKEN_ALL_ACCESS
        )
    if owner is None:
        owner = win32security.GetTokenInformation(hToken, win32security.TokenOwner)
    prev_state = ()
    if force:
        new_state = [
            (
                win32security.LookupPrivilegeValue(None, name),
                win32security.SE_PRIVILEGE_ENABLED,
            )
            for name in (
                win32security.SE_TAKE_OWNERSHIP_NAME,
                win32security.SE_RESTORE_NAME,
            )
        ]
        prev_state = win32security.AdjustTokenPrivileges(hToken, False, new_state)
    try:
        sec_descriptor = win32security.SECURITY_DESCRIPTOR()
        sec_descriptor.SetSecurityDescriptorOwner(owner, False)
        win32security.SetFileSecurity(
            path, win32security.OWNER_SECURITY_INFORMATION, sec_descriptor
        )
    except pywintypes.error as exc:
        # Let's raise OSError so we don't need to import pywintypes in parent module to catch the exception
        raise OSError("Cannot take ownership of file: {0}. {1}.".format(path, exc))
    finally:
        if prev_state:
            win32security.AdjustTokenPrivileges(hToken, False, prev_state)
    return True


def take_ownership_recursive(path: str, owner=None) -> bool:
    """
    Recursive version of set_file_owner
    """

    def take_own(path):
        nonlocal owner
        try:
            take_ownership(path, owner=owner, force=True)
            return True
        except OSError:
            logger.error("Permission error on: {0}.".format(path))
            return False

    files = get_paths_recursive(path, fn_on_perm_error=take_own)

    result = True
    for file in files:
        res = take_ownership(file, force=True)
        if not res:
            result = False

    return result


def easy_permissions(permission):
    """
    Creates ntsecuritycon permission int bitmasks from simple RWX semmantics

    :param permission: (str) Simple R, RX, RWX, F  rights
    :return: (int) ntsecuritycon permission bitmask
    """
    permission = permission.upper()
    if permission == "R":
        return ntsecuritycon.GENERIC_READ
    if permission == "RX":
        return ntsecuritycon.GENERIC_READ | ntsecuritycon.GENERIC_EXECUTE
    if permission in ["RWX", "M"]:
        return (
            ntsecuritycon.GENERIC_READ
            | ntsecuritycon.GENERIC_WRITE
            | ntsecuritycon.GENERIC_EXECUTE
        )
    if permission == "F":
        return ntsecuritycon.GENERIC_ALL
    raise ValueError("Bogus easy permission")


def set_acls(
    path: str,
    user_list: Union[List[str], List[object]] = None,
    group_list: Union[List[str], List[object]] = None,
    owner: Union[str, object] = None,
    permissions: int = None,
    inherit: bool = False,
    inheritance: bool = False,
):
    """
    Set Windows DACL list

    ATTENTION: as of today, inherit=False copies the existing parent DACL list to the objects

    :param path: (str) path to directory/file
    :param user_sid_list: (list) str usernames or PySID objects
    :param group_sid_list: (list) str groupnames or PySID objects
    :param owner: (str) owner name or PySID obect
    :param permissions: (int) permission bitmask
    :param inherit: (bool) inherit parent permissions
    :param inheritance: (bool) apply ACL to sub folders and files
    """
    if inheritance:
        inheritance_flags = (
            win32security.CONTAINER_INHERIT_ACE | win32security.OBJECT_INHERIT_ACE
        )
    else:
        inheritance_flags = win32security.NO_INHERITANCE

    security_descriptor = {
        "AccessMode": win32security.GRANT_ACCESS,
        "AccessPermissions": 0,
        "Inheritance": inheritance_flags,
        "Trustee": {
            "TrusteeType": "",
            "TrusteeForm": win32security.TRUSTEE_IS_SID,
            "Identifier": "",
        },
    }

    # Now create a security descriptor for each user in the ACL list
    security_descriptors = []

    # If no user / group is defined, let's take current user
    if user_list is None and group_list is None:
        user_list = [get_pysid()]

    if user_list is not None:
        for sid in user_list:
            sid = get_pysid(sid)
            s = security_descriptor
            s["AccessPermissions"] = permissions
            s["Trustee"]["TrusteeType"] = win32security.TRUSTEE_IS_USER
            s["Trustee"]["Identifier"] = sid
            security_descriptors.append(s)

    if group_list is not None:
        for sid in group_list:
            sid = get_pysid(sid)
            s = security_descriptor
            s["AccessPermissions"] = permissions
            s["Trustee"]["TrusteeType"] = win32security.TRUSTEE_IS_GROUP
            s["Trustee"]["Identifier"] = sid
            security_descriptors.append(s)

    try:
        sec_descriptor = win32security.GetNamedSecurityInfo(
            path,
            win32security.SE_FILE_OBJECT,
            win32security.DACL_SECURITY_INFORMATION
            | win32security.UNPROTECTED_DACL_SECURITY_INFORMATION,
        )
    except pywintypes.error as exc:
        raise OSError("Failed to read security for file: {0}. {1}".format(path, exc))
    dacl = sec_descriptor.GetSecurityDescriptorDacl()
    dacl.SetEntriesInAcl(security_descriptors)

    security_information_flags = win32security.DACL_SECURITY_INFORMATION

    if not inherit:
        # PROTECTED_DACL_SECURITY_INFORMATION disables inheritance from parent
        security_information_flags = (
            security_information_flags
            | win32security.PROTECTED_DACL_SECURITY_INFORMATION
        )
    else:
        security_information_flags = (
            security_information_flags
            | win32security.UNPROTECTED_DACL_SECURITY_INFORMATION
        )

    # If we want to change owner, SetNamedSecurityInfo will need win32security.OWNER_SECURITY_INFORMATION in SECURITY_INFORMATION
    if owner is not None:
        security_information_flags = (
            security_information_flags | win32security.OWNER_SECURITY_INFORMATION
        )
        if isinstance(owner, str):
            owner = get_pysid(owner)

    try:
        # SetNamedSecurityInfo(path, object_type, security_information, owner, group, dacl, sacl)
        win32security.SetNamedSecurityInfo(
            path,
            win32security.SE_FILE_OBJECT,
            security_information_flags,
            owner,
            None,
            dacl,
            None,
        )
    except pywintypes.error as exc:
        raise OSError from exc


def get_paths_recursive_and_fix_permissions(
    path: str,
    owner: object = None,
    permissions: int = None,
    user_list: Union[List[str], List[object]] = None,
    **kwargs
) -> Iterable:
    """
    Allows all arguments from ofunctions.file_utils.get_paths_recursive()
    Works the same, except that while listing files and directories, we also fix permission issues
    """

    def fix_perms(path: str) -> None:
        nonlocal permissions
        nonlocal owner
        nonlocal user_list
        if not permissions:
            permissions = easy_permissions("F")
        logger.error("Permission error on: {0}.".format(path))
        try:
            set_acls(
                path,
                user_list=user_list,
                owner=owner,
                permissions=permissions,
                inheritance=False,
            )
        except OSError:
            # Lets force ownership change
            try:
                take_ownership(path, owner=owner, force=True)
                # Now try again
                set_acls(
                    path,
                    user_list=user_list,
                    owner=owner,
                    permissions=permissions,
                    inheritance=False,
                )
            except OSError as exc:
                logger.error("Cannot fix permission on {0}. {1}".format(path, exc))
                # Raise OSError since we cannot trust that the file list from get_files_recursive is complete
                raise OSError from OSError

    paths = get_paths_recursive(path, fn_on_perm_error=fix_perms, **kwargs)
    return paths