╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │486 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &forker_id)) orelse │ │487 return writeError(sess, "invalid patch draft"); │ │488 if (user.removed) return writeError(sess, "invalid patch draft"); │ │489 - if (service == .receive_pack and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │489 + if (service == .receive_pack and !try evt.User.holdsKey(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, &forker_id, &sess.fingerprint))│ │490 return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │491 │ │492 const target_owner_id = try evt.parseEventId(&fork_record.event.repo_user_id); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │575 const repo = (try evt.readRepoByOwnerAndName(io, allocator, arena, moment, users_dir, owner_name, repo_name, null)) orelse { │ │576 if (service == .upload_pack) return .not_found; │ │577 const owner_id = (try evt.User.readIdByName(evt.AdminDB, evt.admin_repo_opts.hash, moment, owner_name)) orelse return .not_found;│ │578 - const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &owner_id)) orelse unreachable; │ │579 - return if (isKeyInAuthorizedKeys(owner.event.ssh_keys, fingerprint)) │ │580 - .{ .create = .{ .owner_id = owner_id, .owner = .{ .name = owner.event.name, .email = owner.event.email } } } │ │581 - else │ │582 - .denied; │ │578 + if (!try evt.User.holdsKey(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &owner_id, fingerprint)) return .denied; │ │579 + return .{ .create = .{ .owner_id = owner_id, .owner = try commitAuthor(moment, arena, &owner_id) } }; │ │580 }; │ │581 │ │582 const min_role: evt.Repo.Role = switch (service) { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │588 if (base.atLeast(min_role)) return .{ .allowed = null }; │ │589 } │ │590 │ │594 - // the creator owns the repo │ │591 + // a key can be shared, so a holder whose role falls short doesn't end the search │ │592 const owner_id = try evt.parseEventId(&repo.repo.event.user_id); │ │596 - if (try authorWithKey(moment, arena, &owner_id, fingerprint)) |author| return .{ .allowed = author }; │ │597 - │ │598 - // anyone else holding the key needs a grant with the role. a key can be │ │599 - // shared, so a grant that falls short doesn't end the search. │ │600 - for (try evt.readRepoGrants(io, allocator, arena, users_dir, &owner_id, &repo.event_id)) |grant| { │ │601 - if (!grant.role.atLeast(min_role)) continue; │ │602 - if (try authorWithKey(moment, arena, &try evt.parseEventId(&grant.user_id), fingerprint)) |author| return .{ .allowed = author }; │ │593 + for (try evt.User.readIdsByFingerprint(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, fingerprint)) |holder_id| { │ │594 + const seen = (try evt.readRepoById(io, allocator, arena, users_dir, &owner_id, &repo.event_id, holder_id)) orelse return .not_found;│ │595 + const role = seen.role orelse continue; │ │596 + if (role.atLeast(min_role)) return .{ .allowed = try commitAuthor(moment, arena, &holder_id) }; │ │597 } │ │598 return .denied; │ │599 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │600 │ │607 -// the user as a commit author, or null unless they hold the key │ │608 -fn authorWithKey( │ │601 +// an active user as a commit author │ │602 +fn commitAuthor( │ │603 moment: evt.AdminDB.HashMap(.read_only), │ │604 arena: *std.heap.ArenaAllocator, │ │611 - user_id: []const u8, │ │612 - fingerprint: *const [ssh.fingerprint_len]u8, │ │613 -) !?evt.CommitAuthor { │ │614 - const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, user_id)) orelse return null;│ │615 - if (user.removed or !isKeyInAuthorizedKeys(user.event.ssh_keys, fingerprint)) return null; │ │605 + user_id: *const [evt.event_id_size]u8, │ │606 +) !evt.CommitAuthor { │ │607 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, user_id)) orelse unreachable;│ │608 return .{ .name = user.event.name, .email = user.event.email }; │ │609 } │ │610 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │619 -fn isKeyInAuthorizedKeys(ssh_keys: []const u8, fingerprint: *const [ssh.fingerprint_len]u8) bool {│ │620 - var it = std.mem.splitScalar(u8, ssh_keys, '\n'); │ │621 - while (it.next()) |line| { │ │622 - const fp = fingerprintOfAuthorizedKey(line) orelse continue; │ │623 - if (std.mem.eql(u8, &fp, fingerprint)) return true; │ │624 - } │ │625 - return false; │ │626 -} │ │627 - │ │628 -// the SHA256 fingerprint of one authorized_keys line, or null if the line is │ │629 -// blank or not a parseable "<type> <base64> [comment]" entry │ │630 -fn fingerprintOfAuthorizedKey(line: []const u8) ?[ssh.fingerprint_len]u8 { │ │631 - var it = std.mem.tokenizeAny(u8, line, " \t\r"); │ │632 - _ = it.next() orelse return null; // key type │ │633 - const blob_b64 = it.next() orelse return null; │ │634 - │ │635 - const decoder = std.base64.standard.Decoder; │ │636 - const blob_len = decoder.calcSizeForSlice(blob_b64) catch return null; │ │637 - var blob_buf: [4096]u8 = undefined; │ │638 - if (blob_len > blob_buf.len) return null; │ │639 - decoder.decode(blob_buf[0..blob_len], blob_b64) catch return null; │ │640 - │ │641 - return ssh.formatFingerprint(blob_buf[0..blob_len]); │ │642 -} │ │643 - │ │611 fn writeError(sess: *ssh.SessionCtx, comptime msg: []const u8) !void { │ │612 const body = "ERR haxy ssh: " ++ msg; │ │613 var packet: [body.len + 4]u8 = undefined; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯