╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │ 6 user_id: []const u8, │ │ 7 name: []const u8, │ │ 8 description: []const u8, │ │ 9 +read_access: Access = .private, │ │10 +write_access: Access = .private, │ │11 +read_user_ids: []const u8 = "", │ │12 +write_user_ids: []const u8 = "", │ │13 │ │14 // what the db stores: the event's data plus the commit-derived fields│ │15 pub const Record = struct { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────╮ │25 }; │ │26 │ │27 const Self = @This(); │ │28 + │ │29 +pub const Access = enum { │ │30 + private, │ │31 + public, │ │32 +}; │ │33 │ │34 pub const name_max_len = 32;│ │35 │ ╰────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │48 if (!std.ascii.isAlphanumeric(c) and c != '-' and c != '_' and c != '.')│ │49 return error.InvalidName; │ │50 } │ │51 +} │ │52 + │ │53 +fn validateUserIds(user_ids: []const u8) !void { │ │54 + var lines = std.mem.tokenizeScalar(u8, user_ids, '\n'); │ │55 + while (lines.next()) |id| _ = try evt.parseEventId(id); │ │56 } │ │57 │ │58 pub fn consume( │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │87 break :blk record; │ │88 }; │ │89 │ │76 - if (!record_to_write.removed) try validateName(record_to_write.event.name); │ │90 + if (!record_to_write.removed) { │ │91 + try validateName(record_to_write.event.name); │ │92 + try validateUserIds(record_to_write.event.read_user_ids); │ │93 + try validateUserIds(record_to_write.event.write_user_ids); │ │94 + } │ │95 │ │96 const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set"));│ │97 const user_id_to_repo_id_set = try DB.HashMap(.read_write).init(user_id_to_repo_id_set_cursor); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/serve_http.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const xit = @import("xit"); │ │3 const rp = xit.repo; │ │4 +const evt = @import("./event.zig"); │ │5 const serve_common = @import("./serve_common.zig");│ │6 │ │7 pub fn runListener( │ ╰──────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │118 try writeSimpleResponse(http_server, 403, "Forbidden", "text/plain", "push over HTTP is not supported"); │ │119 return; │ │120 } │ │121 + │ │122 + const owner_repo = evt.parseOwnerRepoPath(repo_rel) orelse { │ │123 + if (http_server.reader.state == .received_head) http_server.reader.state = .ready; │ │124 + try writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "repo path must be <owner>/<repo>"); │ │125 + return; │ │126 + }; │ │127 + const repo_path = blk: { │ │128 + var admin = rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }) catch |err| switch (err) { │ │129 + error.RepoNotFound => return error.RepoNotFound, │ │130 + else => |e| return e, │ │131 + }; │ │132 + defer admin.deinit(io, allocator); │ │133 + var arena = std.heap.ArenaAllocator.init(allocator); │ │134 + defer arena.deinit(); │ │135 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin); │ │136 + const repo = (try evt.Repo.readByOwnerAndName(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, owner_repo.owner, owner_repo.name)) orelse│ │137 + return error.RepoNotFound; │ │138 + if (repo.repo.event.read_access == .private) { │ │139 + if (http_server.reader.state == .received_head) http_server.reader.state = .ready; │ │140 + try writeSimpleResponse(http_server, 403, "Forbidden", "text/plain", "private repos cannot be fetched over HTTP"); │ │141 + return; │ │142 + } │ │143 + const repo_id = std.fmt.bytesToHex(repo.event_id, .lower); │ │144 + break :blk try std.fs.path.join(allocator, &.{ repo_root_path, &repo_id }); │ │145 + }; │ │146 + defer allocator.free(repo_path); │ │147 │ │148 const body = if (request.head.method == .POST) blk: { │ │149 const reader = try request.readerExpectContinue(&.{}); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │154 if (http_server.reader.state == .received_head) { │ │155 http_server.reader.state = .ready; │ │156 } │ │130 - │ │131 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, repo_root_path, admin_repo_path, repo_rel, false)) {│ │132 - .ok => |p| p, │ │133 - .invalid => return writeSimpleResponse(http_server, 400, "Bad Request", "text/plain", "repo path must be <owner>/<repo>"),│ │134 - .not_found => return error.RepoNotFound, │ │135 - }; │ │136 - defer allocator.free(repo_path); │ │157 │ │158 var body_reader = std.Io.Reader.fixed(body); │ │159 const http_backend_options = xit.net_server_http_backend.Options{ │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭──────────────────────────────────────╮ │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)) {│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯