╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │380 if (std.mem.indexOfScalar(u8, repo_identity, '+') != null) { │ │381 return runForkSession(handler, sess, repo_identity, parsed.service, protocol_version, &reader.interface, &writer.interface);│ │382 } │ │383 - │ │384 - const create_if_missing = parsed.service == .receive_pack; │ │383 │ │384 const owner_repo = evt.parseOwnerRepoPath(repo_identity) orelse return writeError(sess, "repo path must be <owner>:<repo>"); │ │385 var author_arena = std.heap.ArenaAllocator.init(allocator); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │386 defer author_arena.deinit(); │ │387 const author = switch (try authorizeRepoKey(io, &author_arena, handler.admin_repo_path, handler.users_dir, owner_repo.owner, owner_repo.name, parsed.service, &sess.fingerprint)) {│ │388 .allowed => |user| user, │ │389 + .create => |creator| return runCreatingPush(handler, sess, &creator, owner_repo.name, protocol_version, &reader.interface, &writer.interface), │ │390 .denied => return switch (parsed.service) { │ │391 .upload_pack => writeError(sess, "unauthorized: this SSH key cannot read this repo"), │ │392 .receive_pack => writeError(sess, "unauthorized: this SSH key cannot push to this repo"), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │394 .not_found => return writeError(sess, "repo not found"), │ │395 }; │ │396 │ │398 - const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.users_dir, handler.admin_repo_path, repo_identity, create_if_missing)) {│ │397 + const repo_path = switch (try serve_common.resolveRepoPath(io, allocator, handler.users_dir, handler.admin_repo_path, repo_identity)) { │ │398 .ok => |p| p, │ │399 .invalid => unreachable, // parsed above │ │400 .not_found => return writeError(sess, "repo not found"), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │401 }; │ │402 defer allocator.free(repo_path); │ │403 + │ │404 + var any_repo = try rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = repo_path }); │ │405 + defer any_repo.deinit(io, allocator); │ │406 + │ │407 + switch (any_repo) { │ │408 + inline else => |*repo| switch (parsed.service) { │ │409 + .upload_pack => { │ │410 + var sideband = progress.Sideband{ .writer = &writer.interface, .sess = sess }; │ │411 + try repo.uploadPack(io, allocator, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version }, &sideband); │ │412 + }, │ │413 + .receive_pack => try push.receivePackAndConsume(repo.self_repo_opts, io, allocator, repo, &reader.interface, &writer.interface, .{ .protocol_version = protocol_version }, author, handler.users_dir, handler.err, sess),│ │414 + }, │ │415 + } │ │416 +} │ │417 │ │405 - if (try serveIfExists(repo_path, handler.users_dir, &reader.interface, &writer.interface, io, allocator, parsed.service, protocol_version, author, handler.err, sess)) return; │ │418 +// push to a repo that doesn't exist yet, creating it in the hash format the │ │419 +// client's first command names │ │420 +fn runCreatingPush( │ │421 + handler: *const SessionHandler, │ │422 + sess: *ssh.SessionCtx, │ │423 + creator: *const RepoCreator, │ │424 + name: []const u8, │ │425 + protocol_version: xit.net_server_common.ProtocolVersion, │ │426 + reader: *std.Io.Reader, │ │427 + writer: *std.Io.Writer, │ │428 +) !void { │ │429 + const allocator = sess.conn.allocator; │ │430 + const io = sess.conn.io; │ │431 │ │407 - if (!create_if_missing) return writeError(sess, "repo not found"); │ │432 + evt.Repo.validateName(name) catch return writeError(sess, "invalid repo name"); │ │433 + │ │434 + try xit.net_server_receive_pack.advertiseUncreated(.xit, writer, .{ .protocol_version = protocol_version }); │ │435 + try writer.flush(); │ │436 + // a client with nothing to push creates nothing │ │437 + const hash_kind = (try xit.net_server_receive_pack.peekObjectFormat(reader)) orelse return; │ │438 + │ │439 + const location = evt.createRepo(io, allocator, handler.users_dir, &creator.owner_id, creator.owner, name, "", .private, hash_kind) catch |err| switch (err) { │ │440 + error.NameTaken => return writeError(sess, "repo was just created by another push, try again"), │ │441 + else => |e| return e, │ │442 + }; │ │443 + const repo_path = try evt.repoPath(allocator, handler.users_dir, &location.owner_id, &location.repo_id); │ │444 + defer allocator.free(repo_path); │ │445 │ │409 - // create the on-disk repo for the just-minted event and serve the push │ │410 - var repo = try rp.Repo(.xit, any_repo_opts.toRepoOpts()).init(io, allocator, .{ .path = repo_path, .bare = true }); │ │411 - defer repo.deinit(io, allocator); │ │412 - try servePack(repo.self_repo_opts, &repo, handler.users_dir, &reader.interface, &writer.interface, io, allocator, parsed.service, protocol_version, author, handler.err, sess); │ │446 + switch (hash_kind) { │ │447 + inline else => |kind| { │ │448 + var repo = try rp.Repo(.xit, any_repo_opts.toRepoOptsWithHash(kind)).open(io, allocator, .{ .path = repo_path }); │ │449 + defer repo.deinit(io, allocator); │ │450 + // stateless since the advertisement already went out │ │451 + try push.receivePackAndConsume(repo.self_repo_opts, io, allocator, &repo, reader, writer, .{ .protocol_version = protocol_version, .is_stateless = true }, creator.owner, handler.users_dir, handler.err, sess); │ │452 + }, │ │453 + } │ │454 } │ │455 │ │456 // serve a patch draft: anyone may fetch it, only its author may push to it │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │516 } │ │517 } │ │518 │ │478 -// serve an existing repo at `repo_path`, or return false if none is there │ │479 -fn serveIfExists( │ │480 - repo_path: []const u8, │ │481 - users_dir: []const u8, │ │482 - reader: *std.Io.Reader, │ │483 - writer: *std.Io.Writer, │ │484 - io: std.Io, │ │485 - allocator: std.mem.Allocator, │ │486 - service: GitService, │ │487 - protocol_version: xit.net_server_common.ProtocolVersion, │ │488 - author: ?evt.CommitAuthor, │ │489 - error_writer: *std.Io.Writer, │ │490 - sess: ?*ssh.SessionCtx, │ │491 -) !bool { │ │492 - // a bare open() creates the directory while probing for the repo, so only │ │493 - // attempt it when the path already exists │ │494 - std.Io.Dir.accessAbsolute(io, repo_path, .{}) catch return false; │ │495 - │ │496 - var any_repo = rp.AnyRepo(.xit, any_repo_opts).open(io, allocator, .{ .path = repo_path }) catch |err| switch (err) { │ │497 - error.RepoNotFound => return false, │ │498 - else => |e| return e, │ │499 - }; │ │500 - defer any_repo.deinit(io, allocator); │ │501 - │ │502 - switch (any_repo) { │ │503 - inline else => |*repo| try servePack(repo.self_repo_opts, repo, users_dir, reader, writer, io, allocator, service, protocol_version, author, error_writer, sess), │ │504 - } │ │505 - return true; │ │506 -} │ │507 - │ │508 -fn servePack( │ │509 - comptime repo_opts: rp.RepoOpts(.xit), │ │510 - repo: *rp.Repo(.xit, repo_opts), │ │511 - users_dir: []const u8, │ │512 - reader: *std.Io.Reader, │ │513 - writer: *std.Io.Writer, │ │514 - io: std.Io, │ │515 - allocator: std.mem.Allocator, │ │516 - service: GitService, │ │517 - protocol_version: xit.net_server_common.ProtocolVersion, │ │518 - author: ?evt.CommitAuthor, │ │519 - error_writer: *std.Io.Writer, │ │520 - sess: ?*ssh.SessionCtx, │ │521 -) !void { │ │522 - switch (service) { │ │523 - .upload_pack => { │ │524 - var sideband = progress.Sideband{ .writer = writer, .sess = sess }; │ │525 - try repo.uploadPack(io, allocator, reader, writer, .{ .protocol_version = protocol_version }, &sideband); │ │526 - }, │ │527 - .receive_pack => try push.receivePackAndConsume(repo_opts, io, allocator, repo, reader, writer, .{ .protocol_version = protocol_version }, author, users_dir, error_writer, sess),│ │528 - } │ │529 -} │ │530 - │ │519 fn parseGitCommand(allocator: std.mem.Allocator, command: []const u8) !ParsedGitCommand { │ │520 var tokens = try std.process.Args.IteratorGeneral(.{ .single_quotes = true }).init(allocator, command); │ │521 defer tokens.deinit(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │533 │ │534 return .{ .service = service, .dir = try allocator.dupe(u8, dir_token) }; │ │535 } │ │536 + │ │537 +const RepoAuthorization = union(enum) { allowed: ?evt.CommitAuthor, create: RepoCreator, denied, not_found };│ │538 │ │549 -const RepoAuthorization = union(enum) { allowed: ?evt.CommitAuthor, denied, not_found }; │ │539 +// the owner of a repo a push may create │ │540 +const RepoCreator = struct { │ │541 + owner_id: [evt.event_id_size]u8, │ │542 + owner: evt.CommitAuthor, │ │543 +}; │ │544 │ │545 fn authorizeRepoKey( │ │546 io: std.Io, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │563 // no viewer yet, so this carries the base role │ │564 const repo = (try evt.readRepoByOwnerAndName(io, allocator, arena, moment, users_dir, owner_name, repo_name, null)) orelse { │ │565 if (service == .upload_pack) return .not_found; │ │572 - const owner = (try evt.User.readByName(io, allocator, admin_repo_path, arena, owner_name)) orelse return .not_found; │ │566 + const owner_id = (try evt.User.readIdByName(evt.AdminDB, evt.admin_repo_opts.hash, moment, owner_name)) orelse return .not_found;│ │567 + const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &owner_id)) orelse unreachable; │ │568 return if (isKeyInAuthorizedKeys(owner.event.ssh_keys, fingerprint)) │ │574 - .{ .allowed = .{ .name = owner.event.name, .email = owner.event.email } } │ │569 + .{ .create = .{ .owner_id = owner_id, .owner = .{ .name = owner.event.name, .email = owner.event.email } } } │ │570 else │ │571 .denied; │ │572 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯