╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────╮ │src/serve_http.zig│ ╰──────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 var arena = std.heap.ArenaAllocator.init(allocator); │ │134 defer arena.deinit(); │ │135 const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │136 - const repo = (try evt.readRepoByOwnerAndName(io, allocator, &arena, moment, users_dir, owner_repo.owner, owner_repo.name)) orelse │ │136 + const repo = (try evt.readRepoByOwnerAndName(io, allocator, &arena, moment, users_dir, owner_repo.owner, owner_repo.name, null)) orelse│ │137 return error.RepoNotFound; │ │138 if (repo.repo.event.read_access == .private) { │ │139 if (http_server.reader.state == .received_head) http_server.reader.state = .ready; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │456 inline else => |*repo| try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &sideband), │ │457 } │ │458 } else { │ │459 - const target = (try evt.readRepoById(io, allocator, &admin_arena, handler.users_dir, fork_record.event.repo_user_id[0..evt.event_id_size], fork_record.event.repo_id)) orelse │ │459 + const target = (try evt.readRepoById(io, allocator, &admin_arena, handler.users_dir, fork_record.event.repo_user_id[0..evt.event_id_size], fork_record.event.repo_id[0..evt.event_id_size], null)) orelse│ │460 return writeError(sess, "repo not found"); │ │461 - if (!std.mem.eql(u8, target.event.name, forker_repo.name)) return writeError(sess, "patch draft belongs to another repo"); │ │461 + if (!std.mem.eql(u8, target.repo.event.name, forker_repo.name)) return writeError(sess, "patch draft belongs to another repo"); │ │462 const target_path = try evt.repoPath(allocator, handler.users_dir, fork_record.event.repo_user_id, fork_record.event.repo_id); │ │463 defer allocator.free(target_path); │ │464 const author: evt.CommitAuthor = .{ .name = user.event.name, .email = user.event.email }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │565 defer admin.deinit(io, allocator); │ │566 │ │567 const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │568 - const repo = (try evt.readRepoByOwnerAndName(io, allocator, arena, moment, users_dir, owner_name, repo_name)) orelse { │ │568 + // no viewer yet, so this carries the base role │ │569 + const repo = (try evt.readRepoByOwnerAndName(io, allocator, arena, moment, users_dir, owner_name, repo_name, null)) orelse {│ │570 if (service == .upload_pack) return .not_found; │ │571 const owner = (try evt.User.readByName(io, allocator, admin_repo_path, arena, owner_name)) orelse return .not_found; │ │572 return if (isKeyInAuthorizedKeys(owner.event.ssh_keys, fingerprint)) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │580 .receive_pack => .write, │ │581 }; │ │582 // skip the key lookups when the base role is enough │ │582 - if (evt.Repo.roleOf(repo.repo, null).atLeast(min_role)) return .{ .allowed = null }; │ │583 + if (repo.role.atLeast(min_role)) return .{ .allowed = null }; │ │584 │ │584 - const user = (try repoUserWithKey(moment, arena, repo.repo.event, fingerprint)) orelse return .denied; │ │585 - if (!evt.Repo.roleOf(repo.repo, user.id).atLeast(min_role)) return .denied; │ │586 - return .{ .allowed = user.author }; │ │587 -} │ │585 + // the creator owns the repo │ │586 + const owner_id = repo.repo.event.user_id[0..evt.event_id_size]; │ │587 + if (try authorWithKey(moment, arena, owner_id, fingerprint)) |author| return .{ .allowed = author }; │ │588 │ │589 -const UserWithKey = struct { │ │590 - id: [evt.event_id_size]u8, │ │591 - author: evt.CommitAuthor, │ │592 -}; │ │593 - │ │594 -// the repo's owner or collaborator holding the key, or null for anyone else │ │595 -fn repoUserWithKey( │ │596 - moment: evt.AdminDB.HashMap(.read_only), │ │597 - arena: *std.heap.ArenaAllocator, │ │598 - repo: evt.Repo, │ │599 - fingerprint: *const [ssh.fingerprint_len]u8, │ │600 -) !?UserWithKey { │ │601 - if (try userWithKey(moment, arena, repo.user_id, fingerprint)) |user| return user; │ │602 - │ │603 - for ([_][]const u8{ repo.owner_user_ids, repo.write_user_ids, repo.read_user_ids }) |user_ids| { │ │604 - var lines = std.mem.tokenizeScalar(u8, user_ids, '\n'); │ │605 - while (lines.next()) |id| { │ │606 - const user_id = try evt.parseEventId(id); │ │607 - if (try userWithKey(moment, arena, &user_id, fingerprint)) |user| return user; │ │608 - } │ │589 + // anyone else holding the key needs a grant with the role. a key can be │ │590 + // shared, so a grant that falls short doesn't end the search. │ │591 + for (try evt.readRepoGrants(io, allocator, arena, users_dir, owner_id, &repo.event_id)) |grant| { │ │592 + if (!grant.role.atLeast(min_role)) continue; │ │593 + if (try authorWithKey(moment, arena, grant.user_id, fingerprint)) |author| return .{ .allowed = author };│ │594 } │ │610 - return null; │ │595 + return .denied; │ │596 } │ │597 │ │613 -fn userWithKey( │ │598 +// the user as a commit author, or null unless they hold the key │ │599 +fn authorWithKey( │ │600 moment: evt.AdminDB.HashMap(.read_only), │ │601 arena: *std.heap.ArenaAllocator, │ │602 user_id: []const u8, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │603 fingerprint: *const [ssh.fingerprint_len]u8, │ │618 -) !?UserWithKey { │ │604 +) !?evt.CommitAuthor { │ │605 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, user_id)) orelse return null;│ │606 if (user.removed or !isKeyInAuthorizedKeys(user.event.ssh_keys, fingerprint)) return null; │ │621 - return .{ │ │622 - .id = user_id[0..evt.event_id_size].*, │ │623 - .author = .{ .name = user.event.name, .email = user.event.email }, │ │624 - }; │ │607 + return .{ .name = user.event.name, .email = user.event.email }; │ │608 } │ │609 │ │610 fn isKeyInAuthorizedKeys(ssh_keys: []const u8, fingerprint: *const [ssh.fingerprint_len]u8) bool { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────╮ │src/test/net.zig│ ╰────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │543 var arena = std.heap.ArenaAllocator.init(allocator); │ │544 defer arena.deinit(); │ │545 const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │546 - const target = (try evt.readRepoByOwnerAndName(io, allocator, &arena, moment, users_dir, "admin", "target")) orelse return error.NotFound; │ │546 + const target = (try evt.readRepoByOwnerAndName(io, allocator, &arena, moment, users_dir, "admin", "target", null)) orelse return error.NotFound;│ │547 repo_id = target.event_id; │ │548 @memcpy(&user_id, target.repo.event.user_id); │ │549 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse return error.NotFound; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/test/undo.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │71 const repo_id = [_]u8{4} ** evt.event_id_size; │ │72 const author: evt.CommitAuthor = .{ .name = "test", .email = "test@example.test" }; │ │73 const writer_hex = std.fmt.bytesToHex(writer, .lower); │ │74 - var repo_event: evt.Repo = .{ .user_id = &owner, .name = "demo", .description = "", .read_access = .public, .write_user_ids = &writer_hex }; │ │74 + var repo_event: evt.Repo = .{ .user_id = &owner, .name = "demo", .description = "", .read_access = .public }; │ │75 const seed = [_]evt.EventWithId{ │ │76 .{ .id = std.fmt.bytesToHex(owner, .lower), .author = author, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = "unused" } } },│ │77 .{ .id = writer_hex, .author = author, .event = .{ .user = .{ .name = "writer", .email = "writer@example.test", .password_hash = "unused" } } }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │80 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &admin, evt.events_ref, &seed); │ │81 var user_repo = try evt.initUserRepo(io, allocator, try evt.userRepoPath(aa, users_dir, &owner)); │ │82 defer user_repo.deinit(io, allocator); │ │83 - try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{.{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }});│ │83 + try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &.{ │ │84 + .{ .id = std.fmt.bytesToHex(repo_id, .lower), .author = author, .event = .{ .repo = repo_event } }, │ │85 + .{ .id = std.fmt.bytesToHex(evt.Grant.idOf(&repo_id, &writer), .lower), .author = author, .event = .{ .grant = .{ .target_id = &repo_id, .user_id = &writer, .role = .write } } }, │ │86 + }); │ │87 const path = try evt.repoPath(aa, users_dir, &owner, &repo_id); │ │88 { │ │89 var repo = try xit.repo.Repo(.xit, .{}).init(io, allocator, .{ .path = path }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/try.zig│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │232 const users_dir = try std.fs.path.join(arena.allocator(), &.{ server_path, "users" }); │ │233 try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .admin, .xit, evt.admin_repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume);│ │234 │ │235 - // admin co-owns every repo, so it has full privileges everywhere │ │236 - // (including undo) while the other users only own their own │ │237 - const admin_user_id_hex = std.fmt.bytesToHex(admin_user_id, .lower); │ │235 + // admin is granted ownership of every repo, so it has full privileges │ │236 + // everywhere (including undo) while the other users only own their own │ │237 var repo_events: [repo_data.len]evt.EventWithId = undefined; │ │238 + var grant_events: [repo_data.len]evt.EventWithId = undefined; │ │239 for (repo_data, 0..) |r, i| { │ │240 + const author: evt.CommitAuthor = .{ .name = user_data[r.user_index].name, .email = user_data[r.user_index].email }; │ │241 repo_events[i] = .{ │ │242 .id = std.fmt.bytesToHex(repo_event_ids[i], .lower), │ │243 .timestamp = @intCast(user_data.len + i + 1), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │243 - .author = .{ .name = user_data[r.user_index].name, .email = user_data[r.user_index].email },│ │244 + .author = author, │ │245 .event = .{ │ │246 .repo = .{ │ │247 .user_id = &user_ids[r.user_index], │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯