╭ changed ──────────╮ │src/event/Grant.zig│ ╰───────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │40 fn validate(event_id: *const [evt.event_id_size]u8, record: Record) !void { │ │41 if (record.event.target_id.len != evt.event_id_size) return error.InvalidTargetId; │ │42 if (record.event.user_id.len != evt.event_id_size) return error.InvalidUserId; │ │43 - if (record.event.role == .none) return error.InvalidRole; │ │43 if (!std.mem.eql(u8, event_id, &idOf(record.event.target_id, record.event.user_id))) return error.InvalidGrantId;│ │44 } │ │45 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │24 public, │ │25 }; │ │26 │ │27 -// what a user may do in a repo, ordered so a higher role includes the lower ones│ │27 +// what a user may do in a repo, ordered so a higher role includes the lower │ │28 +// ones. holding no role is a null, not a value. │ │29 pub const Role = enum { │ │29 - none, │ │30 read, │ │31 write, │ │32 owner, │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │40 pub fn canModify(self: Role, user_email: []const u8, author_email_maybe: ?[]const u8) bool {│ │41 if (self.atLeast(.write)) return true; │ │42 const author_email = author_email_maybe orelse return false; │ │43 - return self.atLeast(.read) and std.mem.eql(u8, user_email, author_email); │ │43 + return std.mem.eql(u8, user_email, author_email); │ │44 } │ │45 }; │ │46 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │47 pub const name_max_len = 32; │ │48 │ │49 -// the role `user_id_maybe` holds in the repo: its creator owns it, and anyone │ │50 -// else gets the higher of the base role and their grant. `user_moment` is the │ │51 -// owner's, which holds the grants. null is a logged-out user. │ │49 +// the role `user_id_maybe` holds in the repo, or null when they hold none: its│ │50 +// creator owns it, and anyone else gets the higher of the base role and their │ │51 +// grant. `user_moment` is the owner's, which holds the grants. a null user is │ │52 +// logged out. │ │53 pub fn roleOf( │ │54 comptime DB: type, │ │55 comptime hash_kind: hash.HashKind, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │58 record: Record, │ │59 repo_id: *const [evt.event_id_size]u8, │ │60 user_id_maybe: ?[evt.event_id_size]u8, │ │60 -) !Role { │ │61 +) !?Role { │ │62 // what the access fields give everyone, logged in or not │ │62 - const base: Role = if (record.event.write_access == .public) .write else if (record.event.read_access == .public) .read else .none; │ │63 - const user_id = user_id_maybe orelse return base; │ │63 + const base_maybe: ?Role = if (record.event.write_access == .public) .write else if (record.event.read_access == .public) .read else null;│ │64 + const user_id = user_id_maybe orelse return base_maybe; │ │65 if (std.mem.eql(u8, record.event.user_id, &user_id)) return .owner; │ │66 │ │66 - const granted = (try evt.Grant.readRole(DB, hash_kind, user_moment, arena, repo_id, &user_id)) orelse return base; │ │67 + const granted = (try evt.Grant.readRole(DB, hash_kind, user_moment, arena, repo_id, &user_id)) orelse return base_maybe; │ │68 + const base = base_maybe orelse return granted; │ │69 return if (granted.atLeast(base)) granted else base; │ │70 } │ │71 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │150 event_id: [evt.event_id_size]u8, │ │151 }; │ │152 │ │151 -// a repo as one viewer sees it │ │153 +// a repo as one viewer sees it. without a role, the viewer may not see it.│ │154 pub const RepoWithRole = struct { │ │155 repo: Record, │ │156 event_id: [evt.event_id_size]u8, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │155 - role: Role, │ │157 + role: ?Role, │ │158 }; │ │159 │ │160 // read a repo by its name from its owner's user repo│ ╰──────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/serve_ssh.zig│ ╰─────────────────╯ ╭────────────────────────────────────────────────────────────────────────╮ │576 .receive_pack => .write, │ │577 }; │ │578 // skip the key lookups when the base role is enough │ │579 - if (repo.role.atLeast(min_role)) return .{ .allowed = null }; │ │579 + if (repo.role) |base| { │ │580 + if (base.atLeast(min_role)) return .{ .allowed = null }; │ │581 + } │ │582 │ │583 // the creator owns the repo │ │584 const owner_id = repo.repo.event.user_id[0..evt.event_id_size];│ ╰────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2075 │ │2076 const owner_repo = evt.parseOwnerRepoPath(identity) orelse return .repo_not_found; │ │2077 const repo = (try evt.readRepoByOwnerAndName(io, arena.child_allocator, arena, moment, users_dir, owner_repo.owner, owner_repo.name, user_id)) orelse return .repo_not_found;│ │2078 - const role = repo.role; │ │2078 // a repo the user can't read doesn't exist to them │ │2080 - if (role == .none) return .repo_not_found; │ │2079 + const role = repo.role orelse return .repo_not_found; │ │2080 if (!role.atLeast(min_role)) return .forbidden; │ │2081 return .{ .actor = .{ │ │2082 .author = .{ .name = user.event.name, .email = user.event.email }, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Fork.zig│ ╰───────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │47 │ │48 const target = (try evt.readRepoById(io, gpa, arena, users_dir, owner_id, &target_id, session.userId())) orelse return error.NotFound; │ │49 // a draft is as readable as the repo it targets │ │50 - if (target.role == .none) return error.NotFound; │ │50 + const target_role = target.role orelse return error.NotFound; │ │51 const target_record = target.repo; │ │52 const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, arena, owner_id)) orelse return error.NotFound;│ │53 if (!std.mem.eql(u8, target_record.event.name, identity.name)) return error.NotFound; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯