╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯