diff --git a/TODO.md b/TODO.md index 2cdbac7..09e65bb 100644 --- a/TODO.md +++ b/TODO.md @@ -440,6 +440,13 @@ AFP Set File Information metadata-rights convergence: and Modify timestamp continues to route through `nw_utime_node()`. The new check only covers metadata that has to remain AFP-specific, keeping AFP as an Apple-facing adapter over mars_nwe policy rather than a parallel file server. +- The Linux AFP smoke suite now has optional negative coverage for this policy + gate. With `--readonly-user NOPASSUSER --readonly-no-password`, the suite can + run FinderInfo, Invisible, and System writes as a user that is expected to lack + Modify rights and assert completion `0x8c`. With `--prepare-readonly-rights`, + the suite uses the existing ncpfs `nwgrant`/`nwrevoke` trustee utilities to + grant only read/file-scan rights (`[RF]` by default) before the negative probe + and revoke the explicit trustee assignment afterwards. Endpoint order: diff --git a/tests/linux/README.md b/tests/linux/README.md index bca2975..9fc20f6 100644 --- a/tests/linux/README.md +++ b/tests/linux/README.md @@ -87,6 +87,28 @@ collected separately. Use `--stop-on-failure` for strict bisect-style runs; by default the script keeps going so one failing endpoint does not hide later AFP output from the report. +The suite can optionally exercise the Modify-rights negative path with a second +user. For a no-password test user such as `NOPASSUSER`, run from the build +`tests/linux` directory: + +```sh +./afp_smoke_suite.sh \ + -S MARS -U SUPERVISOR -P secret \ + --path SYS:PUBLIC/pmdflts.ini \ + --unix-path /var/mars_nwe/SYS/public/pmdflts.ini \ + --readonly-user NOPASSUSER --readonly-no-password \ + --prepare-readonly-rights \ + --out /tmp/mars-afp-smoke.txt +``` + +`--prepare-readonly-rights` uses the standard ncpfs trustee utilities instead +of ad-hoc test NCPs: it calls `nwrevoke` to remove any explicit assignment for +the readonly user on the smoke file, then `nwgrant -r '[RF]'` to grant read and +file-scan rights without Modify. After the negative probes it runs `nwrevoke` +again so the file returns to inherited rights. Use this only on smoke files or +paths where removing an explicit trustee assignment for the readonly test user +is acceptable. + AFP metadata writes and NetWare Modify rights: FinderInfo and AFP-only attribute metadata are stored in `org.mars-nwe.afp.*` diff --git a/tests/linux/afp_set_file_info_smoke.c b/tests/linux/afp_set_file_info_smoke.c index a4d34f6..aca3cb6 100644 --- a/tests/linux/afp_set_file_info_smoke.c +++ b/tests/linux/afp_set_file_info_smoke.c @@ -38,7 +38,7 @@ static void usage(const char *prog) { fprintf(stderr, - "Usage: %s [--afp09|--afp20] [--allow-invalid-namespace] [--allow-invalid-path] " + "Usage: %s [--afp09|--afp20] [--expect-completion CODE] [--allow-invalid-namespace] [--allow-invalid-path] " "[--volume N] [--entry-id ID] [--type FOUR] [--creator FOUR] " "[--invisible|--clear-invisible|--system|--clear-system|--archive|--clear-archive] " "[--mtime-epoch SECONDS] [--finder-info-only|--attributes-only|--timestamp-only] " @@ -52,8 +52,9 @@ static void usage(const char *prog) " %s -S MARS -U SUPERVISOR -P secret --invisible --attributes-only SYS:PUBLIC/pmdflts.ini\n" " %s -S MARS -U SUPERVISOR -P secret --archive --attributes-only SYS:PUBLIC/pmdflts.ini\n" " %s -S MARS -U SUPERVISOR -P secret --mtime-epoch 1700000000 --timestamp-only SYS:PUBLIC/pmdflts.ini\n" - " %s --allow-invalid-namespace -S MARS SYS:PUBLIC/pmdflts.ini\n", - prog, prog, prog, prog, prog, prog); + " %s --allow-invalid-namespace -S MARS SYS:PUBLIC/pmdflts.ini\n" + " %s --expect-completion 0x8c -S MARS -U NOPASSUSER -n --finder-info-only SYS:PUBLIC/pmdflts.ini\n", + prog, prog, prog, prog, prog, prog, prog); } static int parse_u32(const char *text, uint32_t *value) @@ -160,6 +161,7 @@ int main(int argc, char **argv) const char *path = NULL; int allow_invalid_namespace = 0; int allow_invalid_path = 0; + int expect_completion = -1; uint32_t volume_number = 0; uint32_t entry_id = 0; uint8_t finder_info[32]; @@ -200,6 +202,14 @@ int main(int argc, char **argv) set_subfunction = AFP_SET_FILE_INFORMATION; } else if (!strcmp(argv[i], "--afp20")) { set_subfunction = AFP20_SET_FILE_INFORMATION; + } else if (!strcmp(argv[i], "--expect-completion")) { + uint32_t completion; + if (++i >= argc || parse_u32(argv[i], &completion) || completion > 255) { + fprintf(stderr, "invalid --expect-completion value\n"); + ncp_close(conn); + return 2; + } + expect_completion = (int)completion; } else if (!strcmp(argv[i], "--allow-invalid-namespace")) { allow_invalid_namespace = 1; } else if (!strcmp(argv[i], "--allow-invalid-path")) { @@ -347,6 +357,20 @@ int main(int argc, char **argv) request, data_off, NULL); + if (expect_completion >= 0) { + if ((((unsigned int)err) & 0xff) == (unsigned int)expect_completion) { + printf("AFP Set File Information returned expected completion 0x%02x: subfunction=0x%02x path=%s bitmap=0x%04x\n", + expect_completion, set_subfunction, path, request_mask); + ncp_close(conn); + return 0; + } + fprintf(stderr, + "AFP Set File Information expected completion 0x%02x but got 0x%02x (%u): subfunction=0x%02x path=%s bitmap=0x%04x\n", + expect_completion, (unsigned int)err & 0xff, (unsigned int)err, + set_subfunction, path, request_mask); + ncp_close(conn); + return 1; + } if (err == NWE_INVALID_NAMESPACE && allow_invalid_namespace) { printf("AFP Set File Information returned invalid namespace as expected for path=%s\n", path); ncp_close(conn); diff --git a/tests/linux/afp_smoke_suite.sh b/tests/linux/afp_smoke_suite.sh index 6820918..c939e69 100755 --- a/tests/linux/afp_smoke_suite.sh +++ b/tests/linux/afp_smoke_suite.sh @@ -18,6 +18,11 @@ OUT_FILE="" FINDER_TYPE="TEXT" FINDER_CREATOR="MARS" TIMESTAMP_EPOCH="1700000000" +READONLY_USER="" +READONLY_PASSWORD="" +READONLY_NO_PASSWORD=0 +PREPARE_READONLY_RIGHTS=0 +READONLY_RIGHTS="[RF]" KEEP_GOING=1 CAPTURE_LOG=1 @@ -33,6 +38,11 @@ Options: --type FOURCC FinderInfo type written by Set File Info (default: $FINDER_TYPE) --creator FOURCC FinderInfo creator written by Set File Info (default: $FINDER_CREATOR) --mtime-epoch SECONDS AFP modify timestamp to write (default: $TIMESTAMP_EPOCH) + --readonly-user USER Optional user expected to lack Modify rights + --readonly-password PASS Password for --readonly-user + --readonly-no-password Use ncpfs -n for --readonly-user + --prepare-readonly-rights Prepare/revoke explicit rights around the negative test + --readonly-rights RIGHTS Rights granted by --prepare-readonly-rights (default: $READONLY_RIGHTS) --no-log Do not tail/grep the server log --stop-on-failure Stop after the first failing smoke command -h, --help Show this help @@ -65,6 +75,16 @@ while [ $# -gt 0 ]; do FINDER_CREATOR=$2; shift 2 ;; --mtime-epoch) TIMESTAMP_EPOCH=$2; shift 2 ;; + --readonly-user) + READONLY_USER=$2; shift 2 ;; + --readonly-password) + READONLY_PASSWORD=$2; READONLY_NO_PASSWORD=0; shift 2 ;; + --readonly-no-password) + READONLY_PASSWORD=""; READONLY_NO_PASSWORD=1; shift ;; + --prepare-readonly-rights) + PREPARE_READONLY_RIGHTS=1; shift ;; + --readonly-rights) + READONLY_RIGHTS=$2; shift 2 ;; --no-log) CAPTURE_LOG=0; shift ;; --stop-on-failure) @@ -102,8 +122,14 @@ REPORT_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-afp-smoke.XXXXXX") LOG_TMP=$(mktemp "${TMPDIR:-/tmp}/mars-afp-log.XXXXXX") LOG_PID="" FAILURES=0 +RIGHTS_PREPARED=0 cleanup() { + if [ "$RIGHTS_PREPARED" -eq 1 ] && [ -n "$READONLY_USER" ]; then + nwrevoke -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + -o "$READONLY_USER" -t 1 "$NETWARE_PATH" >/dev/null 2>&1 || true + RIGHTS_PREPARED=0 + fi if [ -n "$LOG_PID" ] && kill -0 "$LOG_PID" 2>/dev/null; then kill "$LOG_PID" 2>/dev/null || true wait "$LOG_PID" 2>/dev/null || true @@ -140,6 +166,27 @@ run_cmd() { fi } +run_optional_cmd() { + local label=$1 + local printable=$2 + shift 2 + + section "$label" + emit "\$ $printable" + "$@" 2>&1 | tee -a "$REPORT_TMP" + local status=${PIPESTATUS[0]} + emit "[exit=$status]" + return "$status" +} + +readonly_print_auth() { + if [ "$READONLY_NO_PASSWORD" -eq 1 ]; then + printf '%s' "-n" + else + printf '%s' "-P ******" + fi +} + finish_report() { if [ "$CAPTURE_LOG" -eq 1 ]; then sleep 1 @@ -175,6 +222,16 @@ emit "log=$LOG_FILE" emit "finder_type=$FINDER_TYPE" emit "finder_creator=$FINDER_CREATOR" emit "mtime_epoch=$TIMESTAMP_EPOCH" +if [ -n "$READONLY_USER" ]; then + emit "readonly_user=$READONLY_USER" + if [ "$READONLY_NO_PASSWORD" -eq 1 ]; then + emit "readonly_auth=no-password" + else + emit "readonly_auth=password" + fi + emit "prepare_readonly_rights=$PREPARE_READONLY_RIGHTS" + emit "readonly_rights=$READONLY_RIGHTS" +fi for helper in \ afp_entry_id_smoke \ @@ -205,6 +262,23 @@ fi COMMON_PRINT="-S $SERVER -U $USER_NAME -P ******" +if [ -n "$READONLY_USER" ] && [ "$PREPARE_READONLY_RIGHTS" -eq 1 ]; then + # Establish a conservative explicit trustee assignment for the negative + # metadata-write probes. The final nwrevoke below removes only this + # explicit assignment and returns the object to its inherited rights. + run_optional_cmd \ + "Prepare readonly trustee cleanup" \ + "nwrevoke -S $SERVER -U $USER_NAME -P ****** -o $READONLY_USER -t 1 '$NETWARE_PATH'" \ + nwrevoke -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + -o "$READONLY_USER" -t 1 "$NETWARE_PATH" || true + run_cmd \ + "Prepare readonly trustee rights" \ + "nwgrant -S $SERVER -U $USER_NAME -P ****** -o $READONLY_USER -t 1 -r '$READONLY_RIGHTS' '$NETWARE_PATH'" \ + nwgrant -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + -o "$READONLY_USER" -t 1 -r "$READONLY_RIGHTS" "$NETWARE_PATH" + RIGHTS_PREPARED=1 +fi + run_cmd \ "AFP Entry ID From Path Name" \ "./afp_entry_id_smoke $COMMON_PRINT '$NETWARE_PATH'" \ @@ -318,6 +392,43 @@ run_cmd \ "$SCRIPT_DIR/afp_set_file_info_smoke" -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ --attributes-only --clear-archive "$NETWARE_PATH" +if [ -n "$READONLY_USER" ]; then + READONLY_PRINT="-S $SERVER -U $READONLY_USER $(readonly_print_auth)" + if [ "$READONLY_NO_PASSWORD" -eq 1 ]; then + READONLY_AUTH_ARGS=(-n) + else + READONLY_AUTH_ARGS=(-P "$READONLY_PASSWORD") + fi + + run_cmd \ + "AFP metadata Modify rights rejected: FinderInfo" \ + "./afp_set_file_info_smoke --expect-completion 0x8c $READONLY_PRINT --finder-info-only --type '$FINDER_TYPE' --creator '$FINDER_CREATOR' '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_set_file_info_smoke" --expect-completion 0x8c \ + -S "$SERVER" -U "$READONLY_USER" "${READONLY_AUTH_ARGS[@]}" \ + --finder-info-only --type "$FINDER_TYPE" --creator "$FINDER_CREATOR" "$NETWARE_PATH" + run_cmd \ + "AFP metadata Modify rights rejected: Invisible" \ + "./afp_set_file_info_smoke --expect-completion 0x8c $READONLY_PRINT --attributes-only --invisible '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_set_file_info_smoke" --expect-completion 0x8c \ + -S "$SERVER" -U "$READONLY_USER" "${READONLY_AUTH_ARGS[@]}" \ + --attributes-only --invisible "$NETWARE_PATH" + run_cmd \ + "AFP metadata Modify rights rejected: System" \ + "./afp_set_file_info_smoke --expect-completion 0x8c $READONLY_PRINT --attributes-only --system '$NETWARE_PATH'" \ + "$SCRIPT_DIR/afp_set_file_info_smoke" --expect-completion 0x8c \ + -S "$SERVER" -U "$READONLY_USER" "${READONLY_AUTH_ARGS[@]}" \ + --attributes-only --system "$NETWARE_PATH" +fi + +if [ -n "$READONLY_USER" ] && [ "$PREPARE_READONLY_RIGHTS" -eq 1 ]; then + run_cmd \ + "Restore readonly trustee assignment" \ + "nwrevoke -S $SERVER -U $USER_NAME -P ****** -o $READONLY_USER -t 1 '$NETWARE_PATH'" \ + nwrevoke -S "$SERVER" -U "$USER_NAME" -P "$PASSWORD" \ + -o "$READONLY_USER" -t 1 "$NETWARE_PATH" + RIGHTS_PREPARED=0 +fi + if command -v stat >/dev/null 2>&1; then run_cmd \ "Linux stat: AFP Modify Timestamp" \