╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 user_index: usize, │ │148 name: []const u8, │ │149 description: []const u8, │ │150 + read_access: evt.Repo.Access = .public, │ │151 }{ │ │152 .{ .user_index = 1, .name = "ziglings", .description = "Learn the Zig programming language by fixing tiny broken programs" },│ │153 .{ .user_index = 2, .name = "linux", .description = "Linux kernel source tree" }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │160 .{ .user_index = 9, .name = "cpython", .description = "The Python programming language" }, │ │161 .{ .user_index = 10, .name = "docker", .description = "Container platform for developing, shipping, and running applications" },│ │162 .{ .user_index = 1, .name = "vim", .description = "The ubiquitous text editor" }, │ │162 - .{ .user_index = 2, .name = "neovim", .description = "Hyperextensible Vim-based text editor" }, │ │163 + // private, so only bob and admin can see it │ │164 + .{ .user_index = 2, .name = "neovim", .description = "Hyperextensible Vim-based text editor", .read_access = .private }, │ │165 .{ .user_index = 3, .name = "emacs", .description = "GNU Emacs source code mirror" }, │ │166 .{ .user_index = 4, .name = "tmux", .description = "Terminal multiplexer" }, │ │167 .{ .user_index = 5, .name = "zsh", .description = "Mirror of the Z shell source code repository" }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │240 .user_id = &user_ids[r.user_index], │ │241 .name = r.name, │ │242 .description = r.description, │ │241 - .read_access = .public, │ │243 + .read_access = r.read_access, │ │244 .write_user_ids = &admin_user_id_hex,│ │245 }, │ │246 }, │ ╰──────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │70 // the repo page can build without a moment in local mode; the home and │ │71 // user pages always read from the admin db. │ │72 return switch (route.parent()) { │ │73 - .home => .{ .home = try Home.init(arena, session.haxy_moment orelse return error.NoMoment, switch (route) { │ │73 + .home => .{ .home = try Home.init(arena, session.haxy_moment orelse return error.NoMoment, session.userId(), switch (route) {│ │74 .home_users => |start| start, │ │75 else => 0, │ │76 }, switch (route) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1754 forbidden, │ │1755 }; │ │1756 │ │1757 +// the account `user_id` names, or null once it has been removed. a user id is │ │1758 +// checked here wherever one enters, so the role lookups can trust theirs. │ │1759 +pub fn activeUser( │ │1760 + moment: evt.AdminDB.HashMap(.read_only), │ │1761 + arena: *std.heap.ArenaAllocator, │ │1762 + user_id: [evt.event_id_size]u8, │ │1763 +) !?evt.User.Record { │ │1764 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse return null; │ │1765 + return if (user.removed) null else user; │ │1766 +} │ │1767 + │ │1768 +// the role the user holds in the repo `identity` names, or null when no repo │ │1769 +// has that identity. a null user is logged out. │ │1770 +pub fn repoRole( │ │1771 + moment: evt.AdminDB.HashMap(.read_only), │ │1772 + arena: *std.heap.ArenaAllocator, │ │1773 + user_id: ?[evt.event_id_size]u8, │ │1774 + identity: []const u8, │ │1775 +) !?evt.Repo.Role { │ │1776 + const owner_repo = evt.parseOwnerRepoPath(identity) orelse return null; │ │1777 + const repo = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, owner_repo.owner, owner_repo.name)) orelse return null;│ │1778 + return evt.Repo.roleOf(repo.repo, user_id); │ │1779 +} │ │1780 + │ │1781 // whether the user may write to the repo `identity` names with at least │ │1782 // `min_role` │ │1783 pub fn authorizeUser( │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1787 identity: []const u8, │ │1788 min_role: evt.Repo.Role, │ │1789 ) !Authorization { │ │1766 - const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse return .login_required; │ │1767 - // a removed account's open sessions stop writing │ │1768 - if (user.removed) return .login_required; │ │1769 - const owner_repo = evt.parseOwnerRepoPath(identity) orelse return .repo_not_found; │ │1770 - const repo = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, owner_repo.owner, owner_repo.name)) orelse return .repo_not_found;│ │1790 + const user = (try activeUser(moment, arena, user_id)) orelse return .login_required; │ │1791 │ │1772 - const role = evt.Repo.roleOf(repo.repo, user_id); │ │1792 + const role = (try repoRole(moment, arena, user_id, identity)) orelse return .repo_not_found; │ │1793 + // a repo the user can't read doesn't exist to them │ │1794 + if (role == .none) return .repo_not_found; │ │1795 if (!role.atLeast(min_role)) return .forbidden; │ │1796 return .{ .actor = .{ │ │1797 .author = .{ .name = user.event.name, .email = user.event.email }, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1993 │ │1994 // load the logged-in user's name and persisted preferences from the db │ │1995 pub fn loadUser(self: *Self) !void { │ │1974 - const user_id = self.data.user_id orelse return; │ │1996 + const user_id = self.userId() orelse return; │ │1997 const moment = self.haxy_moment orelse return; │ │1976 - if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, self.arena, user_id)) |user| {│ │1977 - self.data.user_name = user.event.name; │ │1978 - self.data.enable_ansi = user.event.enable_ansi; │ │1979 - } │ │1998 + const user = (try activeUser(moment, self.arena, user_id)) orelse return self.logOut(); │ │1999 + self.data.user_name = user.event.name; │ │2000 + self.data.enable_ansi = user.event.enable_ansi; │ │2001 + } │ │2002 + │ │2003 + // a removed account's sessions read as logged out │ │2004 + fn logOut(self: *Self) void { │ │2005 + self.data.user_id = null; │ │2006 + self.data.user_name = null; │ │2007 } │ │2008 │ │2009 pub fn userId(self: *const Self) ?[evt.event_id_size]u8 { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2089 const moment = try evt.currentMoment(evt.admin_repo_opts, repo); │ │2090 self.haxy_moment = moment; │ │2091 │ │2065 - const user_id = self.data.user_id orelse return; │ │2092 + const user_id = self.userId() orelse return; │ │2093 var arena = std.heap.ArenaAllocator.init(allocator); │ │2094 defer arena.deinit(); │ │2068 - if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, user_id)) |user| {│ │2069 - self.data.enable_ansi = user.event.enable_ansi; │ │2070 - } │ │2095 + const user = (try activeUser(moment, &arena, user_id)) orelse return self.logOut(); │ │2096 + self.data.enable_ansi = user.event.enable_ansi; │ │2097 } │ │2098 │ │2099 // request a forward navigation to `route`; the host consumes next_page │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Fork.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │46 @memcpy(&target_id, fork_record.event.repo_id); │ │47 │ │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;│ │49 + // a draft is as readable as the repo it targets │ │50 + 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; │ │53 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Home.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │29 pub fn init( │ │30 arena: *std.heap.ArenaAllocator, │ │31 haxy_moment: evt.AdminDB.HashMap(.read_only), │ │32 + user_id: ?[evt.event_id_size]u8, │ │33 // pagination window start for each list tab; the inactive tab gets 0.│ │34 users_start: usize, │ │35 repos_start: usize, │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯