╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────╮ │ 4 const rp = xit.repo; │ │ 5 const hash = xit.hash; │ │ 6 const bcrypt = std.crypto.pwhash.bcrypt; │ │ 7 +const ssh = @import("../serve_ssh_protocol.zig");│ │ 8 │ │ 9 name: []const u8, │ │10 email: []const u8, │ ╰─────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │38 // resolves a commit's author email to its user at read time │ │39 pub const email_to_user_id_key = "email->user-id"; │ │40 │ │41 +// the users holding each ssh key. a set, since two users may register one key.│ │42 +pub const fingerprint_to_user_id_set_key = "ssh-fingerprint->user-id-set"; │ │43 + │ │44 pub fn validateName(name: []const u8) !void { │ │45 if (name.len == 0) return error.NameEmpty; │ │46 if (name.len < name_min_len) return error.NameTooShort; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │80 │ │81 const email_to_user_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, email_to_user_id_key)); │ │82 const email_to_user_id = try DB.HashMap(.read_write).init(email_to_user_id_cursor); │ │83 + │ │84 + const fingerprint_to_user_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, fingerprint_to_user_id_set_key));│ │85 + const fingerprint_to_user_id_set = try DB.HashMap(.read_write).init(fingerprint_to_user_id_set_cursor); │ │86 │ │87 var existing_record_maybe: ?Record = null; │ │88 const existing_cursor_maybe = try event_id_to_user.getCursor(user_key); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │103 if (!existing_record.removed) { │ │104 _ = try name_to_user_id.remove(existing_record.event.name); │ │105 _ = try email_to_user_id.remove(hash.hashInt(hash_kind, existing_record.event.email)); │ │106 + │ │107 + var keys = std.mem.splitScalar(u8, existing_record.event.ssh_keys, '\n'); │ │108 + while (keys.next()) |line| { │ │109 + const fingerprint = fingerprintOfKey(line) orelse continue; │ │110 + const user_id_set = try DB.SortedSet(.read_write).init(try fingerprint_to_user_id_set.putCursor(hash.hashInt(hash_kind, &fingerprint)));│ │111 + _ = try user_id_set.remove(event_id); │ │112 + } │ │113 } │ │114 } │ │115 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │130 if (!record_to_write.removed) { │ │131 try name_to_user_id.put(record_to_write.event.name, .{ .bytes = event_id }); │ │132 try email_to_user_id.put(hash.hashInt(hash_kind, record_to_write.event.email), .{ .bytes = event_id }); │ │133 + │ │134 + var keys = std.mem.splitScalar(u8, record_to_write.event.ssh_keys, '\n'); │ │135 + while (keys.next()) |line| { │ │136 + const fingerprint = fingerprintOfKey(line) orelse continue; │ │137 + const user_id_set = try DB.SortedSet(.read_write).init(try fingerprint_to_user_id_set.putCursor(hash.hashInt(hash_kind, &fingerprint)));│ │138 + try user_id_set.put(event_id); │ │139 + } │ │140 } │ │141 } │ │142 + │ │143 +// the SHA256 fingerprint of one authorized_keys line, or null if the line is │ │144 +// blank or not a parseable "<type> <base64> [comment]" entry │ │145 +fn fingerprintOfKey(line: []const u8) ?[ssh.fingerprint_len]u8 { │ │146 + var it = std.mem.tokenizeAny(u8, line, " \t\r"); │ │147 + _ = it.next() orelse return null; // key type │ │148 + const blob_b64 = it.next() orelse return null; │ │149 │ │150 + const decoder = std.base64.standard.Decoder; │ │151 + const blob_len = decoder.calcSizeForSlice(blob_b64) catch return null; │ │152 + var blob_buf: [4096]u8 = undefined; │ │153 + if (blob_len > blob_buf.len) return null; │ │154 + decoder.decode(blob_buf[0..blob_len], blob_b64) catch return null; │ │155 + │ │156 + return ssh.formatFingerprint(blob_buf[0..blob_len]); │ │157 +} │ │158 + │ │159 pub const password_hash_max_len = bcrypt.hash_length * 2; │ │160 │ │161 pub fn hashPassword( │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │264 return user_id; │ │265 } │ │266 │ │267 +// the ids of the active users holding the ssh key with this fingerprint │ │268 +pub fn readIdsByFingerprint( │ │269 + comptime DB: type, │ │270 + comptime hash_kind: hash.HashKind, │ │271 + haxy_moment: DB.HashMap(.read_only), │ │272 + arena: *std.heap.ArenaAllocator, │ │273 + fingerprint: *const [ssh.fingerprint_len]u8, │ │274 +) ![]const [evt.event_id_size]u8 { │ │275 + var user_ids: std.ArrayList([evt.event_id_size]u8) = .empty; │ │276 + const index_cursor = (try haxy_moment.getCursor(hash.hashInt(hash_kind, fingerprint_to_user_id_set_key))) orelse return user_ids.items;│ │277 + const index = try DB.HashMap(.read_only).init(index_cursor); │ │278 + const user_id_set_cursor = (try index.getCursor(hash.hashInt(hash_kind, fingerprint))) orelse return user_ids.items; │ │279 + const user_id_set = try DB.SortedSet(.read_only).init(user_id_set_cursor); │ │280 + │ │281 + var iter = try user_id_set.iterator(); │ │282 + while (try iter.next()) |kv_pair_cursor| { │ │283 + var cursor = kv_pair_cursor; │ │284 + const kv_pair = try cursor.readKeyValuePair(); │ │285 + var user_id: [evt.event_id_size]u8 = undefined; │ │286 + _ = try kv_pair.key_cursor.readBytes(&user_id); │ │287 + try user_ids.append(arena.allocator(), user_id); │ │288 + } │ │289 + return user_ids.items; │ │290 +} │ │291 + │ │292 +// whether the user holds the ssh key with this fingerprint │ │293 +pub fn holdsKey( │ │294 + comptime DB: type, │ │295 + comptime hash_kind: hash.HashKind, │ │296 + haxy_moment: DB.HashMap(.read_only), │ │297 + arena: *std.heap.ArenaAllocator, │ │298 + user_id: *const [evt.event_id_size]u8, │ │299 + fingerprint: *const [ssh.fingerprint_len]u8, │ │300 +) !bool { │ │301 + for (try readIdsByFingerprint(DB, hash_kind, haxy_moment, arena, fingerprint)) |holder_id| { │ │302 + if (std.mem.eql(u8, &holder_id, user_id)) return true; │ │303 + } │ │304 + return false; │ │305 +} │ │306 + │ │307 // a user's record map via the event-id->user index │ │308 fn userMap( │ │309 comptime DB: type, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯