╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────╮ │1 const std = @import("std"); │ │2 -const builtin = @import("builtin");│ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 const xitui = xit.xitui; │ ╰──────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │344 const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &admin_arena, fork_record.event.user_id)) orelse│ │345 return writeError(sess, "invalid patch draft"); │ │346 if (user.removed) return writeError(sess, "invalid patch draft"); │ │348 - if (builtin.mode != .Debug and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │347 + if (parsed.service == .receive_pack and !isKeyInAuthorizedKeys(user.event.ssh_keys, &sess.fingerprint)) │ │348 return writeError(sess, "unauthorized: this SSH key is not registered to the patch author"); │ │349 │ │350 const draft_path = try fork.forkPath(allocator, handler.repo_root_path, &route.id); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │386 const create_if_missing = parsed.service == .receive_pack; │ │387 const any_repo_opts: rp.AnyRepoOpts(.xit) = .{}; │ │388 │ │390 - // authenticate pushes: the authenticated key must be registered to the │ │391 - // repo's owner. │ │392 - if (create_if_missing) { │ │393 - const owner_repo = evt.parseOwnerRepoPath(repo_identity) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │394 - if (builtin.mode != .Debug and !try isKeyAuthorized(io, allocator, handler.admin_repo_path, owner_repo.owner, &sess.fingerprint)) │ │395 - return writeError(sess, "unauthorized: this SSH key is not registered to the repo owner"); │ │389 + const owner_repo = evt.parseOwnerRepoPath(repo_identity) orelse return writeError(sess, "repo path must be <owner>/<repo>"); │ │390 + switch (try authorizeRepoKey(io, allocator, handler.admin_repo_path, owner_repo.owner, owner_repo.name, parsed.service, &sess.fingerprint)) { │ │391 + .allowed => {}, │ │392 + .denied => return switch (parsed.service) { │ │393 + .upload_pack => writeError(sess, "unauthorized: this SSH key cannot read this repo"), │ │394 + .receive_pack => writeError(sess, "unauthorized: this SSH key cannot push to this repo"), │ │395 + }, │ │396 + .not_found => return writeError(sess, "repo not found"), │ │397 } │ │398 │ │399 const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.repo_root_path, handler.admin_repo_path, repo_identity, create_if_missing)) {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │498 return .{ .service = service, .dir = try allocator.dupe(u8, dir_token) };│ │499 } │ │500 │ │500 -// true if `fingerprint` matches one of the named user's registered SSH keys │ │501 -fn isKeyAuthorized( │ │501 +const RepoAuthorization = enum { allowed, denied, not_found }; │ │502 + │ │503 +fn authorizeRepoKey( │ │504 io: std.Io, │ │505 allocator: std.mem.Allocator, │ │506 admin_repo_path: []const u8, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────╮ │507 owner_name: []const u8, │ │508 + repo_name: []const u8, │ │509 + service: GitService, │ │510 fingerprint: *const [ssh.fingerprint_len]u8, │ │507 -) !bool { │ │511 +) !RepoAuthorization { │ │512 var arena = std.heap.ArenaAllocator.init(allocator);│ │513 defer arena.deinit(); │ │514 │ ╰─────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │511 - const user = (try evt.User.readByName(io, allocator, admin_repo_path, &arena, owner_name)) orelse return false; │ │515 + var admin = rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |err| switch (err) { │ │516 + error.RepoNotFound => return .not_found, │ │517 + else => |e| return e, │ │518 + }; │ │519 + defer admin.deinit(io, allocator); │ │520 │ │513 - return isKeyInAuthorizedKeys(user.event.ssh_keys, fingerprint); │ │521 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │522 + const repo = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, owner_name, repo_name)) orelse {│ │523 + if (service == .upload_pack) return .not_found; │ │524 + const owner = (try evt.User.readByName(io, allocator, admin_repo_path, &arena, owner_name)) orelse return .not_found; │ │525 + return if (isKeyInAuthorizedKeys(owner.event.ssh_keys, fingerprint)) .allowed else .denied; │ │526 + }; │ │527 + │ │528 + if ((service == .upload_pack and repo.repo.event.read_access == .public) or │ │529 + (service == .receive_pack and repo.repo.event.write_access == .public)) return .allowed; │ │530 + if (try keyBelongsToUser(moment, &arena, repo.repo.event.user_id, fingerprint)) return .allowed; │ │531 + │ │532 + const user_id_lists: []const []const u8 = switch (service) { │ │533 + .upload_pack => &.{ repo.repo.event.write_user_ids, repo.repo.event.read_user_ids }, │ │534 + .receive_pack => &.{repo.repo.event.write_user_ids}, │ │535 + }; │ │536 + for (user_id_lists) |user_ids| { │ │537 + var lines = std.mem.tokenizeScalar(u8, user_ids, '\n'); │ │538 + while (lines.next()) |id| { │ │539 + const user_id = try evt.parseEventId(id); │ │540 + if (try keyBelongsToUser(moment, &arena, &user_id, fingerprint)) return .allowed; │ │541 + } │ │542 + } │ │543 + return .denied; │ │544 +} │ │545 + │ │546 +fn keyBelongsToUser( │ │547 + moment: evt.AdminDB.HashMap(.read_only), │ │548 + arena: *std.heap.ArenaAllocator, │ │549 + user_id: []const u8, │ │550 + fingerprint: *const [ssh.fingerprint_len]u8, │ │551 +) !bool { │ │552 + const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, user_id)) orelse return false; │ │553 + return !user.removed and isKeyInAuthorizedKeys(user.event.ssh_keys, fingerprint); │ │554 } │ │555 │ │556 fn isKeyInAuthorizedKeys(ssh_keys: []const u8, fingerprint: *const [ssh.fingerprint_len]u8) bool { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯