1use crate::error::{human_bytes, Error, Result};
30use crate::model::{now_ms, Level, PrincipalKind, UnixMillis};
31
32use super::{AuthDoc, AuthTable};
33
34#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct Clan {
39 pub name: String,
41 pub created_by: String,
43 pub created_at: UnixMillis,
45 #[serde(default)]
47 pub members: Vec<ClanMember>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 pub invites: Vec<ClanInvite>,
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub offers: Vec<ClanRepoOffer>,
54}
55
56#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
60#[serde(deny_unknown_fields)]
61pub struct ClanRepoOffer {
62 pub repo: String,
64 pub to: String,
66 pub by: String,
68 pub at: UnixMillis,
70}
71
72#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
74#[serde(deny_unknown_fields)]
75pub struct ClanMember {
76 pub principal: String,
78 #[serde(default)]
80 pub admin: bool,
81 #[serde(default)]
83 pub contribution_bytes: u64,
84 pub joined_at: UnixMillis,
86}
87
88#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
90#[serde(deny_unknown_fields)]
91pub struct ClanInvite {
92 pub principal: String,
94 pub by: String,
96 pub at: UnixMillis,
98}
99
100#[derive(Clone, Copy, PartialEq, Eq, Debug)]
102pub enum ClanRole {
103 Admin,
105 Member,
107 Invited,
109}
110
111#[derive(Clone, Copy, PartialEq, Eq, Debug)]
113pub enum ClanRefusal {
114 NotFound,
116 Denied,
118 Precondition,
120 Invalid,
122 Exists,
124}
125
126#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
129pub struct AccountRoom {
130 pub cap: Option<u64>,
132 pub stored: u64,
134}
135
136impl Clan {
137 pub fn room_bytes(&self) -> u64 {
139 self.members
140 .iter()
141 .map(|m| m.contribution_bytes)
142 .fold(0, u64::saturating_add)
143 }
144
145 pub fn member(&self, principal: &str) -> Option<&ClanMember> {
147 self.members.iter().find(|m| m.principal == principal)
148 }
149
150 pub fn role_of(&self, principal: &str) -> Option<ClanRole> {
152 match self.member(principal) {
153 Some(m) if m.admin => Some(ClanRole::Admin),
154 Some(_) => Some(ClanRole::Member),
155 None if self.invites.iter().any(|i| i.principal == principal) => Some(ClanRole::Invited),
156 None => None,
157 }
158 }
159
160 pub fn admins(&self) -> usize {
162 self.members.iter().filter(|m| m.admin).count()
163 }
164
165 pub fn floor_for(&self, member: &str, used: u64) -> u64 {
169 self.floor_in_force(member, used, &[])
170 }
171
172 pub fn floor_in_force(&self, member: &str, used: u64, lapsed: &[String]) -> u64 {
177 if lapsed.iter().any(|l| l == member) {
178 return 0;
179 }
180 let others: u64 = self
181 .members
182 .iter()
183 .filter(|m| m.principal != member && !lapsed.contains(&m.principal))
184 .map(|m| m.contribution_bytes)
185 .fold(0, u64::saturating_add);
186 used.saturating_sub(others)
187 }
188
189 pub fn room_in_force(&self, lapsed: &[String]) -> u64 {
192 self.members
193 .iter()
194 .filter(|m| !lapsed.contains(&m.principal))
195 .map(|m| m.contribution_bytes)
196 .fold(0, u64::saturating_add)
197 }
198
199 pub fn offer_of(&self, repo: &str) -> Option<&ClanRepoOffer> {
201 self.offers.iter().find(|o| o.repo == repo)
202 }
203}
204
205fn refused(clan: &str, refusal: ClanRefusal, reason: String) -> Error {
206 Error::Clan {
207 clan: clan.to_owned(),
208 refusal,
209 reason,
210 }
211}
212
213impl AuthTable {
214 pub fn clan(&self, name: &str) -> Result<Option<Clan>> {
218 let doc = self.doc.load()?;
219 if doc.clans.is_empty() {
220 return Ok(None);
221 }
222 doc.clans.get(name)
223 }
224
225 pub fn is_clan(&self, name: &str) -> Result<bool> {
227 Ok(self.clan(name)?.is_some())
228 }
229
230 pub fn clans_of(&self, principal: &str) -> Result<Vec<Clan>> {
232 let doc = self.doc.load()?;
233 let mut out = Vec::new();
234 for name in doc.clan_index.get(principal)?.unwrap_or_default() {
235 if let Some(clan) = doc.clans.get(&name)? {
236 out.push(clan);
237 }
238 }
239 Ok(out)
240 }
241
242 pub fn clan_role(&self, principal: &str, namespace: &str) -> Result<Option<ClanRole>> {
245 Ok(self.clan(namespace)?.and_then(|clan| clan.role_of(principal)))
246 }
247
248 pub fn contributed_by(&self, account: &str) -> Result<u64> {
251 let doc = self.doc.load()?;
252 Self::contributed_in(&doc, account, None)
253 }
254
255 pub fn room_left(&self, account: &str, room: AccountRoom) -> Result<Option<u64>> {
259 let Some(cap) = room.cap else { return Ok(None) };
260 let doc = self.doc.load()?;
261 let committed = room
262 .stored
263 .saturating_add(Self::promised_by(&doc, account)?)
264 .saturating_add(Self::contributed_in(&doc, account, None)?);
265 Ok(Some(cap.saturating_sub(committed)))
266 }
267
268 pub(super) fn clan_level(doc: &AuthDoc, principal: &str, namespace: &str) -> Result<Option<Level>> {
273 if doc.clans.is_empty() {
274 return Ok(None);
275 }
276 let Some(clan) = doc.clans.get(namespace)? else {
277 return Ok(None);
278 };
279 Ok(clan.member(principal).map(|m| if m.admin { Level::Admin } else { Level::Write }))
280 }
281
282 pub(super) fn contributed_in(doc: &AuthDoc, account: &str, except: Option<&str>) -> Result<u64> {
284 if doc.clan_index.is_empty() {
285 return Ok(0);
286 }
287 let mut sum: u64 = 0;
288 for name in doc.clan_index.get(account)?.unwrap_or_default() {
289 if Some(name.as_str()) == except {
290 continue;
291 }
292 if let Some(clan) = doc.clans.get(&name)? {
293 if let Some(m) = clan.member(account) {
294 sum = sum.saturating_add(m.contribution_bytes);
295 }
296 }
297 }
298 Ok(sum)
299 }
300
301 pub fn clan_for(&self, clan: &str, by: &str) -> Result<Clan> {
305 let doc = self.doc.load()?;
306 Self::clan_for_admin(&doc, clan, by, "dissolve it")
307 }
308
309 pub fn create_clan(&self, name: &str, creator: &str, contribution: u64, room: AccountRoom) -> Result<Clan> {
316 crate::sandbox::check_component(name, name)?;
317 if name == crate::model::ANONYMOUS_PRINCIPAL_ID {
318 return Err(Error::ReservedPrincipalId(name.to_owned()));
319 }
320 let at = now_ms();
321 self.doc.update(|doc| {
322 Self::ordinary_account(doc, name, creator, "make a clan")?;
323 if doc.clans.contains_key(name)? {
324 return Err(refused(name, ClanRefusal::Exists, format!("a clan called {name}")));
325 }
326 if doc.principals.contains_key(name)? {
327 return Err(refused(name, ClanRefusal::Exists, format!("an account called {name}")));
328 }
329 Self::fits_in_room(doc, name, creator, contribution, room)?;
330 let clan = Clan {
331 name: name.to_owned(),
332 created_by: creator.to_owned(),
333 created_at: at,
334 members: vec![ClanMember {
335 principal: creator.to_owned(),
336 admin: true,
337 contribution_bytes: contribution,
338 joined_at: at,
339 }],
340 invites: Vec::new(),
341 offers: Vec::new(),
342 };
343 doc.clans.insert(name.to_owned(), clan.clone())?;
344 Self::index_clan(doc, creator, name)?;
345 Ok(clan)
346 })
347 }
348
349 pub fn invite_to_clan(&self, clan: &str, by: &str, principal: &str) -> Result<Clan> {
352 let at = now_ms();
353 self.doc.update(|doc| {
354 let mut row = Self::clan_for_admin(doc, clan, by, "invite an account")?;
355 Self::ordinary_account(doc, clan, principal, "join a clan")?;
356 match row.role_of(principal) {
357 Some(ClanRole::Admin | ClanRole::Member) => {
358 return Err(refused(clan, ClanRefusal::Precondition, format!("{principal} is already in clan {clan}")))
359 }
360 Some(ClanRole::Invited) => {
361 return Err(refused(
362 clan,
363 ClanRefusal::Precondition,
364 format!("{principal} is already invited to clan {clan}, and has not answered"),
365 ))
366 }
367 None => {}
368 }
369 row.invites.push(ClanInvite {
370 principal: principal.to_owned(),
371 by: by.to_owned(),
372 at,
373 });
374 doc.clans.insert(clan.to_owned(), row.clone())?;
375 Self::index_clan(doc, principal, clan)?;
376 Ok(row)
377 })
378 }
379
380 pub fn answer_clan_invite(
385 &self,
386 clan: &str,
387 principal: &str,
388 accept: bool,
389 contribution: u64,
390 room: AccountRoom,
391 ) -> Result<Clan> {
392 let at = now_ms();
393 self.doc.update(|doc| {
394 let mut row = Self::visible_clan(doc, clan, principal)?;
395 if row.role_of(principal) != Some(ClanRole::Invited) {
396 return Err(refused(
397 clan,
398 ClanRefusal::Precondition,
399 format!("{principal} has no invitation to clan {clan} to answer"),
400 ));
401 }
402 row.invites.retain(|i| i.principal != principal);
403 if accept {
404 Self::ordinary_account(doc, clan, principal, "join a clan")?;
409 Self::fits_in_room(doc, clan, principal, contribution, room)?;
410 row.members.push(ClanMember {
411 principal: principal.to_owned(),
412 admin: false,
413 contribution_bytes: contribution,
414 joined_at: at,
415 });
416 } else {
417 Self::unindex_clan(doc, principal, clan)?;
418 }
419 doc.clans.insert(clan.to_owned(), row.clone())?;
420 Ok(row)
421 })
422 }
423
424 pub fn withdraw_clan_invite(&self, clan: &str, by: &str, principal: &str) -> Result<Clan> {
426 self.doc.update(|doc| {
427 let mut row = Self::clan_for_admin(doc, clan, by, "withdraw an invitation")?;
428 if row.role_of(principal) != Some(ClanRole::Invited) {
429 return Err(refused(
430 clan,
431 ClanRefusal::Precondition,
432 format!("{principal} has no invitation to clan {clan}"),
433 ));
434 }
435 row.invites.retain(|i| i.principal != principal);
436 Self::unindex_clan(doc, principal, clan)?;
437 doc.clans.insert(clan.to_owned(), row.clone())?;
438 Ok(row)
439 })
440 }
441
442 pub fn set_clan_contribution(
447 &self,
448 clan: &str,
449 member: &str,
450 bytes: u64,
451 room: AccountRoom,
452 used: Option<u64>,
453 lapsed: &[String],
454 ) -> Result<Clan> {
455 self.doc.update(|doc| {
456 let mut row = Self::visible_clan(doc, clan, member)?;
457 Self::refuse_gated(doc, clan, member)?;
458 let Some(now) = row.member(member).map(|m| m.contribution_bytes) else {
459 return Err(refused(
460 clan,
461 ClanRefusal::Denied,
462 format!("{member} is not a member of clan {clan}: only a member gives it room"),
463 ));
464 };
465 if bytes > now {
466 Self::fits_in_room(doc, clan, member, bytes, room)?;
467 } else if bytes < now {
468 Self::above_floor(&row, clan, member, bytes, used, lapsed, "lower its contribution")?;
469 }
470 for m in &mut row.members {
471 if m.principal == member {
472 m.contribution_bytes = bytes;
473 }
474 }
475 doc.clans.insert(clan.to_owned(), row.clone())?;
476 Ok(row)
477 })
478 }
479
480 pub fn leave_clan(&self, clan: &str, member: &str, used: Option<u64>, lapsed: &[String]) -> Result<Clan> {
484 self.doc.update(|doc| {
485 let mut row = Self::visible_clan(doc, clan, member)?;
486 Self::refuse_gated(doc, clan, member)?;
487 let Some(me) = row.member(member).cloned() else {
488 return Err(refused(clan, ClanRefusal::Precondition, format!("{member} is not a member of clan {clan}")));
489 };
490 if me.admin && row.admins() == 1 {
491 return Err(refused(
492 clan,
493 ClanRefusal::Precondition,
494 format!(
495 "{member} is clan {clan}'s last admin, and the last admin cannot leave: make another \
496 member admin first, or dissolve the clan"
497 ),
498 ));
499 }
500 Self::above_floor(&row, clan, member, 0, used, lapsed, "leave")?;
501 row.members.retain(|m| m.principal != member);
502 Self::unindex_clan(doc, member, clan)?;
503 doc.clans.insert(clan.to_owned(), row.clone())?;
504 Ok(row)
505 })
506 }
507
508 pub fn remove_from_clan(&self, clan: &str, by: &str, member: &str, used: Option<u64>, lapsed: &[String]) -> Result<Clan> {
512 self.doc.update(|doc| {
513 let mut row = Self::clan_for_admin(doc, clan, by, "remove a member")?;
514 if member == by {
515 return Err(refused(
516 clan,
517 ClanRefusal::Invalid,
518 format!("{by} cannot remove itself from clan {clan}: that is leaving"),
519 ));
520 }
521 if row.member(member).is_none() {
522 return Err(refused(clan, ClanRefusal::Precondition, format!("{member} is not a member of clan {clan}")));
523 }
524 Self::above_floor(&row, clan, member, 0, used, lapsed, "be removed")?;
525 row.members.retain(|m| m.principal != member);
526 Self::unindex_clan(doc, member, clan)?;
527 doc.clans.insert(clan.to_owned(), row.clone())?;
528 Ok(row)
529 })
530 }
531
532 pub fn set_clan_admin(&self, clan: &str, by: &str, member: &str, admin: bool) -> Result<Clan> {
535 self.doc.update(|doc| {
536 let mut row = Self::clan_for_admin(doc, clan, by, "change who administers it")?;
537 let Some(was) = row.member(member).map(|m| m.admin) else {
538 return Err(refused(clan, ClanRefusal::Precondition, format!("{member} is not a member of clan {clan}")));
539 };
540 if was && !admin && row.admins() == 1 {
541 return Err(refused(
542 clan,
543 ClanRefusal::Precondition,
544 format!("{member} is clan {clan}'s last admin: make another member admin first"),
545 ));
546 }
547 for m in &mut row.members {
548 if m.principal == member {
549 m.admin = admin;
550 }
551 }
552 doc.clans.insert(clan.to_owned(), row.clone())?;
553 Ok(row)
554 })
555 }
556
557 pub fn dissolve_clan(&self, clan: &str, by: &str) -> Result<Clan> {
561 self.doc.update(|doc| {
562 let row = Self::clan_for_admin(doc, clan, by, "dissolve it")?;
563 for m in &row.members {
564 Self::unindex_clan(doc, &m.principal, clan)?;
565 }
566 for i in &row.invites {
567 Self::unindex_clan(doc, &i.principal, clan)?;
568 }
569 doc.clans.remove(clan)?;
570 Ok(row)
571 })
572 }
573
574 pub fn offer_clan_repo(&self, clan: &str, by: &str, repo: &str, to: &str) -> Result<Clan> {
582 let at = now_ms();
583 self.doc.update(|doc| {
584 let mut row = Self::clan_for_admin(doc, clan, by, "move a repository out of it")?;
585 if to == clan || doc.clans.contains_key(to)? {
586 return Err(refused(
587 clan,
588 ClanRefusal::Invalid,
589 format!("{to} is a clan: a repository moves out of a clan into an account"),
590 ));
591 }
592 let Some(account) = doc.principals.get(to)? else {
593 return Err(refused(clan, ClanRefusal::NotFound, format!("there is no account {to}")));
594 };
595 if account.kind != PrincipalKind::User || doc.employers.contains_key(to)? {
596 return Err(refused(
597 clan,
598 ClanRefusal::Precondition,
599 format!("{to} holds no account of its own to move {clan}/{repo} into"),
600 ));
601 }
602 if let Some(open) = row.offer_of(repo) {
603 return Err(refused(
604 clan,
605 ClanRefusal::Precondition,
606 format!("{clan}/{repo} is already offered to {}; withdraw that offer first", open.to),
607 ));
608 }
609 row.offers.push(ClanRepoOffer {
610 repo: repo.to_owned(),
611 to: to.to_owned(),
612 by: by.to_owned(),
613 at,
614 });
615 doc.clans.insert(clan.to_owned(), row.clone())?;
616 Ok(row)
617 })
618 }
619
620 pub fn clan_repo_offer(&self, clan: &str, repo: &str) -> Result<Option<ClanRepoOffer>> {
622 Ok(self.clan(clan)?.and_then(|row| row.offer_of(repo).cloned()))
623 }
624
625 pub fn clan_offers_to(&self, to: &str) -> Result<Vec<(String, ClanRepoOffer)>> {
628 let doc = self.doc.load()?;
629 if doc.clans.is_empty() {
630 return Ok(Vec::new());
631 }
632 let mut out = Vec::new();
633 for (name, row) in doc.clans.entries()? {
634 for offer in row.offers {
635 if offer.to == to {
636 out.push((name.clone(), offer));
637 }
638 }
639 }
640 Ok(out)
641 }
642
643 pub fn withdraw_clan_repo_offer(&self, clan: &str, by: &str, repo: &str) -> Result<Clan> {
645 self.doc.update(|doc| {
646 let mut row = Self::clan_for_admin(doc, clan, by, "withdraw an offer")?;
647 if row.offer_of(repo).is_none() {
648 return Err(refused(clan, ClanRefusal::Precondition, format!("{clan}/{repo} is offered to nobody")));
649 }
650 row.offers.retain(|o| o.repo != repo);
651 doc.clans.insert(clan.to_owned(), row.clone())?;
652 Ok(row)
653 })
654 }
655
656 pub fn close_clan_repo_offer(&self, clan: &str, repo: &str, to: &str) -> Result<ClanRepoOffer> {
661 self.doc.update(|doc| {
662 let mut row = doc
663 .clans
664 .get(clan)?
665 .ok_or_else(|| refused(clan, ClanRefusal::NotFound, format!("there is no clan {clan}")))?;
666 let Some(offer) = row.offer_of(repo).filter(|o| o.to == to).cloned() else {
667 return Err(refused(clan, ClanRefusal::NotFound, format!("{clan}/{repo} is not offered to {to}")));
668 };
669 row.offers.retain(|o| o.repo != repo);
670 doc.clans.insert(clan.to_owned(), row)?;
671 Ok(offer)
672 })
673 }
674
675 pub(super) fn forget_clan_member(doc: &mut AuthDoc, id: &str) -> Result<()> {
682 if doc.clan_index.is_empty() {
683 return Ok(());
684 }
685 for name in doc.clan_index.remove(id)?.unwrap_or_default() {
686 let Some(mut row) = doc.clans.get(&name)? else { continue };
687 row.members.retain(|m| m.principal != id);
688 row.invites.retain(|i| i.principal != id);
689 if row.admins() == 0 {
690 if let Some(first) = row.members.iter_mut().min_by_key(|m| m.joined_at) {
691 first.admin = true;
692 }
693 }
694 doc.clans.insert(name, row)?;
695 }
696 Ok(())
697 }
698
699 fn clan_for_admin(doc: &AuthDoc, clan: &str, by: &str, what: &str) -> Result<Clan> {
701 let row = Self::visible_clan(doc, clan, by)?;
702 Self::refuse_gated(doc, clan, by)?;
703 if row.role_of(by) != Some(ClanRole::Admin) {
704 return Err(refused(
705 clan,
706 ClanRefusal::Denied,
707 format!("only clan {clan}'s admin may {what}; {by} is not its admin"),
708 ));
709 }
710 Ok(row)
711 }
712
713 fn visible_clan(doc: &AuthDoc, clan: &str, who: &str) -> Result<Clan> {
717 match doc.clans.get(clan)? {
718 Some(row) if row.role_of(who).is_some() => Ok(row),
719 _ => Err(refused(clan, ClanRefusal::NotFound, format!("there is no clan {clan} you are in or invited to"))),
720 }
721 }
722
723 fn ordinary_account(doc: &AuthDoc, clan: &str, id: &str, what: &str) -> Result<()> {
727 let Some(principal) = doc.principals.get(id)? else {
728 return Err(refused(clan, ClanRefusal::NotFound, format!("there is no account {id}")));
729 };
730 if principal.kind != PrincipalKind::User {
731 return Err(refused(
732 clan,
733 ClanRefusal::Precondition,
734 format!("{id} is a {:?}, and only a person's own account may {what}", principal.kind),
735 ));
736 }
737 if let Some(employer) = doc.employers.get(id)? {
738 return Err(refused(
739 clan,
740 ClanRefusal::Precondition,
741 format!(
742 "{id} is an employee of {employer}: an enterprise account's people may not {what} — \
743 their account admin's rules win"
744 ),
745 ));
746 }
747 Self::refuse_gated(doc, clan, id)
748 }
749
750 fn refuse_gated(doc: &AuthDoc, clan: &str, id: &str) -> Result<()> {
752 if Self::gated(doc, id)? {
753 return Err(refused(clan, ClanRefusal::Precondition, format!("account {id} is locked")));
754 }
755 Ok(())
756 }
757
758 fn fits_in_room(doc: &AuthDoc, clan: &str, account: &str, bytes: u64, room: AccountRoom) -> Result<()> {
763 let Some(cap) = room.cap else { return Ok(()) };
764 let committed = room
765 .stored
766 .saturating_add(Self::promised_by(doc, account)?)
767 .saturating_add(Self::contributed_in(doc, account, Some(clan))?);
768 let free = cap.saturating_sub(committed);
769 if bytes > free {
770 return Err(refused(
771 clan,
772 ClanRefusal::Precondition,
773 format!(
774 "{account} has {} of its own room unused, and {} is asked for clan {clan}: {} short. \
775 A clan's room is its members' own; add room to the account, or give less",
776 human_bytes(free),
777 human_bytes(bytes),
778 human_bytes(bytes - free)
779 ),
780 ));
781 }
782 Ok(())
783 }
784
785 fn above_floor(
788 row: &Clan,
789 clan: &str,
790 member: &str,
791 bytes: u64,
792 used: Option<u64>,
793 lapsed: &[String],
794 what: &str,
795 ) -> Result<()> {
796 let Some(used) = used else {
797 return Err(refused(
798 clan,
799 ClanRefusal::Precondition,
800 format!(
801 "what clan {clan} stores could not be measured, so {member} may not {what} now: a room \
802 that is in use is never given back unseen"
803 ),
804 ));
805 };
806 let floor = row.floor_in_force(member, used, lapsed);
807 if bytes < floor {
808 let own = row.member(member).map_or(0, |m| m.contribution_bytes);
809 return Err(refused(
810 clan,
811 ClanRefusal::Precondition,
812 format!(
813 "clan {clan} stores {} in its room of {}, so {member}'s {} is in use: it may go down to {} \
814 and no lower, and {member} may not {what} until the clan stores less",
815 human_bytes(used),
816 human_bytes(row.room_in_force(lapsed)),
817 human_bytes(own),
818 human_bytes(floor)
819 ),
820 ));
821 }
822 Ok(())
823 }
824
825 fn index_clan(doc: &mut AuthDoc, principal: &str, clan: &str) -> Result<()> {
826 let mut names = doc.clan_index.get(principal)?.unwrap_or_default();
827 if !names.iter().any(|n| n == clan) {
828 names.push(clan.to_owned());
829 names.sort();
830 }
831 doc.clan_index.insert(principal.to_owned(), names)?;
832 Ok(())
833 }
834
835 fn unindex_clan(doc: &mut AuthDoc, principal: &str, clan: &str) -> Result<()> {
836 let Some(mut names) = doc.clan_index.get(principal)? else {
837 return Ok(());
838 };
839 names.retain(|n| n != clan);
840 if names.is_empty() {
841 doc.clan_index.remove(principal)?;
842 } else {
843 doc.clan_index.insert(principal.to_owned(), names)?;
844 }
845 Ok(())
846 }
847}