╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │2258 .event = .{ .user = event }, │ │2259 }}); │ │2260 } │ │2261 - │ │2262 -pub const KeyAction = enum { add, remove }; │ │2261 │ │2264 -// add an authorized_keys line to the user's ssh keys, or remove the lines│ │2265 -// holding the same key. removing a key they don't hold writes nothing. │ │2266 -pub fn changeUserKey( │ │2262 +// add an authorized_keys line to the user's ssh keys │ │2263 +pub fn addUserKey( │ │2264 io: std.Io, │ │2265 allocator: std.mem.Allocator, │ │2266 users_dir: []const u8, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │2267 admin_repo: *rp.Repo(.xit, admin_repo_opts), │ │2268 user_id: *const [event_id_size]u8, │ │2272 - action: KeyAction, │ │2269 key_text: []const u8, │ │2270 ) !void { │ │2271 const key = std.mem.trim(u8, key_text, " \t\r");│ ╰──────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2274 const fingerprint = User.fingerprintOfKey(key) orelse return error.InvalidKey; │ │2275 // the key types the ssh server verifies │ │2276 const key_type = std.mem.sliceTo(key, ' '); │ │2281 - if (action == .add and !std.mem.eql(u8, key_type, "ssh-ed25519") and !std.mem.eql(u8, key_type, "ssh-rsa")) return error.UnsupportedKey;│ │2277 + if (!std.mem.eql(u8, key_type, "ssh-ed25519") and !std.mem.eql(u8, key_type, "ssh-rsa")) return error.UnsupportedKey; │ │2278 + try writeUserKeys(io, allocator, users_dir, admin_repo, user_id, &fingerprint, key); │ │2279 +} │ │2280 + │ │2281 +// remove the user's ssh key with this fingerprint. removing a key they don't │ │2282 +// hold writes nothing. │ │2283 +pub fn removeUserKey( │ │2284 + io: std.Io, │ │2285 + allocator: std.mem.Allocator, │ │2286 + users_dir: []const u8, │ │2287 + admin_repo: *rp.Repo(.xit, admin_repo_opts), │ │2288 + user_id: *const [event_id_size]u8, │ │2289 + fingerprint: *const [User.fingerprint_len]u8, │ │2290 +) !void { │ │2291 + try writeUserKeys(io, allocator, users_dir, admin_repo, user_id, fingerprint, null); │ │2292 +} │ │2293 │ │2294 +// rewrite the user's ssh keys with `added` appended, or without the key with │ │2295 +// `fingerprint` when nothing is added │ │2296 +fn writeUserKeys( │ │2297 + io: std.Io, │ │2298 + allocator: std.mem.Allocator, │ │2299 + users_dir: []const u8, │ │2300 + admin_repo: *rp.Repo(.xit, admin_repo_opts), │ │2301 + user_id: *const [event_id_size]u8, │ │2302 + fingerprint: *const [User.fingerprint_len]u8, │ │2303 + added: ?[]const u8, │ │2304 +) !void { │ │2305 var arena = std.heap.ArenaAllocator.init(allocator); │ │2306 defer arena.deinit(); │ │2307 const moment = try currentMoment(admin_repo_opts, admin_repo); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │2313 var lines = std.mem.splitScalar(u8, user.event.ssh_keys, '\n'); │ │2314 while (lines.next()) |line| { │ │2315 const line_fingerprint = User.fingerprintOfKey(line) orelse continue;│ │2294 - if (std.mem.eql(u8, &line_fingerprint, &fingerprint)) { │ │2316 + if (std.mem.eql(u8, &line_fingerprint, fingerprint)) { │ │2317 held = true; │ │2296 - if (action == .remove) continue; │ │2318 + if (added == null) continue; │ │2319 } │ │2320 if (keys.items.len > 0) try keys.append(arena.allocator(), '\n'); │ │2321 try keys.appendSlice(arena.allocator(), line); │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │2322 } │ │2301 - switch (action) { │ │2302 - .add => { │ │2303 - if (held) return error.KeyAdded; │ │2304 - if (keys.items.len > 0) try keys.append(arena.allocator(), '\n');│ │2305 - try keys.appendSlice(arena.allocator(), key); │ │2306 - }, │ │2307 - .remove => if (!held) return, │ │2308 - } │ │2323 + if (added) |key| { │ │2324 + if (held) return error.KeyAdded; │ │2325 + if (keys.items.len > 0) try keys.append(arena.allocator(), '\n'); │ │2326 + try keys.appendSlice(arena.allocator(), key); │ │2327 + } else if (!held) return; │ │2328 │ │2329 var event = user.event; │ │2330 event.ssh_keys = keys.items; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │37 │ │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 +pub const fingerprint_len = ssh.fingerprint_len; │ │42 │ │43 // the users holding each ssh key. a set, since two users may register one key.│ │44 pub const fingerprint_to_user_id_set_key = "ssh-fingerprint->user-id-set"; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │222 pub const UserTab = struct { │ │223 view: UserView = .notifications, │ │224 start: usize = 0, │ │225 + // the fingerprint of the ssh key a keys route acts on ("" = none) │ │226 + key: Array(evt.User.fingerprint_len) = .{}, │ │227 + // the confirmation of removing `key` │ │228 + remove_key: bool = false, │ │229 + │ │230 + pub fn eql(a: UserTab, b: UserTab) bool { │ │231 + return a.view == b.view and a.start == b.start and std.mem.eql(u8, a.key.slice(), b.key.slice()) and a.remove_key == b.remove_key;│ │232 + } │ │233 }; │ │234 │ │235 pub const RefOrOid = enum { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │240 │ │241 // the "key:value" segments of a url tail, collected in any order │ │242 const Params = struct { │ │235 - const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from, moment, merge, user }; │ │243 + const ParamKey = enum { start, line, branch, tag, object, base, patchrev, theirs, find, search, from, moment, merge, user, key };│ │244 │ │245 // a branch:/tag:/object: param; the value stays url-encoded │ │246 const RefParam = struct { kind: RefOrOid, value: []const u8 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │334 const merge_seg = @tagName(Params.ParamKey.merge) ++ ":"; │ │335 const theirs_seg = @tagName(Params.ParamKey.theirs) ++ ":"; │ │336 const user_seg = @tagName(Params.ParamKey.user) ++ ":"; │ │337 + const key_seg = @tagName(Params.ParamKey.key) ++ ":"; │ │338 + // ends a key removal's confirmation url, so its form posts to the removal│ │339 + const remove_key_word = "removekey"; │ │340 const issue_seg = "issue:"; │ │341 const patch_seg = "patch:"; │ │342 const discuss_seg = "discuss:"; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │443 start: usize = 0, │ │444 // the user a row's form acts on ("" = none) │ │445 user: Array(evt.User.name_max_len) = .{}, │ │446 + // the confirmation of taking `user`'s role away │ │447 + confirm: ?Repo.Roles.Action = null, │ │448 }; │ │449 │ │450 pub fn repoSettingsRoute(identity: []const u8) ?RoutablePage {│ ╰───────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯