Linux NFS development
 help / color / mirror / Atom feed
* [PATCH] Add tests for OPEN(create) with ACLs
@ 2025-11-15 17:08 Chuck Lever
  2025-11-16  0:05 ` Calum Mackay
  0 siblings, 1 reply; 2+ messages in thread
From: Chuck Lever @ 2025-11-15 17:08 UTC (permalink / raw)
  To: Calum Mackay
  Cc: NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo, Tom Talpey,
	linux-nfs, Chuck Lever

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


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* Re: [PATCH] Add tests for OPEN(create) with ACLs
  2025-11-15 17:08 [PATCH] Add tests for OPEN(create) with ACLs Chuck Lever
@ 2025-11-16  0:05 ` Calum Mackay
  0 siblings, 0 replies; 2+ messages in thread
From: Calum Mackay @ 2025-11-16  0:05 UTC (permalink / raw)
  To: Chuck Lever
  Cc: Calum Mackay, NeilBrown, Jeff Layton, Olga Kornievskaia, Dai Ngo,
	Tom Talpey, linux-nfs, Chuck Lever

On 15/11/2025 5:08 pm, Chuck Lever wrote:
> From: Chuck Lever <chuck.lever@oracle.com>
> 
> Check that the server can attach an ACL when it creates a file.

Thanks Chuck, I'll add these to my backlog, and get them in asap.

I can also use the unsupported idea elsewhere, e.g. as we discussed 
relaating to the DELEG24/25 failures, in earlier kernels, for 
unsupported FATTR4_OPEN_ARGUMENTS.

cheers,
c.


> 
> 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)

-- 
Calum Mackay
Linux Kernel Engineering
Oracle Linux and Virtualisation


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2025-11-16  0:06 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-15 17:08 [PATCH] Add tests for OPEN(create) with ACLs Chuck Lever
2025-11-16  0:05 ` Calum Mackay

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox