Overview

ACL Structure

An NFSv4 ACL has the following structure:

  • An ACL (ACL) has zero or more entries.

  • An ACL entry, in short ACE, (Entry) has these attributes:

    • The type of the entry (entry_type).

      This field determines whether the ACE allows or denies access.

    • The tag type of the entry (tag_type).

      Together with the qualifier field this field controls for whom this ACE describes access permissions.

    • The principal (aka qualifier) of the entry (qualifier).

      The ID of a user or group for whom this ACE describes access permissions. This field makes sense only for entries of tag type ACL_USER or ACL_GROUP.

      Typically checked against the proper effective IDs of a process.

    • The set of permissions (permset).

      This field defines what kind of access the process matching this ACE has for accessing the associated file.

    • The set of flags (flagset).

      They are used to control and/or view the inheritance of ACLs.

  • A permission set (Permset) in an ACE has just methods to set and reset permission bits.

  • A flag set (Flagset) in an ACE has just methods to set and reset flag bits.

Implementation Note

The implementation is fairly low-level. The provided “abstractions” are leaky. Most operations are implemented by an immediate call into the native C implementation – the given classes are just thin wrappers around the OS level handles/pointers.

Usage Examples

Get an ACL from a file with and print it:

import nfs4acl

with nfs4acl.ACL(file="_tmp/mnt") as acl:
    # print in the standard form
    print(acl)

    # print with numeric IDs instead
    print(acl.to_any_text(
        options=nfs4acl.const.ACL_TEXT_NUMERIC_IDS))

Be sure to use the with construct or call close() to cleanup any associated OS resources.

Iterate over all entries:

from nfs4acl import *

with ACL(file="_tmp/mnt") as acl:
    for entry in acl:
        # do something
        ...

Append a new entry with:

from nfs4acl import *

entry = Entry(acl)

or:

from nfs4acl import *

entry = acl.append()

Prepend a new entry at the begin:

from nfs4acl import *

entry = Entry(acl, pos=0)

Delete an existing entry from an ACL:

# find the entry
...

# delete it
entry.delete()

Warning

After adding or deleting entries all existing entries (and dependent permission and flag sets) will be invalid.

Re-using such items afterwards is outright dangerous because these items are typically implemented as descriptors (i.e. pointers) into an existing ACL structure. After manipulating the number of entries an existing object can point to another entry or to nowhere (e.g. because the ACL got reallocated completely).

While the parent ACL instance is able to account for this, the package applies proper protections against re-use of child items after such operations.

from nfs4acl import *

with acl:

    # get the entries as list
    entries = list(acl)

    # all entries are valid
    for e in entries:
        assert e

    # insert a new one at the beginning
    new_entry = Entry(acl, pos=0)

    #
    # ATTENTION : Re-using entries is invalid because
    #             the underlying ACL structure got
    #             rearranged.
    #             The entries therefore have been
    #             invalidated by the package.
    #
    for e in entries:
        # now all the existing old entries are invalid
        assert not e
        #
        # and some other operation like this will raise
        # a ValueError exception
        #
        e.delete()

Make a file executable for the owner:

from nfs4acl import *

with ACL(fd=myfile.fileno()) as acl:
    entries = filter(
        lambda x: x.tag_type == const.ACL_USER_OBJ)
    if entries:
        for entry in entries:
            entry.permset.add(const.ACL_EXECUTE)
    else:
        #
        # new entry for the owner:
        # add one with just the execute permission
        #
        entry = acl.append()
        entry.tag_type = const.ACL_USER_OBJ
        entry.entry_type = const.ACL_ENTRY_TYPE_ALLOW
        ps = entry.permset
        ps.add(const.ACL_EXECUTE)
        #
        # NOTE: This changed the entry inline because it
        #       is a descriptor into the entry.
        #       No need to to
        #           entry.permset = ps
        #
    # apply this to the file
    acl.applyto(myfile)

Copy a complete ACL entry from another ACL:

import nfs4acl

with nfs4acl.ACL(file="source") as source:
    with nfs4acl.ACL(file="destination") as destination:
        # find the correct entry pair
        ...

        destination_entry.copy(source_entry)

        assert destination_entry == source_entry

Create a new empty ACL:

import nfs4acl

acl = nfs4acl.ACL()

# of course...
assert acl is not None

# ... but an ACL is logically False if there are no
#     entries
assert not acl

# you have to close an empty ACL also
acl.close()

Copy a flag set from another ACL:

import nfs4acl

with nfs4acl.ACL(file="source") as source:
    with nfs4acl.ACL(file="destination") as destination:
        # find the correct entry pair
        ...

        # copy: now the flagset setter comes really handy
        destination_entry.flagset = source_entry.flagset

        assert destination_entry.flagset == source_entry.flagset

Copy a complete ACL:

from nfs4acl import *

with ACL(file="source") as given_acl:
    with ACL(acl=given_acl) as the_copy:
        # do something with the copy
        ...

        # maybe you want it to save into a different file
        the_copy.applyto(file="destination")

If you just want to copy an ACL from one file to another file unchanged:

from nfs4acl import *

with ACL(file="source") as acl:
    acl.applyto(file="destination")

Create an ACL from a text representation:

from nfs4acl import *

with ACL(file="source") as given_acl:
    with ACL(text=str(given_acl)) as the_copy:
        # it is a real copy ...
        assert not (given_acl is the_copy)

        # .. but logically identical
        assert given_acl == the_copy

Unit Tests

Preparation

The unit tests need a clean scratch area in the filesystem with support for NFSv4 ACLs. On FreeBSD the ZFS filesystem is a perfect match for this.

To setup a clean filesystem test area with NFSv4 ACL support there is a helper script tests/zfstest.sh. It has to be run as user root with the current working directory in tests.

The script helps to create a file-backed ZFS pool named “nfs4aclpool” and helps to mount a temporary filesystem at mountpoint _tmp/mnt as scratch area for the unit tests.

Usage:

  1. Setup the pool:

    cd tests
    ./zfstest.sh pool
    

    It creates an empty imagefile in _tmp and uses this to create a file-backed ZFS pool named “nfs4aclpool”. A completely new image file will be created automatically. An existing file will be deleted first.

  2. Setup the filesystem at the mountpoint _tmp/mnt and make the real user who runs the tests the owner:

    cd tests
    ./zfstest.sh mount <userid>
    
  3. Develop and run unit tests …

  4. Unmount the working space:

    cd tests
    ./zfstest.sh umount
    
  5. Destroy the ZFS pool:

    cd tests
    ./zfstest.sh unpool
    

    Remove the image file if needed:

    rm _tmp/zfspool.img
    

Running

Unit tests are executed by:

cd tests
python test.py

Unit tests use Python’s built-in unittest package and have no other external dependencies.