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