╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │508 const role = value.role orelse return writeError(sess, "repo not found"); │ │509 if (!role.atLeast(patch_role)) return writeError(sess, "unauthorized: your role in this repo may not write patches"); │ │510 } │ │511 + } │ │512 + if (service == .upload_pack) { │ │513 + // a fork is as readable as its target, and only its forker's once the target is gone │ │514 + const readable = if (target) |value| readable: { │ │515 + const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &target_owner_id)) orelse unreachable; │ │516 + break :readable try authorizeRepoKey(io, &admin_arena, handler.admin_repo_path, handler.server.users_dir, owner.event.name, value.repo.event.name, .upload_pack, &sess.fingerprint) == .allowed;│ │517 + } else try evt.User.holdsKey(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &forker_id, &sess.fingerprint); │ │518 + if (!readable) return writeError(sess, "unauthorized: this SSH key cannot read this patch draft"); │ │519 } │ │520 │ │521 const draft_path = try fork.forkPath(allocator, handler.server.users_dir, &forker_id, &fork_id); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │861 defer allocator.free(denied_url); │ │862 try client.addRemote(io, allocator, .{ .name = "denied", .value = denied_url }); │ │863 try std.testing.expectError(error.ServerReportedError, client.push(io, allocator, "denied", "master:patch", false, .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); │ │864 + // a fork is as readable as its private target, so a key nobody holds can't fetch it │ │865 + try std.testing.expectError(error.ServerReportedError, client.fetch(io, allocator, "denied", .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); │ │866 + │ │867 + // once the target is gone, only the forker's key may fetch the fork │ │868 + { │ │869 + var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; │ │870 + defer user_repo.deinit(io, allocator); │ │871 + try evt.remove(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, &repo_id, .repo, .{ .name = "admin", .email = "admin@example.test" });│ │872 + } │ │873 + try std.testing.expectError(error.ServerReportedError, client.fetch(io, allocator, "denied", .{ .wire = .{ .ssh = .{ .command = reject_ssh_cmd } } })); │ │874 } │ │875 │ │876 fn testPushCreatesMissingRepo( │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯