╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │490 }, │ │491 }); │ │492 } │ │493 - │ │494 -const RequestAuthor = struct { │ │495 - author: evt.CommitAuthor, │ │496 - user_id: ?[evt.event_id_size]u8 = null, │ │497 -}; │ │493 │ │499 -// the author and user id for an event created by this request, or null when the│ │500 -// request may not create one. local mode has no accounts, so it has no user id.│ │501 -fn eventAuthor( │ │494 +// the actor for a write to the repo `repo_base` names with at least `min_role`,│ │495 +// or null once the refusal has been sent. local mode always writes. │ │496 +fn authorizeWrite( │ │497 io: std.Io, │ │498 allocator: std.mem.Allocator, │ │499 arena: *std.heap.ArenaAllocator, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │500 request: *std.http.Server.Request, │ │501 host: Host, │ │507 -) !?RequestAuthor { │ │502 + repo_base: []const u8, │ │503 + min_role: evt.Repo.Role, │ │504 +) !?ui.Actor { │ │505 const server = switch (host) { │ │506 .server => |server| server, │ │510 - .local => |src| return .{ .author = try ui.localAuthor(src, io, allocator, arena) }, │ │507 + .local => |src| return .{ .author = try ui.localAuthor(src, io, allocator, arena), .role = .owner }, │ │508 }; │ │512 - const token = getCookieValue(request, cookie_name) orelse return null; │ │513 - var user_id: [evt.event_id_size]u8 = undefined; │ │514 - if (!server.session_store.lookup(token, &user_id)) return null; │ │509 + const authorization: ui.Authorization = blk: { │ │510 + const token = getCookieValue(request, cookie_name) orelse break :blk .login_required; │ │511 + var user_id: [evt.event_id_size]u8 = undefined; │ │512 + if (!server.session_store.lookup(token, &user_id)) break :blk .login_required; │ │513 │ │516 - var repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ │517 - defer repo.deinit(io, allocator); │ │518 - const moment = try evt.currentMoment(evt.admin_repo_opts, &repo); │ │519 - const user = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, arena, &user_id)) orelse return null;│ │520 - return .{ │ │521 - .author = .{ .name = user.event.name, .email = user.event.email }, │ │522 - .user_id = user_id, │ │514 + const repo_prefix = "/repo/"; │ │515 + if (!std.mem.startsWith(u8, repo_base, repo_prefix)) break :blk .repo_not_found; │ │516 + │ │517 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = server.admin_repo_path }); │ │518 + defer admin_repo.deinit(io, allocator); │ │519 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │520 + break :blk try ui.authorizeUser(moment, arena, user_id, repo_base[repo_prefix.len..], min_role); │ │521 }; │ │522 + switch (authorization) { │ │523 + .actor => |actor| return actor, │ │524 + .login_required => try respondLoginRequired(request), │ │525 + .repo_not_found => try respondRepoNotFound(request), │ │526 + .forbidden => try respondForbidden(request), │ │527 + } │ │528 + return null; │ │529 } │ │530 │ │531 // refuse a write from a request with no logged-in user. the body goes unread, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │533 fn respondLoginRequired(request: *std.http.Server.Request) !void { │ │534 try request.respond("log in to make changes", .{ │ │535 .status = .forbidden, │ │536 + .keep_alive = false, │ │537 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ │538 + }); │ │539 +} │ │540 + │ │541 +// refuse a write the user's role in the repo doesn't allow │ │542 +fn respondForbidden(request: *std.http.Server.Request) !void { │ │543 + try request.respond("you don't have permission to do that", .{ │ │544 + .status = .forbidden, │ │545 + .keep_alive = false, │ │546 + .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ │547 + }); │ │548 +} │ │549 + │ │550 +fn respondRepoNotFound(request: *std.http.Server.Request) !void { │ │551 + try request.respond("repo not found", .{ │ │552 + .status = .not_found, │ │553 .keep_alive = false, │ │554 .extra_headers = &.{.{ .name = "content-type", .value = "text/plain" }},│ │555 }); │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │593 ) !void { │ │594 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │595 defer author_arena.deinit(); │ │574 - const request_author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │575 - const author = request_author.author; │ │596 + const actor = (try authorizeWrite(io, allocator, &author_arena, request, host, base, .read)) orelse return; │ │597 + const author = actor.author; │ │598 const body = try readFormBody(request, allocator); │ │599 defer allocator.free(body); │ │600 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │658 .server => |server| server, │ │659 .local => return respondRemoveNotFound(request), │ │660 }; │ │639 - const user_id = request_author.user_id orelse return respondLoginRequired(request); │ │661 + const user_id = actor.user_id orelse return respondLoginRequired(request); │ │662 if (!evt.Patch.branchValid(target_branch) or !try request_repo.source.hasBranch(io, allocator, target_branch)) {│ │663 const form_location = try std.fmt.allocPrint(allocator, "{s}/patches/new", .{base}); │ │664 defer allocator.free(form_location); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │761 }; │ │762 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │763 defer author_arena.deinit(); │ │742 - const request_author = (try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request);│ │743 - const user_id = request_author.user_id orelse return respondLoginRequired(request); │ │744 - const author = request_author.author; │ │764 + // only the draft's author may publish it, which publish checks │ │765 + const actor = (try authorizeWrite(io, allocator, &author_arena, request, host, parts.repo_base, .read)) orelse return; │ │766 + const user_id = actor.user_id orelse return respondLoginRequired(request); │ │767 + const author = actor.author; │ │768 │ │769 const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │770 defer request_repo.deinit(allocator); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │807 }; │ │808 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │809 defer author_arena.deinit(); │ │787 - const author = ((try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request)).author;│ │810 + const author = ((try authorizeWrite(io, allocator, &author_arena, request, host, parts.repo_base, .write)) orelse return).author; │ │811 │ │812 const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │813 defer request_repo.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │862 │ │863 var author_arena = std.heap.ArenaAllocator.init(allocator); │ │864 defer author_arena.deinit(); │ │842 - const author = ((try eventAuthor(io, allocator, &author_arena, request, host)) orelse return respondLoginRequired(request)).author;│ │865 + const author = ((try authorizeWrite(io, allocator, &author_arena, request, host, parts.repo_base, .read)) orelse return).author; │ │866 │ │867 const form_body = try readFormBody(request, allocator); │ │868 defer allocator.free(form_body); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────╮ │1040 │ │1041 const AttachParentParts = struct { │ │1042 repo_base: []const u8, │ │1043 + kind: evt.EventKind, │ │1044 id_bytes: [evt.event_id_size]u8,│ │1045 }; │ │1046 │ ╰──────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1050 const segment_at = std.mem.lastIndexOfScalar(u8, base, '/') orelse return null; │ │1051 const segment = base[segment_at + 1 ..]; │ │1052 const colon_at = std.mem.lastIndexOfScalar(u8, segment, ':') orelse return null; │ │1053 + const kind = std.meta.stringToEnum(evt.EventKind, segment[0..colon_at]) orelse return null;│ │1054 const id_bytes = evt.parseEventId(segment[colon_at + 1 ..]) catch return null; │ │1030 - return .{ .repo_base = base[0..segment_at], .id_bytes = id_bytes }; │ │1055 + return .{ .repo_base = base[0..segment_at], .kind = kind, .id_bytes = id_bytes }; │ │1056 } │ │1057 │ │1058 // attach the uploaded file to the event the url names, then reload. the file is │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯