╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────╮ │1813 .home_users => |bb| aa != bb,│ │1814 else => false, │ │1815 }, │ │1823 - .home_repos => |aa| switch (b) { │ │1824 - .home_repos => |bb| aa != bb,│ │1825 - else => false, │ │1826 - }, │ │1816 else => false, │ │1817 }; │ │1818 } │ ╰───────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────╮ │1953 // local mode has no accounts, so it has no user id│ │1954 user_id: ?[evt.event_id_size]u8 = null, │ │1955 role: evt.Repo.Role, │ │1956 + // the repo owner's id, which local mode lacks │ │1957 + repo_user_id: ?[evt.event_id_size]u8 = null, │ │1958 }; │ │1959 │ │1960 pub const Authorization = union(enum) { │ ╰─────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1975 return if (user.removed) null else user; │ │1976 } │ │1977 │ │1987 -// the role the user holds in the repo `identity` names, or null when no repo │ │1988 -// has that identity. a null user is logged out. │ │1989 -pub fn repoRole( │ │1990 - moment: evt.AdminDB.HashMap(.read_only), │ │1991 - arena: *std.heap.ArenaAllocator, │ │1992 - user_id: ?[evt.event_id_size]u8, │ │1993 - identity: []const u8, │ │1994 -) !?evt.Repo.Role { │ │1995 - const owner_repo = evt.parseOwnerRepoPath(identity) orelse return null; │ │1996 - const repo = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, owner_repo.owner, owner_repo.name)) orelse return null;│ │1997 - return evt.Repo.roleOf(repo.repo, user_id); │ │1998 -} │ │1999 - │ │1978 // whether the user may write to the repo `identity` names with at least │ │1979 // `min_role` │ │1980 pub fn authorizeUser( │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │1981 + io: std.Io, │ │1982 moment: evt.AdminDB.HashMap(.read_only),│ │1983 arena: *std.heap.ArenaAllocator, │ │1984 + repos_dir: []const u8, │ │1985 user_id: [evt.event_id_size]u8, │ │1986 identity: []const u8, │ │1987 min_role: evt.Repo.Role, │ ╰──────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1988 ) !Authorization { │ │1989 const user = (try activeUser(moment, arena, user_id)) orelse return .login_required; │ │1990 │ │2011 - const role = (try repoRole(moment, arena, user_id, identity)) orelse return .repo_not_found; │ │1991 + const owner_repo = evt.parseOwnerRepoPath(identity) orelse return .repo_not_found; │ │1992 + const repo = (try evt.readRepoByOwnerAndName(io, arena.child_allocator, arena, moment, repos_dir, owner_repo.owner, owner_repo.name)) orelse return .repo_not_found;│ │1993 + const role = evt.Repo.roleOf(repo.repo, user_id); │ │1994 // a repo the user can't read doesn't exist to them │ │1995 if (role == .none) return .repo_not_found; │ │1996 if (!role.atLeast(min_role)) return .forbidden; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │1998 .author = .{ .name = user.event.name, .email = user.event.email },│ │1999 .user_id = user_id, │ │2000 .role = role, │ │2001 + .repo_user_id = repo.repo.event.user_id[0..evt.event_id_size].*, │ │2002 } }; │ │2003 } │ │2004 │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2244 const user_id = self.userId() orelse return null; │ │2245 // the session's moment predates this input, so read the roles afresh │ │2246 const admin_repo = self.admin_repo orelse return null; │ │2247 + const io = self.io orelse return null; │ │2248 + const repos_dir = self.repos_dir orelse return null; │ │2249 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │2265 - return switch (try authorizeUser(moment, self.page_arena, user_id, identity, min_role)) { │ │2250 + return switch (try authorizeUser(io, moment, self.page_arena, repos_dir, user_id, identity, min_role)) {│ │2251 .actor => |actor| actor, │ │2252 .login_required, .repo_not_found, .forbidden => null, │ │2253 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Fork.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │37 const repos_dir = session.repos_dir orelse return error.NotFound; │ │38 const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │39 const route_fork = route.forkRoute() orelse return error.UnexpectedRoute; │ │40 + // the route's identity names the forker and the target repo │ │41 const identity = ui.RoutablePage.RepoIdentity.parse(route_fork.name.slice()) orelse return error.NotFound; │ │42 const id = evt.parseEventId(route_fork.id.slice()) catch return error.NotFound; │ │42 - const fork_record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, arena, &id)) orelse return error.NotFound; │ │43 + const gpa = arena.child_allocator; │ │44 + const forker_id = (try evt.User.readIdByName(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, identity.owner)) orelse return error.NotFound;│ │45 + const fork_record = (try evt.readForkById(io, gpa, arena, repos_dir, &forker_id, &id)) orelse return error.NotFound; │ │46 if (fork_record.removed) return error.NotFound; │ │47 if (fork_record.event.repo_id.len != evt.event_id_size) return error.NotFound; │ │48 var target_id: [evt.event_id_size]u8 = undefined; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │49 @memcpy(&target_id, fork_record.event.repo_id); │ │50 + const owner_id = fork_record.event.repo_user_id[0..evt.event_id_size]; │ │51 │ │48 - const target_record = (try evt.Repo.readById(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, arena, fork_record.event.repo_id)) orelse return error.NotFound;│ │52 + const target_record = (try evt.readRepoById(io, gpa, arena, repos_dir, owner_id, fork_record.event.repo_id)) orelse return error.NotFound; │ │53 // a draft is as readable as the repo it targets │ │54 if (evt.Repo.roleOf(target_record, session.userId()) == .none) return error.NotFound; │ │51 - const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, arena, target_record.event.user_id)) orelse return error.NotFound; │ │52 - if (!std.mem.eql(u8, owner.event.name, identity.owner) or !std.mem.eql(u8, target_record.event.name, identity.name)) return error.NotFound; │ │55 + const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, arena, owner_id)) orelse return error.NotFound; │ │56 + if (!std.mem.eql(u8, target_record.event.name, identity.name)) return error.NotFound; │ │57 │ │58 const aa = arena.allocator(); │ │59 + const target_identity = try std.fmt.allocPrint(aa, "{s}/{s}", .{ owner.event.name, target_record.event.name }); │ │60 const id_hex = std.fmt.bytesToHex(id, .lower); │ │61 const fork_path = try fork.forkPath(aa, repos_dir, &id_hex); │ │62 var fork_repo = try rp.Repo(.xit, .{}).open(io, arena.child_allocator, .{ .path = fork_path, .require_repo_root = true }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 89 .draft = fork_record.event.stage == .draft, │ │ 90 .revision_oid = try aa.dupe(u8, &fork_oid), │ │ 91 .fork_exists = true, │ │ 92 + .forker = identity.owner, │ │ 93 }; │ │ 88 - var patch_data = try Patches.detailResult(aa, identity.identity, retained_entry); │ │ 94 + var patch_data = try Patches.detailResult(aa, target_identity, retained_entry); │ │ 95 │ │ 96 if (target_repo_maybe) |*target_repo| switch (fork_record.event.stage) { │ │ 91 - .draft => if (try Patches.loadDraftEntry(.xit, .{}, arena, io, haxy_moment, &fork_repo, target_repo, id)) |entry| { │ │ 92 - patch_data = try Patches.detailResult(aa, identity.identity, entry); │ │ 97 + .draft => if (try Patches.loadDraftEntry(.xit, .{}, arena, io, haxy_moment, &fork_repo, target_repo, id, identity.owner)) |entry| {│ │ 98 + patch_data = try Patches.detailResult(aa, target_identity, entry); │ │ 99 patch_data.repo_source = target_source; │ │100 }, │ │101 .publish => { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯