╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │423 .fork => .{ │ │424 .fork = if (json_event.data) |value| │ │425 try std.json.parseFromValueLeaky(Fork, arena.allocator(), value, .{ .ignore_unknown_fields = true }) │ │426 + else │ │427 + null, │ │428 + }, │ │429 + .grant => .{ │ │430 + .grant = if (json_event.data) |value| │ │431 + try std.json.parseFromValueLeaky(Grant, arena.allocator(), value, .{ .ignore_unknown_fields = true })│ │432 else │ │433 null, │ │434 }, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────╮ │512 .user => .{ .user = null }, │ │513 .repo => .{ .repo = null }, │ │514 .fork => .{ .fork = null }, │ │515 + .grant => .{ .grant = null }, │ │516 .issue => .{ .issue = null }, │ │517 .discuss => .{ .discuss = null },│ │518 .comment => .{ .comment = null },│ ╰──────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1207 const record_maybe: ?Fork.Record = if (event_maybe) |event| .{ .event = event, .created_order = event_order, .updated_order = event_order } else null; │ │1208 try Fork.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │1209 }, │ │1210 + .grant => |event_maybe| { │ │1211 + const record_maybe: ?Grant.Record = if (event_maybe) |event| .{ .event = event, .created_order = event_order, .updated_order = event_order } else null;│ │1212 + try Grant.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, record_maybe, &arena, &repo_event_oid); │ │1213 + }, │ │1214 .issue => |event_maybe| { │ │1215 const record_maybe: ?Issue.Record = if (event_maybe) |event| .{ │ │1216 .event = event, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────╮ │1814 .attach => Attachment.record_map_key, │ │1815 .patchrev => PatchRev.record_map_key, │ │1816 .patch => Patch.record_map_key, │ │1774 - .user, .repo, .fork => return null, │ │1817 + .user, .repo, .fork, .grant => return null,│ │1818 }; │ │1819 │ │1820 const DB = EventDB(repo_opts.hash); │ ╰─────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────╮ │ 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 -// collaborators who hold the owner's role without being the creator │ │14 -owner_user_ids: []const u8 = "", │ │11 │ │12 // what the db stores: the event's data plus the commit-derived fields│ │13 pub const Record = struct { │ ╰──────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │46 │ │47 pub const name_max_len = 32; │ │48 │ │53 -// the role `user_id_maybe` holds in the repo. null is a logged-out user, who │ │54 -// gets the base role the access fields give everyone. │ │55 -pub fn roleOf(record: Record, user_id_maybe: ?[evt.event_id_size]u8) Role { │ │56 - const event = record.event; │ │57 - const base: Role = if (event.write_access == .public) .write else if (event.read_access == .public) .read else .none; │ │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. │ │52 +pub fn roleOf( │ │53 + comptime DB: type, │ │54 + comptime hash_kind: hash.HashKind, │ │55 + user_moment: DB.HashMap(.read_only), │ │56 + arena: *std.heap.ArenaAllocator, │ │57 + record: Record, │ │58 + repo_id: *const [evt.event_id_size]u8, │ │59 + user_id_maybe: ?[evt.event_id_size]u8, │ │60 +) !Role { │ │61 + // 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; │ │59 - if (std.mem.eql(u8, event.user_id, &user_id)) return .owner; │ │64 + if (std.mem.eql(u8, record.event.user_id, &user_id)) return .owner; │ │65 │ │61 - const user_id_hex = std.fmt.bytesToHex(user_id, .lower); │ │62 - if (containsUserId(event.owner_user_ids, &user_id_hex)) return .owner; │ │63 - if (base == .write or containsUserId(event.write_user_ids, &user_id_hex)) return .write; │ │64 - if (base == .read or containsUserId(event.read_user_ids, &user_id_hex)) return .read; │ │65 - return .none; │ │66 + const granted = (try evt.Grant.readRole(DB, hash_kind, user_moment, arena, repo_id, &user_id)) orelse return base; │ │67 + return if (granted.atLeast(base)) granted else base; │ │68 } │ │69 │ │68 -fn containsUserId(user_ids: []const u8, user_id_hex: []const u8) bool { │ │69 - var lines = std.mem.tokenizeScalar(u8, user_ids, '\n'); │ │70 - while (lines.next()) |id| { │ │71 - if (std.mem.eql(u8, id, user_id_hex)) return true; │ │72 - } │ │73 - return false; │ │74 -} │ │75 - │ │70 // the moment keys `evt.merge` reads and writes for this kind │ │71 pub const merge_policy: evt.MergePolicy = .target_wins; │ │72 pub const record_map_key = "event-id->repo"; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────╮ │84 } │ │85 } │ │86 │ │93 -fn validateUserIds(user_ids: []const u8) !void { │ │94 - var lines = std.mem.tokenizeScalar(u8, user_ids, '\n');│ │95 - while (lines.next()) |id| _ = try evt.parseEventId(id);│ │96 -} │ │97 - │ │87 pub fn consume( │ │88 comptime DB: type, │ │89 comptime hash_kind: hash.HashKind, │ ╰───────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │110 │ │111 var record_to_write = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_record_maybe);│ │112 │ │124 - if (!record_to_write.removed) { │ │125 - try validateName(record_to_write.event.name); │ │126 - try validateUserIds(record_to_write.event.read_user_ids); │ │127 - try validateUserIds(record_to_write.event.write_user_ids); │ │128 - try validateUserIds(record_to_write.event.owner_user_ids); │ │129 - } │ │113 + if (!record_to_write.removed) try validateName(record_to_write.event.name); │ │114 │ │115 if (existing_record_maybe) |existing_record| { │ │116 // updates preserve the original creation metadata │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────╮ │148 event_id: [evt.event_id_size]u8, │ │149 }; │ │150 │ │151 +// a repo as one viewer sees it │ │152 +pub const RepoWithRole = struct { │ │153 + repo: Record, │ │154 + event_id: [evt.event_id_size]u8, │ │155 + role: Role, │ │156 +}; │ │157 + │ │158 // read a repo by its name from its owner's user repo│ │159 pub fn readByName( │ │160 comptime DB: type, │ ╰──────────────────────────────────────────────────────────╯ ╭ added ────────────╮ │src/event/Grant.zig│ ╰───────────────────╯ ╭──────╮ │next →│ ╰──────╯