feat(identity): implement --copy-as USER[:GROUP] safe subset (P7 Wave E)

Force the receiver to apply the requested owner/group to every written
entry through the confined fd-relative identity path instead of switching
the process credentials (unsafe for the multithreaded receiver).  An
unprivileged receiver refuses the transfer up front in server_module_gate,
before STATUS_OK, so no data is written with the wrong ownership.

- new Config fields copy_as_set/copy_as_uid/copy_as_gid + defaults
- identity_parse_copy_as (name/@N/* resolution, primary-gid default,
  gid==uid fallback for numeric ids with no passwd entry); implies -M
- identity snapshot + highest-priority forcing in identity_resolve_targets
- identity_copy_as_refused() helper
- trailing config-frame block (presence int + two int32 ids, >=0 checked)
- PROTOCOL_VERSION 2.17.0 -> 2.18.0; version-sensitive tests updated
- unit tests for parse + wire round-trip/negative-id rejection
- integration TestCopyAs: unprivileged refusal + root chown assertion
- RSYNC_COMPAT.md --copy-as row updated (safe subset + divergence); README
  protocol version refreshed
This commit is contained in:
2026-09-12 11:58:37 +02:00
parent f64d252faf
commit 80dd64aae6
12 changed files with 474 additions and 18 deletions
+12
View File
@@ -1414,6 +1414,18 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
if (identity_parse_chown(config, argv[++i]) != 0)
return -1;
config->use_metadata = true;
} else if (strncmp(argv[i], "--copy-as=", 10) == 0) {
if (identity_parse_copy_as(config, argv[i] + 10) != 0)
return -1;
config->use_metadata = true;
} else if (opt_is(argv[i], "--copy-as", NULL)) {
if (i + 1 >= argc) {
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
return -1;
}
if (identity_parse_copy_as(config, argv[++i]) != 0)
return -1;
config->use_metadata = true;
} else if (strncmp(argv[i], "--outbuf=", 9) == 0) {
if (set_outbuf_option(config, argv[i] + 9) != 0)
return -1;