Canonical Voices

Posts tagged with 'acl'

mandel

On of the features that I really like from Ubuntu One is the ability to have Read Only shares that will allow me to share files with some of my friends without them having the chance to change my files. In order to support that in a more explicit way on Windows we needed to be able to change the ACEs of an ACL from a file to stop the user from changing the files. In reality there is no need to change the ACEs since the server will ensure that the files are not changed, but as with python, is better to be explicit that to be implicit.

Our solution has the following details:

  • The file system is not using FAT.
  • We assume that the average user does not change the ACEs of a file usually.
  • If the user changes the ACEs he does not add any deny ACE.
  • We want to keep the already present ACEs.

The idea is very simple, we will add a ACE for the path that will remove the user the write rights so that we cannot edit/rename/delete a file and that he can only list the directories. The full code is the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
USER_SID = LookupAccountName("", GetUserName())[0]
 
def _add_deny_ace(path, rights):
    """Remove rights from a path for the given groups."""
    if not os.path.exists(path):
        raise WindowsError('Path %s could not be found.' % path)
 
    if rights is not None:
        security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
        dacl = security_descriptor.GetSecurityDescriptorDacl()
        # set the attributes of the group only if not null
        dacl.AddAccessDeniedAceEx(ACL_REVISION_DS,
                CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE, rights,
                USER_SID)
        security_descriptor.SetSecurityDescriptorDacl(1, dacl, 0)
        SetFileSecurity(path, DACL_SECURITY_INFORMATION, security_descriptor)
 
 
def _remove_deny_ace(path):
    """Remove the deny ace for the given groups."""
    if not os.path.exists(path):
        raise WindowsError('Path %s could not be found.' % path)
    security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
    dacl = security_descriptor.GetSecurityDescriptorDacl()
    # if we delete an ace in the acl the index is outdated and we have
    # to ensure that we do not screw it up. We keep the number of deleted
    # items to update accordingly the index.
    num_delete = 0
    for index in range(0, dacl.GetAceCount()):
        ace = dacl.GetAce(index - num_delete)
        # check if the ace is for the user and its type is 1, that means
        # is a deny ace and we added it, lets remove it
        if USER_SID == ace[2] and ace[0][0] == 1:
            dacl.DeleteAce(index - num_delete)
            num_delete += 1
    security_descriptor.SetSecurityDescriptorDacl(1, dacl, 0)
    SetFileSecurity(path, DACL_SECURITY_INFORMATION, security_descriptor)
 
 
def set_no_rights(path):
    """Set the rights for 'path' to be none.
 
    Set the groups to be empty which will remove all the rights of the file.
 
    """
    os.chmod(path, 0o000)
    rights = FILE_ALL_ACCESS
    _add_deny_ace(path, rights)
 
 
def set_file_readonly(path):
    """Change path permissions to readonly in a file."""
    # we use the win32 api because chmod just sets the readonly flag and
    # we want to have more control over the permissions
    rights = FILE_WRITE_DATA | FILE_APPEND_DATA | FILE_GENERIC_WRITE
    # the above equals more or less to 0444
    _add_deny_ace(path, rights)
 
 
def set_file_readwrite(path):
    """Change path permissions to readwrite in a file."""
    # the above equals more or less to 0774
    _remove_deny_ace(path)
    os.chmod(path, stat.S_IWRITE)
 
 
def set_dir_readonly(path):
    """Change path permissions to readonly in a dir."""
    rights = FILE_WRITE_DATA | FILE_APPEND_DATA
 
    # the above equals more or less to 0444
    _add_deny_ace(path, rights)
 
 
def set_dir_readwrite(path):
    """Change path permissions to readwrite in a dir.
 
    Helper that receives a windows path.
 
    """
    # the above equals more or less to 0774
    _remove_deny_ace(path)
    # remove the read only flag
    os.chmod(path, stat.S_IWRITE)

Adding the Deny ACE

The idea of the code is very simple, we will add a Deny ACE to the path so that the user cannot write it. The Deny ACE is different if it is a file or a directory since we want the user to be able to list the contents of a directory.

3
4
5
6
7
8
9
10
11
12
13
14
15
16
def _add_deny_ace(path, rights):
    """Remove rights from a path for the given groups."""
    if not os.path.exists(path):
        raise WindowsError('Path %s could not be found.' % path)
 
    if rights is not None:
        security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
        dacl = security_descriptor.GetSecurityDescriptorDacl()
        # set the attributes of the group only if not null
        dacl.AddAccessDeniedAceEx(ACL_REVISION_DS,
                CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE, rights,
                USER_SID)
        security_descriptor.SetSecurityDescriptorDacl(1, dacl, 0)
        SetFileSecurity(path, DACL_SECURITY_INFORMATION, security_descriptor)

Remove the Deny ACE

Very similar to the above but doing the opposite, lets remove the Deny ACES present for the current user. If you notice we store how many we removed, the reason is simple, if we remove an ACE the index is no longer valid so we have to calculate the correct one by knowing how many we removed.

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def _remove_deny_ace(path):
    """Remove the deny ace for the given groups."""
    if not os.path.exists(path):
        raise WindowsError('Path %s could not be found.' % path)
    security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
    dacl = security_descriptor.GetSecurityDescriptorDacl()
    # if we delete an ace in the acl the index is outdated and we have
    # to ensure that we do not screw it up. We keep the number of deleted
    # items to update accordingly the index.
    num_delete = 0
    for index in range(0, dacl.GetAceCount()):
        ace = dacl.GetAce(index - num_delete)
        # check if the ace is for the user and its type is 1, that means
        # is a deny ace and we added it, lets remove it
        if USER_SID == ace[2] and ace[0][0] == 1:
            dacl.DeleteAce(index - num_delete)
            num_delete += 1
    security_descriptor.SetSecurityDescriptorDacl(1, dacl, 0)
    SetFileSecurity(path, DACL_SECURITY_INFORMATION, security_descriptor)

Implement access

Our access implementation takes into account the Deny ACE added to ensure that we do not only look at the flags.

def access(path):
    """Return if the path is at least readable."""
    # lets consider the access on an illegal path to be a special case
    # since that will only occur in the case where the user created the path
    # for a file to be readable it has to be readable either by the user or
    # by the everyone group
    # XXX: ENOPARSE ^ (nessita)
    if not os.path.exists(path):
        return False
    security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
    dacl = security_descriptor.GetSecurityDescriptorDacl()
    for index in range(0, dacl.GetAceCount()):
        # add the sid of the ace if it can read to test that we remove
        # the r bitmask and test if the bitmask is the same, if not, it means
        # we could read and removed it.
        ace = dacl.GetAce(index)
        if USER_SID == ace[2] and ace[0][0] == 1:
            # check wich access is denied
            if ace[1] | FILE_GENERIC_READ == ace[1] or\
               ace[1] | FILE_ALL_ACCESS == ace[1]:
                return False
    return True

Implement can_write

The following code is similar to access but checks if we have a readonly file.

def can_write(path):
    """Return if the path is at least readable."""
    # lets consider the access on an illegal path to be a special case
    # since that will only occur in the case where the user created the path
    # for a file to be readable it has to be readable either by the user or
    # by the everyone group
    # XXX: ENOPARSE ^ (nessita)
    if not os.path.exists(path):
        return False
    security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
    dacl = security_descriptor.GetSecurityDescriptorDacl()
    for index in range(0, dacl.GetAceCount()):
        # add the sid of the ace if it can read to test that we remove
        # the r bitmask and test if the bitmask is the same, if not, it means
        # we could read and removed it.
        ace = dacl.GetAce(index)
        if USER_SID == ace[2] and ace[0][0] == 1:
            if ace[1] | FILE_GENERIC_WRITE == ace[1] or\
               ace[1] | FILE_WRITE_DATA == ace[1] or\
               ace[1] | FILE_APPEND_DATA == ace[1] or\
               ace[1] | FILE_ALL_ACCESS == ace[1]:
                # check wich access is denied
                return False
    return True

And that is about it, I hope it helps other projects :D

Read more
mandel

While working on making the Ubuntu One code more multiplatform I founded myself having to write some code that would set the attributes of a file on Windows. Ideally os.chmod would do the trick, but of course this is windows, and it is not fully supported. According to the python documentation:

Note: Although Windows supports chmod(), you can only set the file’s read-only flag with it (via the stat.S_IWRITE and stat.S_IREAD constants or a corresponding integer value). All other bits are ignored.

Grrrreat… To solve this issue I have written a small function that will allow to set the attributes of a file by using the win32api and win32security modules. This solves partially the issues since 0444 and others cannot be perfectly map to the Windows world. In my code I have made the assumption that using the groups ‘Everyone’, ‘Administrators’ and the user name would be close enough for our use cases.

Here is the code in case anyone has to go through this:

from win32api import MoveFileEx, GetUserName
 
from win32file import (
    MOVEFILE_COPY_ALLOWED,
    MOVEFILE_REPLACE_EXISTING,
    MOVEFILE_WRITE_THROUGH
)
from win32security import (
    LookupAccountName,
    GetFileSecurity,
    SetFileSecurity,
    ACL,
    DACL_SECURITY_INFORMATION,
    ACL_REVISION
)
from ntsecuritycon import (
    FILE_ALL_ACCESS,
    FILE_GENERIC_EXECUTE,
    FILE_GENERIC_READ,
    FILE_GENERIC_WRITE,
    FILE_LIST_DIRECTORY
)
 
EVERYONE_GROUP = 'Everyone'
ADMINISTRATORS_GROUP = 'Administrators'
 
def _get_group_sid(group_name):
    """Return the SID for a group with the given name."""
    return LookupAccountName('', group_name)[0]
 
 
def _set_file_attributes(path, groups):
    """Set file attributes using the wind32api."""
    security_descriptor = GetFileSecurity(path, DACL_SECURITY_INFORMATION)
    dacl = ACL()
    for group_name in groups:
        # set the attributes of the group only if not null
        if groups[group_name]:
            group_sid = _get_group_sid(group_name)
            dacl.AddAccessAllowedAce(ACL_REVISION, groups[group_name],
                group_sid)
    # the dacl has all the info of the dff groups passed in the parameters
    security_descriptor.SetSecurityDescriptorDacl(1, dacl, 0)
    SetFileSecurity(path, DACL_SECURITY_INFORMATION, security_descriptor)
 
def set_file_readonly(path):
    """Change path permissions to readonly in a file."""
    # we use the win32 api because chmod just sets the readonly flag and
    # we want to have imore control over the permissions
    groups = {}
    groups[EVERYONE_GROUP] = FILE_GENERIC_READ
    groups[ADMINISTRATORS_GROUP] = FILE_GENERIC_READ
    groups[GetUserName()] = FILE_GENERIC_READ
    # the above equals more or less to 0444
    _set_file_attributes(path, groups)

For those who might want to remove the read access from a group, you just have to not pass the group in the groups parameter which would remove the group from the security descriptor.

Read more