╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/Home.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │37 return .{ │ │38 .header = try Header.init(arena), │ │39 .users = try Users.init(arena, haxy_moment, users_start), │ │39 - .repos = try Repos.init(arena, haxy_moment, repos_start), │ │40 + .repos = try Repos.init(arena, haxy_moment, user_id, repos_start),│ │41 .settings = Settings.init(), │ │42 .auth = Auth.init(), │ │43 .quit = Quit.init(), │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Home/Repos.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────╮ │22 pub fn init( │ │23 arena: *std.heap.ArenaAllocator, │ │24 haxy_moment: evt.AdminDB.HashMap(.read_only),│ │25 + user_id: ?[evt.event_id_size]u8, │ │26 start: usize, │ │27 ) !Self { │ │28 const DB = evt.AdminDB; │ ╰─────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │57 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, event_id)) orelse continue;│ │58 const repo_map = try DB.HashMap(.read_only).init(repo_cursor); │ │59 const repo_event = try evt.read(evt.Repo.Record, DB, hash_kind, arena, repo_map); │ │60 + // unreadable repos leave their window short rather than shifting the others │ │61 + if (evt.Repo.roleOf(repo_event, user_id) == .none) continue; │ │62 try repos.append(arena.allocator(), repo_event); │ │63 │ │64 const owner = try evt.User.readById(DB, hash_kind, haxy_moment, arena, repo_event.event.user_id); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 } else { │ │209 const haxy_moment = session.haxy_moment orelse return error.NoMoment; │ │210 const found = (try evt.Repo.readByOwnerAndName(DB, hash_kind, haxy_moment, arena, repo_identity.owner, repo_identity.name)) orelse return error.NotFound;│ │211 + // a repo the session can't read doesn't exist to it │ │212 + if (evt.Repo.roleOf(found.repo, session.userId()) == .none) return error.NotFound; │ │213 repo = found.repo; │ │214 repo_id_maybe = found.event_id; │ │215 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 93 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, event_id)) orelse continue;│ │ 94 const repo_map = try DB.HashMap(.read_only).init(repo_cursor); │ │ 95 const repo_event = try evt.read(evt.Repo.Record, DB, hash_kind, arena, repo_map); │ │ 96 + // unreadable repos leave their window short rather than shifting the others │ │ 97 + if (evt.Repo.roleOf(repo_event, session.userId()) == .none) continue; │ │ 98 try repos.append(arena.allocator(), repo_event); │ │ 99 } │ │100 repos_next_start = if (end < count) end else null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │125 │ │126 const repo_cursor = try repo_records.getCursor(hash.hashInt(hash_kind, record.event.repo_id)) orelse continue; │ │127 const target_repo = try evt.read(evt.Repo.Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(repo_cursor));│ │128 + if (evt.Repo.roleOf(target_repo, session.userId()) == .none) continue; │ │129 const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, target_repo.event.user_id)) orelse continue; │ │130 const target = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ owner.event.name, target_repo.event.name }); │ │131 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │507 .local => |src| return .{ .author = try ui.localAuthor(src, io, allocator, arena), .role = .owner },│ │508 }; │ │509 const authorization: ui.Authorization = blk: { │ │510 - const token = getCookieValue(request, cookie_name) orelse break :blk .login_required; │ │511 - var user_id: [evt.event_id_size]u8 = undefined; │ │512 - if (!server.session_store.lookup(token, &user_id)) break :blk .login_required; │ │510 + const user_id = requestUserId(request, server.session_store) orelse break :blk .login_required; │ │511 │ │512 const repo_prefix = "/repo/"; │ │513 if (!std.mem.startsWith(u8, repo_base, repo_prefix)) break :blk .repo_not_found; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │526 return null; │ │527 } │ │528 │ │529 +// whether the request may read the repo `repo_base` names. local mode always │ │530 +// may. │ │531 +fn mayRead( │ │532 + io: std.Io, │ │533 + allocator: std.mem.Allocator, │ │534 + request: *std.http.Server.Request, │ │535 + host: Host, │ │536 + repo_base: []const u8, │ │537 +) !bool { │ │538 + const server = switch (host) { │ │539 + .server => |server| server, │ │540 + .local => return true, │ │541 + }; │ │542 + const repo_prefix = "/repo/"; │ │543 + if (!std.mem.startsWith(u8, repo_base, repo_prefix)) return false; │ │544 + │ │545 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path });│ │546 + defer admin_repo.deinit(io, allocator); │ │547 + var arena = std.heap.ArenaAllocator.init(allocator); │ │548 + defer arena.deinit(); │ │549 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │550 + const user_id = blk: { │ │551 + const id = requestUserId(request, server.session_store) orelse break :blk null; │ │552 + break :blk if (try ui.activeUser(moment, &arena, id) != null) id else null; │ │553 + }; │ │554 + const role = (try ui.repoRole(moment, &arena, user_id, repo_base[repo_prefix.len..])) orelse return false; │ │555 + return role != .none; │ │556 +} │ │557 + │ │558 +// the logged-in user the request's session cookie names │ │559 +fn requestUserId(request: *std.http.Server.Request, session_store: SessionStore) ?[evt.event_id_size]u8 { │ │560 + const token = getCookieValue(request, cookie_name) orelse return null; │ │561 + var user_id: [evt.event_id_size]u8 = undefined; │ │562 + return if (session_store.lookup(token, &user_id)) user_id else null; │ │563 +} │ │564 + │ │565 // refuse a write from a request with no logged-in user. the body goes unread, │ │566 // so the connection closes. │ │567 fn respondLoginRequired(request: *std.http.Server.Request) !void { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │968 attachment: AttachmentRequest, │ │969 host: Host, │ │970 ) !void { │ │971 + if (!try mayRead(io, allocator, request, host, attachment.repo_base)) return respondAttachmentNotFound(request); │ │972 const request_repo = (try requestRepoSource(io, allocator, host, attachment.repo_base)) orelse return respondAttachmentNotFound(request);│ │973 defer request_repo.deinit(allocator); │ │974 const source = request_repo.source; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯