linux-nfs.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Chuck Lever <cel@kernel.org>
To: Calum Mackay <calum.mackay@oracle.com>
Cc: NeilBrown <neil@brown.name>, Jeff Layton <jlayton@kernel.org>,
	Olga Kornievskaia <okorniev@redhat.com>,
	Dai Ngo <dai.ngo@oracle.com>, Tom Talpey <tom@talpey.com>,
	<linux-nfs@vger.kernel.org>, Chuck Lever <chuck.lever@oracle.com>
Subject: [PATCH] Add tests for OPEN(create) with ACLs
Date: Sat, 15 Nov 2025 12:08:15 -0500	[thread overview]
Message-ID: <20251115170815.20696-1-cel@kernel.org> (raw)

From: Chuck Lever <chuck.lever@oracle.com>

Check that the server can attach an ACL when it creates a file.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 nfs4.1/server41tests/environment.py |  92 +++++++++++
 nfs4.1/server41tests/st_open.py     | 238 +++++++++++++++++++++++++++-
 2 files changed, 327 insertions(+), 3 deletions(-)

diff --git a/nfs4.1/server41tests/environment.py b/nfs4.1/server41tests/environment.py
index 48284e029634..0b39bce29870 100644
--- a/nfs4.1/server41tests/environment.py
+++ b/nfs4.1/server41tests/environment.py
@@ -277,6 +277,98 @@ debug_fail = False
 def fail(msg):
     raise testmod.FailureException(msg)
 
+def unsupported(msg):
+    raise testmod.UnsupportedException(msg)
+
+def access_mask_to_str(mask):
+    """Convert an ACE access_mask to a symbolic string representation"""
+    perms = [
+        (ACE4_READ_DATA, "READ_DATA"),
+        (ACE4_WRITE_DATA, "WRITE_DATA"),
+        (ACE4_APPEND_DATA, "APPEND_DATA"),
+        (ACE4_READ_NAMED_ATTRS, "READ_NAMED_ATTRS"),
+        (ACE4_WRITE_NAMED_ATTRS, "WRITE_NAMED_ATTRS"),
+        (ACE4_EXECUTE, "EXECUTE"),
+        (ACE4_DELETE_CHILD, "DELETE_CHILD"),
+        (ACE4_READ_ATTRIBUTES, "READ_ATTRIBUTES"),
+        (ACE4_WRITE_ATTRIBUTES, "WRITE_ATTRIBUTES"),
+        (ACE4_DELETE, "DELETE"),
+        (ACE4_READ_ACL, "READ_ACL"),
+        (ACE4_WRITE_ACL, "WRITE_ACL"),
+        (ACE4_WRITE_OWNER, "WRITE_OWNER"),
+        (ACE4_SYNCHRONIZE, "SYNCHRONIZE"),
+    ]
+    return " | ".join(name for bit, name in perms if mask & bit) or "(none)"
+
+def attr_bitmap_to_str(bitmap):
+    """Convert an attribute bitmap to a symbolic string representation"""
+    attrs = [
+        (FATTR4_SUPPORTED_ATTRS, "SUPPORTED_ATTRS"),
+        (FATTR4_TYPE, "TYPE"),
+        (FATTR4_FH_EXPIRE_TYPE, "FH_EXPIRE_TYPE"),
+        (FATTR4_CHANGE, "CHANGE"),
+        (FATTR4_SIZE, "SIZE"),
+        (FATTR4_LINK_SUPPORT, "LINK_SUPPORT"),
+        (FATTR4_SYMLINK_SUPPORT, "SYMLINK_SUPPORT"),
+        (FATTR4_NAMED_ATTR, "NAMED_ATTR"),
+        (FATTR4_FSID, "FSID"),
+        (FATTR4_UNIQUE_HANDLES, "UNIQUE_HANDLES"),
+        (FATTR4_LEASE_TIME, "LEASE_TIME"),
+        (FATTR4_RDATTR_ERROR, "RDATTR_ERROR"),
+        (FATTR4_ACL, "ACL"),
+        (FATTR4_ACLSUPPORT, "ACLSUPPORT"),
+        (FATTR4_ARCHIVE, "ARCHIVE"),
+        (FATTR4_CANSETTIME, "CANSETTIME"),
+        (FATTR4_CASE_INSENSITIVE, "CASE_INSENSITIVE"),
+        (FATTR4_CASE_PRESERVING, "CASE_PRESERVING"),
+        (FATTR4_CHOWN_RESTRICTED, "CHOWN_RESTRICTED"),
+        (FATTR4_FILEHANDLE, "FILEHANDLE"),
+        (FATTR4_FILEID, "FILEID"),
+        (FATTR4_FILES_AVAIL, "FILES_AVAIL"),
+        (FATTR4_FILES_FREE, "FILES_FREE"),
+        (FATTR4_FILES_TOTAL, "FILES_TOTAL"),
+        (FATTR4_FS_LOCATIONS, "FS_LOCATIONS"),
+        (FATTR4_HIDDEN, "HIDDEN"),
+        (FATTR4_HOMOGENEOUS, "HOMOGENEOUS"),
+        (FATTR4_MAXFILESIZE, "MAXFILESIZE"),
+        (FATTR4_MAXLINK, "MAXLINK"),
+        (FATTR4_MAXNAME, "MAXNAME"),
+        (FATTR4_MAXREAD, "MAXREAD"),
+        (FATTR4_MAXWRITE, "MAXWRITE"),
+        (FATTR4_MIMETYPE, "MIMETYPE"),
+        (FATTR4_MODE, "MODE"),
+        (FATTR4_NO_TRUNC, "NO_TRUNC"),
+        (FATTR4_NUMLINKS, "NUMLINKS"),
+        (FATTR4_OWNER, "OWNER"),
+        (FATTR4_OWNER_GROUP, "OWNER_GROUP"),
+        (FATTR4_QUOTA_AVAIL_HARD, "QUOTA_AVAIL_HARD"),
+        (FATTR4_QUOTA_AVAIL_SOFT, "QUOTA_AVAIL_SOFT"),
+        (FATTR4_QUOTA_USED, "QUOTA_USED"),
+        (FATTR4_RAWDEV, "RAWDEV"),
+        (FATTR4_SPACE_AVAIL, "SPACE_AVAIL"),
+        (FATTR4_SPACE_FREE, "SPACE_FREE"),
+        (FATTR4_SPACE_TOTAL, "SPACE_TOTAL"),
+        (FATTR4_SPACE_USED, "SPACE_USED"),
+        (FATTR4_SYSTEM, "SYSTEM"),
+        (FATTR4_TIME_ACCESS, "TIME_ACCESS"),
+        (FATTR4_TIME_ACCESS_SET, "TIME_ACCESS_SET"),
+        (FATTR4_TIME_BACKUP, "TIME_BACKUP"),
+        (FATTR4_TIME_CREATE, "TIME_CREATE"),
+        (FATTR4_TIME_DELTA, "TIME_DELTA"),
+        (FATTR4_TIME_METADATA, "TIME_METADATA"),
+        (FATTR4_TIME_MODIFY, "TIME_MODIFY"),
+        (FATTR4_TIME_MODIFY_SET, "TIME_MODIFY_SET"),
+        (FATTR4_MOUNTED_ON_FILEID, "MOUNTED_ON_FILEID"),
+        (FATTR4_SUPPATTR_EXCLCREAT, "SUPPATTR_EXCLCREAT"),
+        (FATTR4_SEC_LABEL, "SEC_LABEL"),
+        (FATTR4_XATTR_SUPPORT, "XATTR_SUPPORT"),
+    ]
+    result = []
+    for bit, name in attrs:
+        if bitmap & (1 << bit):
+            result.append(name)
+    return ", ".join(result) if result else "(none)"
+
 def check(res, stat=NFS4_OK, msg=None, warnlist=[]):
 
     if type(stat) is str:
diff --git a/nfs4.1/server41tests/st_open.py b/nfs4.1/server41tests/st_open.py
index 28540b59a8fe..2a06f301543a 100644
--- a/nfs4.1/server41tests/st_open.py
+++ b/nfs4.1/server41tests/st_open.py
@@ -1,11 +1,11 @@
 from .st_create_session import create_session
 from xdrdef.nfs4_const import *
 
-from .environment import check, fail, create_file, open_file, close_file, write_file, read_file
-from .environment import open_create_file_op
+from .environment import check, fail, unsupported, create_file, open_file, close_file, write_file, read_file
+from .environment import open_create_file_op, do_getattrdict, access_mask_to_str, attr_bitmap_to_str
 from xdrdef.nfs4_type import open_owner4, openflag4, createhow4, open_claim4
 from xdrdef.nfs4_type import creatverfattr, fattr4, stateid4, locker4, lock_owner4
-from xdrdef.nfs4_type import open_to_lock_owner4
+from xdrdef.nfs4_type import open_to_lock_owner4, nfsace4
 import nfs_ops
 op = nfs_ops.NFS4ops()
 import threading
@@ -17,6 +17,61 @@ def expect(res, seqid):
     if got != seqid:
         fail("Expected open_stateid.seqid == %i, got %i" % (seqid, got))
 
+def make_test_acl():
+    """Create a test ACL that maps cleanly to POSIX ACLs
+
+    Uses OWNER@, GROUP@, and EVERYONE@ to match POSIX user/group/other
+    structure, which helps servers that map NFSv4 ACLs to POSIX ACLs.
+
+    Includes both WRITE_DATA and APPEND_DATA for write permission, since
+    Linux NFS server's conservative NFSv4-to-POSIX mapping requires both
+    to grant POSIX write permission.
+    """
+    return [
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA | ACE4_WRITE_DATA | ACE4_APPEND_DATA | ACE4_READ_ACL,
+                b"OWNER@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"GROUP@"),
+        nfsace4(ACE4_ACCESS_ALLOWED_ACE_TYPE, 0,
+                ACE4_READ_DATA,
+                b"EVERYONE@")
+    ]
+
+def verify_acl(returned_acl, expected_acl):
+    """Verify that returned ACL contains expected ACEs
+
+    Server may add additional ACEs, but the requested ones must be present
+    with at least the requested permissions.
+    """
+    if len(returned_acl) < len(expected_acl):
+        fail("Returned ACL has fewer entries than requested: "
+             "expected at least %d, got %d" % (len(expected_acl), len(returned_acl)))
+
+    # Verify the ACEs we set are present (server may add additional ACEs)
+    for i, expected_ace in enumerate(expected_acl):
+        if i >= len(returned_acl):
+            fail("Missing ACE %d in returned ACL" % i)
+        returned_ace = returned_acl[i]
+        if returned_ace.type != expected_ace.type:
+            fail("ACE %d type mismatch: expected %d, got %d" %
+                 (i, expected_ace.type, returned_ace.type))
+        if returned_ace.who != expected_ace.who:
+            fail("ACE %d who mismatch: expected %s, got %s" %
+                 (i, expected_ace.who, returned_ace.who))
+        # Check that requested permissions are present (server may add more)
+        if (returned_ace.access_mask & expected_ace.access_mask) != expected_ace.access_mask:
+            missing = expected_ace.access_mask & ~returned_ace.access_mask
+            fail("ACE %d access_mask mismatch:\n"
+                 "  Expected: %s\n"
+                 "  Got:      %s\n"
+                 "  Missing:  %s" %
+                 (i,
+                  access_mask_to_str(expected_ace.access_mask),
+                  access_mask_to_str(returned_ace.access_mask),
+                  access_mask_to_str(missing)))
+
 def testSupported(t, env):
     """Do a simple OPEN create
 
@@ -195,3 +250,180 @@ def testCloseWithZeroSeqid(t, env):
     stateid.seqid = 0
     res = close_file(sess1, fh, stateid=stateid)
     check(res)
+
+def testSuppattrExclcreat(t, env):
+    """Check that FATTR4_SUPPATTR_EXCLCREAT is supported and valid
+
+    FLAGS: open all
+    CODE: OPEN12
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPORTED_ATTRS,
+                                                         FATTR4_SUPPATTR_EXCLCREAT]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+
+    if FATTR4_SUPPORTED_ATTRS not in attrs_info:
+        fail("Server did not return FATTR4_SUPPORTED_ATTRS")
+
+    # Check if SUPPATTR_EXCLCREAT is in the supported attributes
+    supported = attrs_info[FATTR4_SUPPORTED_ATTRS]
+    if not (supported & (1 << FATTR4_SUPPATTR_EXCLCREAT)):
+        unsupported("Server does not support FATTR4_SUPPATTR_EXCLCREAT")
+
+    # If supported, check that it was returned
+    if FATTR4_SUPPATTR_EXCLCREAT not in attrs_info:
+        fail("FATTR4_SUPPATTR_EXCLCREAT not returned even though it "
+             "appears in FATTR4_SUPPORTED_ATTRS")
+
+def testSuppattrExclcreatSubset(t, env):
+    """FATTR4_SUPPATTR_EXCLCREAT must be subset of SUPPORTED_ATTRS
+
+    FLAGS: open all
+    CODE: OPEN13
+    DEPEND: OPEN12
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPORTED_ATTRS,
+                                                         FATTR4_SUPPATTR_EXCLCREAT]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+
+    supported = attrs_info[FATTR4_SUPPORTED_ATTRS]
+    excl_supported = attrs_info[FATTR4_SUPPATTR_EXCLCREAT]
+
+    # SUPPATTR_EXCLCREAT must be a subset of SUPPORTED_ATTRS
+    # i.e., every bit set in excl_supported must also be set in supported
+    invalid = excl_supported & ~supported
+    if invalid != 0:
+        fail("FATTR4_SUPPATTR_EXCLCREAT contains attributes not in "
+             "FATTR4_SUPPORTED_ATTRS:\n"
+             "  Invalid attributes: %s" % attr_bitmap_to_str(invalid))
+
+def testACLSupported(t, env):
+    """Check that server supports FATTR4_ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN8a
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPORTED_ATTRS]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+    if FATTR4_SUPPORTED_ATTRS not in attrs_info:
+        fail("Server did not return FATTR4_SUPPORTED_ATTRS")
+    supported = attrs_info[FATTR4_SUPPORTED_ATTRS]
+    if not (supported & (1 << FATTR4_ACL)):
+        unsupported("Server does not support FATTR4_ACL attribute")
+
+def testACLExclusiveSupported(t, env):
+    """Check that server supports setting ACL during EXCLUSIVE4_1 create
+
+    FLAGS: open acl all
+    CODE: OPEN8b
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    res = sess.compound([op.putrootfh(),
+                        op.getattr(nfs4lib.list2bitmap([FATTR4_SUPPATTR_EXCLCREAT]))])
+    check(res)
+    attrs_info = res.resarray[-1].obj_attributes
+    if FATTR4_SUPPATTR_EXCLCREAT not in attrs_info:
+        unsupported("Server does not support FATTR4_SUPPATTR_EXCLCREAT")
+    excl_supported = attrs_info[FATTR4_SUPPATTR_EXCLCREAT]
+    if not (excl_supported & (1 << FATTR4_ACL)):
+        unsupported("Server does not support setting FATTR4_ACL during "
+                    "EXCLUSIVE4_1 create")
+
+def testOpenCreateWithACLUnchecked(t, env):
+    """OPEN with UNCHECKED4 CREATE setting NFSv4 ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN9
+    DEPEND: OPEN8a
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = make_test_acl()
+
+    # Create file with ACL attribute using UNCHECKED4 mode
+    attrs = {FATTR4_MODE: 0o644, FATTR4_ACL: acl}
+    res = create_file(sess, env.testname(t), attrs=attrs, mode=UNCHECKED4)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify the ACL was set correctly by reading it back
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+
+    verify_acl(attrs_dict[FATTR4_ACL], acl)
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateWithACLGuarded(t, env):
+    """OPEN with GUARDED4 CREATE setting NFSv4 ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN10
+    DEPEND: OPEN8a
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = make_test_acl()
+
+    # Create file with ACL attribute using GUARDED4 mode
+    attrs = {FATTR4_MODE: 0o644, FATTR4_ACL: acl}
+    res = create_file(sess, env.testname(t), attrs=attrs, mode=GUARDED4)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify the ACL was set correctly by reading it back
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+
+    verify_acl(attrs_dict[FATTR4_ACL], acl)
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
+
+def testOpenCreateWithACLExclusive(t, env):
+    """OPEN with EXCLUSIVE4_1 CREATE setting NFSv4 ACL attribute
+
+    FLAGS: open acl all
+    CODE: OPEN11
+    DEPEND: OPEN8b
+    """
+    sess = env.c1.new_client_session(env.testname(t))
+    acl = make_test_acl()
+
+    # Create file with ACL attribute using EXCLUSIVE4_1 mode
+    # EXCLUSIVE4_1 allows attributes to be set atomically with create
+    # Don't set MODE with ACL - let the ACL determine permissions
+    attrs = {FATTR4_ACL: acl}
+    verifier = b"testverif"
+    res = create_file(sess, env.testname(t), attrs=attrs,
+                      mode=EXCLUSIVE4_1, verifier=verifier)
+    check(res)
+    expect(res, seqid=1)
+
+    fh = res.resarray[-1].object
+    stateid = res.resarray[-2].stateid
+
+    # Verify the ACL was set correctly by reading it back
+    attrs_dict = do_getattrdict(sess, fh, [FATTR4_ACL])
+    if FATTR4_ACL not in attrs_dict:
+        fail("ACL attribute not returned after OPEN with CREATE")
+
+    verify_acl(attrs_dict[FATTR4_ACL], acl)
+
+    res = close_file(sess, fh, stateid=stateid)
+    check(res)
-- 
2.51.0


             reply	other threads:[~2025-11-15 17:08 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-15 17:08 Chuck Lever [this message]
2025-11-16  0:05 ` [PATCH] Add tests for OPEN(create) with ACLs Calum Mackay

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251115170815.20696-1-cel@kernel.org \
    --to=cel@kernel.org \
    --cc=calum.mackay@oracle.com \
    --cc=chuck.lever@oracle.com \
    --cc=dai.ngo@oracle.com \
    --cc=jlayton@kernel.org \
    --cc=linux-nfs@vger.kernel.org \
    --cc=neil@brown.name \
    --cc=okorniev@redhat.com \
    --cc=tom@talpey.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).