1use std::collections::BTreeMap;
2use std::sync::LazyLock;
3use std::{fmt, ops::Deref, str::FromStr};
4
5use crypto::{PublicKey, Signature};
6use radicle_cob::{Embed, ObjectId, TypeName};
7use radicle_git_ext as git_ext;
8use radicle_git_ext::Oid;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::identity::doc::Doc;
13use crate::node::device::Device;
14use crate::node::NodeId;
15use crate::storage;
16use crate::{
17 cob,
18 cob::{
19 op, store,
20 store::{Cob, CobAction, Transaction},
21 ActorId, Timestamp, Uri,
22 },
23 identity::{
24 doc::{DocError, RepoId},
25 Did,
26 },
27 storage::{ReadRepository, RepositoryError, WriteRepository},
28};
29
30use super::{Author, EntryId};
31
32pub static TYPENAME: LazyLock<TypeName> =
34 LazyLock::new(|| FromStr::from_str("xyz.radicle.id").expect("type name is valid"));
35
36pub type Op = cob::Op<Action>;
38
39pub type RevisionId = EntryId;
41
42pub type IdentityStream<'a> = cob::stream::Stream<'a, Action>;
43
44impl<'a> IdentityStream<'a> {
45 pub fn init(identity: ObjectId, store: &'a storage::git::Repository) -> Self {
46 let history = cob::stream::CobRange::new(&TYPENAME, &identity);
47 Self::new(&store.backend, history, TYPENAME.clone())
48 }
49}
50
51#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
53#[serde(tag = "type")]
54pub enum Action {
55 #[serde(rename = "revision")]
56 Revision {
57 title: cob::Title,
59 #[serde(default, skip_serializing_if = "String::is_empty")]
61 description: String,
62 blob: Oid,
65 parent: Option<RevisionId>,
67 signature: Signature,
69 },
70 RevisionEdit {
71 revision: RevisionId,
73 title: cob::Title,
75 #[serde(default, skip_serializing_if = "String::is_empty")]
77 description: String,
78 },
79 #[serde(rename = "revision.accept")]
80 RevisionAccept {
81 revision: RevisionId,
82 signature: Signature,
84 },
85 #[serde(rename = "revision.reject")]
86 RevisionReject { revision: RevisionId },
87 #[serde(rename = "revision.redact")]
88 RevisionRedact { revision: RevisionId },
89}
90
91impl CobAction for Action {
92 fn produces_identifier(&self) -> bool {
93 matches!(self, Self::Revision { .. })
94 }
95}
96
97#[derive(Error, Debug)]
99pub enum ApplyError {
100 #[error("causal dependency {0:?} missing")]
108 Missing(EntryId),
109 #[error("initialization failed: {0}")]
111 Init(&'static str),
112 #[error("invalid signature from {0} for blob {1}")]
114 InvalidSignature(PublicKey, Oid),
115 #[error("not authorized to perform this action")]
117 NotAuthorized,
118 #[error("parent id is missing from revision")]
119 MissingParent,
120 #[error("verdict for this revision has already been applied")]
121 DuplicateVerdict,
122 #[error("revision is in an unexpected state")]
123 UnexpectedState,
124 #[error("revision has been redacted")]
125 Redacted,
126 #[error("document does not contain any changes to current identity")]
127 DocUnchanged,
128 #[error("git: {0}")]
129 Git(#[from] git2::Error),
130 #[error("git: {0}")]
131 GitExt(#[from] git_ext::Error),
132 #[error("identity document error: {0}")]
133 Doc(#[from] DocError),
134}
135
136#[derive(Error, Debug)]
138pub enum Error {
139 #[error("apply failed: {0}")]
140 Apply(#[from] ApplyError),
141 #[error("store: {0}")]
142 Store(#[from] store::Error),
143 #[error("op decoding failed: {0}")]
144 Op(#[from] op::OpEncodingError),
145 #[error(transparent)]
146 Doc(#[from] DocError),
147 #[error("revision {0} was not found")]
148 NotFound(RevisionId),
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
153#[serde(rename_all = "camelCase")]
154pub struct Identity {
155 pub id: RepoId,
158 pub current: RevisionId,
161 pub root: RevisionId,
163 pub heads: BTreeMap<Did, RevisionId>,
166
167 revisions: BTreeMap<RevisionId, Option<Revision>>,
169 timeline: Vec<EntryId>,
171}
172
173impl cob::store::CobWithType for Identity {
174 fn type_name() -> &'static TypeName {
175 &TYPENAME
176 }
177}
178
179impl std::ops::Deref for Identity {
180 type Target = Revision;
181
182 fn deref(&self) -> &Self::Target {
183 self.current()
184 }
185}
186
187impl Identity {
188 pub fn new(revision: Revision) -> Self {
189 let root = revision.id;
190
191 Self {
192 id: revision.blob.into(),
193 root,
194 current: root,
195 heads: revision
196 .delegates()
197 .iter()
198 .copied()
199 .map(|did| (did, root))
200 .collect(),
201 revisions: BTreeMap::from_iter([(root, Some(revision))]),
202 timeline: vec![root],
203 }
204 }
205
206 pub fn initialize<'a, R, G>(
207 doc: &Doc,
208 store: &'a R,
209 signer: &Device<G>,
210 ) -> Result<IdentityMut<'a, R>, cob::store::Error>
211 where
212 G: crypto::signature::Signer<crypto::Signature>,
213 R: WriteRepository + cob::Store<Namespace = NodeId>,
214 {
215 let mut store = cob::store::Store::open(store)?;
216 let (id, identity) = Transaction::<Identity, _>::initial(
217 "Initialize identity",
218 &mut store,
219 signer,
220 |tx, repo| {
221 tx.revision(
222 #[allow(clippy::unwrap_used)]
224 cob::Title::new("Initial revision").unwrap(),
225 "",
226 doc,
227 None,
228 repo,
229 signer,
230 )
231 },
232 )?;
233
234 Ok(IdentityMut {
235 id,
236 identity,
237 store,
238 })
239 }
240
241 pub fn get<R: ReadRepository + cob::Store>(
242 object: &ObjectId,
243 repo: &R,
244 ) -> Result<Identity, store::Error> {
245 use cob::store::CobWithType;
246
247 cob::get::<Self, _>(repo, Self::type_name(), object)
248 .map(|r| r.map(|cob| cob.object))?
249 .ok_or_else(move || store::Error::NotFound(TYPENAME.clone(), *object))
250 }
251
252 pub fn get_mut<'a, R: WriteRepository + cob::Store<Namespace = NodeId>>(
254 id: &ObjectId,
255 repo: &'a R,
256 ) -> Result<IdentityMut<'a, R>, store::Error> {
257 let obj = Self::get(id, repo)?;
258 let store = cob::store::Store::open(repo)?;
259
260 Ok(IdentityMut {
261 id: *id,
262 identity: obj,
263 store,
264 })
265 }
266
267 pub fn load<R: ReadRepository + cob::Store>(repo: &R) -> Result<Identity, RepositoryError> {
268 let oid = repo.identity_root()?;
269 let oid = ObjectId::from(oid);
270
271 Self::get(&oid, repo).map_err(RepositoryError::from)
272 }
273
274 pub fn load_mut<R: WriteRepository + cob::Store<Namespace = NodeId>>(
275 repo: &R,
276 ) -> Result<IdentityMut<R>, RepositoryError> {
277 let oid = repo.identity_root()?;
278 let oid = ObjectId::from(oid);
279
280 Self::get_mut(&oid, repo).map_err(RepositoryError::from)
281 }
282}
283
284impl Identity {
285 pub fn id(&self) -> RepoId {
287 self.id
288 }
289
290 pub fn doc(&self) -> &Doc {
292 &self.current().doc
293 }
294
295 pub fn current(&self) -> &Revision {
297 self.revision(&self.current)
298 .expect("Identity::current: the current revision must always exist")
299 }
300
301 pub fn root(&self) -> &Revision {
303 self.revision(&self.root)
304 .expect("Identity::root: the root revision must always exist")
305 }
306
307 pub fn head(&self) -> Oid {
310 self.current
311 }
312
313 pub fn revision(&self, revision: &RevisionId) -> Option<&Revision> {
315 self.revisions.get(revision).and_then(|r| r.as_ref())
316 }
317
318 pub fn revisions(&self) -> impl DoubleEndedIterator<Item = &Revision> {
320 self.timeline
321 .iter()
322 .filter_map(|id| self.revisions.get(id).and_then(|o| o.as_ref()))
323 }
324
325 pub fn latest_by(&self, who: &Did) -> Option<&Revision> {
326 self.revisions().rev().find(|r| r.author.id() == who)
327 }
328}
329
330impl store::Cob for Identity {
331 type Action = Action;
332 type Error = ApplyError;
333
334 fn from_root<R: ReadRepository>(op: Op, repo: &R) -> Result<Self, Self::Error> {
335 let mut actions = op.actions.into_iter();
336 let Some(Action::Revision {
337 title,
338 description,
339 blob,
340 signature,
341 parent,
342 }) = actions.next()
343 else {
344 return Err(ApplyError::Init(
345 "the first action must be of type `revision`",
346 ));
347 };
348 if parent.is_some() {
349 return Err(ApplyError::Init(
350 "the initial revision must not have a parent",
351 ));
352 }
353 if actions.next().is_some() {
354 return Err(ApplyError::Init(
355 "the first operation must contain only one action",
356 ));
357 }
358 let root = Doc::load_at(op.id, repo)?;
359 if root.blob != blob {
360 return Err(ApplyError::Init("invalid object id specified in revision"));
361 }
362 if root.blob != *repo.id() {
363 return Err(ApplyError::Init(
364 "repository root does not match identifier",
365 ));
366 }
367 assert_eq!(root.commit, op.id);
368
369 let founder = root.delegates().first();
370 if founder.as_key() != &op.author {
371 return Err(ApplyError::Init("delegate does not match committer"));
372 }
373 if root
376 .verify_signature(founder, &signature, root.blob)
377 .is_err()
378 {
379 return Err(ApplyError::InvalidSignature(**founder, root.blob));
380 }
381 let revision = Revision::new(
382 root.commit,
383 title,
384 description,
385 op.author.into(),
386 root.blob,
387 root.doc,
388 State::Accepted,
389 signature,
390 parent,
391 op.timestamp,
392 );
393 Ok(Identity::new(revision))
394 }
395
396 fn op<'a, R: ReadRepository, I: IntoIterator<Item = &'a cob::Entry>>(
397 &mut self,
398 op: Op,
399 concurrent: I,
400 repo: &R,
401 ) -> Result<(), ApplyError> {
402 let id = op.id;
403 let concurrent = concurrent.into_iter().collect::<Vec<_>>();
404
405 for action in op.actions {
406 match self.action(action, id, op.author, op.timestamp, &concurrent, repo) {
407 Ok(()) => {}
408 Err(ApplyError::UnexpectedState) => {
412 if concurrent.is_empty() {
413 return Err(ApplyError::UnexpectedState);
414 }
415 }
416 Err(ApplyError::Redacted) => {}
419 Err(other) => return Err(other),
420 }
421 debug_assert!(!self.timeline.contains(&id));
422 self.timeline.push(id);
423 }
424 Ok(())
425 }
426}
427
428impl Identity {
429 fn action<R: ReadRepository>(
439 &mut self,
440 action: Action,
441 entry: EntryId,
442 author: ActorId,
443 timestamp: Timestamp,
444 _concurrent: &[&cob::Entry],
445 repo: &R,
446 ) -> Result<(), ApplyError> {
447 let current = self.current().clone();
448
449 if !current.is_delegate(&author.into()) {
450 return Err(ApplyError::UnexpectedState);
451 }
452 match action {
453 Action::RevisionAccept {
454 revision,
455 signature,
456 } => {
457 let id = revision;
458 let Some(revision) = lookup::revision_mut(&mut self.revisions, &id)? else {
459 return Err(ApplyError::Redacted);
460 };
461 if !revision.is_active() {
462 return Err(ApplyError::UnexpectedState);
464 }
465 assert_eq!(revision.parent, Some(current.id));
466
467 self.heads.insert(author.into(), id);
468 revision.accept(author, signature, ¤t)?;
469
470 self.adopt(id);
471 }
472 Action::RevisionReject { revision } => {
473 let Some(revision) = lookup::revision_mut(&mut self.revisions, &revision)? else {
474 return Err(ApplyError::Redacted);
475 };
476 if !revision.is_active() {
477 return Err(ApplyError::UnexpectedState);
479 }
480 assert_eq!(revision.parent, Some(current.id));
481
482 revision.reject(author)?;
483 }
484 Action::RevisionEdit {
485 title,
486 description,
487 revision,
488 } => {
489 if revision == self.current {
490 return Err(ApplyError::NotAuthorized);
491 }
492 let Some(revision) = lookup::revision_mut(&mut self.revisions, &revision)? else {
493 return Err(ApplyError::Redacted);
494 };
495 if !revision.is_active() {
496 return Err(ApplyError::UnexpectedState);
498 }
499 if revision.author.public_key() != &author {
500 return Err(ApplyError::NotAuthorized);
503 }
504 assert_eq!(revision.parent, Some(current.id));
505
506 revision.title = title;
507 revision.description = description;
508 }
509 Action::RevisionRedact { revision } => {
510 if revision == self.current {
511 return Err(ApplyError::UnexpectedState);
513 }
514 if let Some(revision) = self.revisions.get_mut(&revision) {
515 if let Some(r) = revision {
516 if r.is_accepted() {
517 return Err(ApplyError::UnexpectedState);
519 }
520 if r.author.public_key() != &author {
521 return Err(ApplyError::NotAuthorized);
524 }
525 *revision = None;
526 }
527 } else {
528 return Err(ApplyError::Missing(revision));
529 }
530 }
531 Action::Revision {
532 title,
533 description,
534 blob,
535 signature,
536 parent,
537 } => {
538 debug_assert!(!self.revisions.contains_key(&entry));
539
540 let doc = repo.blob(blob)?;
541 let doc = Doc::from_blob(&doc)?;
542 let Some(parent) = parent else {
544 return Err(ApplyError::MissingParent);
545 };
546 let Some(parent) = lookup::revision(&self.revisions, &parent)? else {
547 return Err(ApplyError::Redacted);
548 };
549 let state = if parent.id == current.id {
552 if doc == parent.doc {
555 return Err(ApplyError::DocUnchanged);
556 }
557 State::Active
558 } else {
559 State::Stale
560 };
561
562 if parent.verify_signature(&author, &signature, blob).is_err() {
564 return Err(ApplyError::InvalidSignature(author, blob));
565 }
566 let revision = Revision::new(
567 entry,
568 title,
569 description,
570 author.into(),
571 blob,
572 doc,
573 state,
574 signature,
575 Some(parent.id),
576 timestamp,
577 );
578 let id = revision.id;
579
580 self.heads.insert(author.into(), id);
581 self.revisions.insert(id, Some(revision));
582
583 if state == State::Active {
584 self.adopt(id);
585 }
586 }
587 }
588 Ok(())
589 }
590
591 fn adopt(&mut self, id: RevisionId) {
593 if self.current == id {
594 return;
595 }
596 let votes = self
597 .heads
598 .values()
599 .filter(|revision| **revision == id)
600 .count();
601 if self.is_majority(votes) {
602 self.current = id;
603 self.current_mut().state = State::Accepted;
604
605 for r in self
607 .revisions
608 .iter_mut()
609 .filter_map(|(_, r)| r.as_mut())
610 .filter(|r| r.state == State::Active)
611 {
612 r.state = State::Stale;
613 }
614 }
615 }
616
617 fn revision_mut(&mut self, revision: &RevisionId) -> Option<&mut Revision> {
619 self.revisions.get_mut(revision).and_then(|r| r.as_mut())
620 }
621
622 fn current_mut(&mut self) -> &mut Revision {
624 let current = self.current;
625 self.revision_mut(¤t)
626 .expect("Identity::current_mut: the current revision must always exist")
627 }
628}
629
630impl<R: ReadRepository> cob::Evaluate<R> for Identity {
631 type Error = Error;
632
633 fn init(entry: &cob::Entry, repo: &R) -> Result<Self, Self::Error> {
634 let op = Op::try_from(entry)?;
635 let object = Identity::from_root(op, repo)?;
636
637 Ok(object)
638 }
639
640 fn apply<'a, I: Iterator<Item = (&'a EntryId, &'a cob::Entry)>>(
641 &mut self,
642 entry: &cob::Entry,
643 concurrent: I,
644 repo: &R,
645 ) -> Result<(), Self::Error> {
646 let op = Op::try_from(entry)?;
647
648 self.op(op, concurrent.map(|(_, e)| e), repo)
649 .map_err(Error::Apply)
650 }
651}
652
653#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
654pub enum Verdict {
655 Accept(Signature),
658 Reject,
660}
661
662#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
664#[serde(rename_all = "camelCase")]
665pub enum State {
666 Active,
669 Accepted,
672 Rejected,
675 Stale,
678}
679
680impl std::fmt::Display for State {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
682 match self {
683 Self::Active => write!(f, "active"),
684 Self::Accepted => write!(f, "accepted"),
685 Self::Rejected => write!(f, "rejected"),
686 Self::Stale => write!(f, "stale"),
687 }
688 }
689}
690
691#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
698pub struct Revision {
699 pub id: RevisionId,
701 pub blob: Oid,
703 pub title: cob::Title,
705 pub state: State,
707 pub description: String,
709 pub author: Author,
711 pub doc: Doc,
713 pub timestamp: Timestamp,
715 pub parent: Option<RevisionId>,
717
718 verdicts: BTreeMap<PublicKey, Verdict>,
720}
721
722impl std::ops::Deref for Revision {
723 type Target = Doc;
724
725 fn deref(&self) -> &Self::Target {
726 &self.doc
727 }
728}
729
730impl Revision {
731 pub fn signatures(&self) -> impl Iterator<Item = (&PublicKey, Signature)> {
732 self.verdicts().filter_map(|(key, verdict)| match verdict {
733 Verdict::Accept(sig) => Some((key, *sig)),
734 Verdict::Reject => None,
735 })
736 }
737
738 pub fn is_accepted(&self) -> bool {
739 matches!(self.state, State::Accepted)
740 }
741
742 pub fn is_active(&self) -> bool {
743 matches!(self.state, State::Active)
744 }
745
746 pub fn verdicts(&self) -> impl Iterator<Item = (&PublicKey, &Verdict)> {
747 self.verdicts.iter()
748 }
749
750 pub fn accepted(&self) -> impl Iterator<Item = Did> + '_ {
751 self.signatures().map(|(key, _)| key.into())
752 }
753
754 pub fn rejected(&self) -> impl Iterator<Item = Did> + '_ {
755 self.verdicts().filter_map(|(key, v)| match v {
756 Verdict::Accept(_) => None,
757 Verdict::Reject => Some(key.into()),
758 })
759 }
760
761 pub fn sign<G: crypto::signature::Signer<crypto::Signature>>(
762 &self,
763 signer: &G,
764 ) -> Result<Signature, DocError> {
765 self.doc.signature_of(signer)
766 }
767}
768
769impl Revision {
771 fn new(
772 id: RevisionId,
773 title: cob::Title,
774 description: String,
775 author: Author,
776 blob: Oid,
777 doc: Doc,
778 state: State,
779 signature: Signature,
780 parent: Option<RevisionId>,
781 timestamp: Timestamp,
782 ) -> Self {
783 let verdicts = BTreeMap::from_iter([(*author.public_key(), Verdict::Accept(signature))]);
784
785 Self {
786 id,
787 title,
788 description,
789 author,
790 blob,
791 doc,
792 state,
793 verdicts,
794 parent,
795 timestamp,
796 }
797 }
798
799 fn accept(
800 &mut self,
801 author: PublicKey,
802 signature: Signature,
803 current: &Revision,
804 ) -> Result<(), ApplyError> {
805 if current
807 .verify_signature(&author, &signature, self.blob)
808 .is_err()
809 {
810 return Err(ApplyError::InvalidSignature(author, self.blob));
811 }
812 if self
813 .verdicts
814 .insert(author, Verdict::Accept(signature))
815 .is_some()
816 {
817 return Err(ApplyError::DuplicateVerdict);
818 }
819 Ok(())
820 }
821
822 fn reject(&mut self, key: PublicKey) -> Result<(), ApplyError> {
823 if self.verdicts.insert(key, Verdict::Reject).is_some() {
824 return Err(ApplyError::DuplicateVerdict);
825 }
826 if self.is_active() && self.rejected().count() > self.delegates().len() - self.majority() {
830 self.state = State::Rejected;
831 }
832 Ok(())
833 }
834}
835
836impl<R: ReadRepository> store::Transaction<Identity, R> {
837 pub fn accept(
838 &mut self,
839 revision: RevisionId,
840 signature: Signature,
841 ) -> Result<(), store::Error> {
842 self.push(Action::RevisionAccept {
843 revision,
844 signature,
845 })
846 }
847
848 pub fn reject(&mut self, revision: RevisionId) -> Result<(), store::Error> {
849 self.push(Action::RevisionReject { revision })
850 }
851
852 pub fn edit(
853 &mut self,
854 revision: RevisionId,
855 title: cob::Title,
856 description: impl ToString,
857 ) -> Result<(), store::Error> {
858 self.push(Action::RevisionEdit {
859 revision,
860 title,
861 description: description.to_string(),
862 })
863 }
864
865 pub fn redact(&mut self, revision: RevisionId) -> Result<(), store::Error> {
866 self.push(Action::RevisionRedact { revision })
867 }
868}
869
870impl<R: WriteRepository> store::Transaction<Identity, R> {
871 pub fn revision<G: crypto::signature::Signer<crypto::Signature>>(
872 &mut self,
873 title: cob::Title,
874 description: impl ToString,
875 doc: &Doc,
876 parent: Option<RevisionId>,
877 repo: &R,
878 signer: &Device<G>,
879 ) -> Result<(), store::Error> {
880 let (blob, bytes, signature) = doc.sign(signer).map_err(store::Error::Identity)?;
881 let embed =
883 Embed::<Uri>::store("radicle.json", &bytes, repo.raw()).map_err(store::Error::Git)?;
884 debug_assert_eq!(embed.content, Uri::from(blob)); self.embed([embed])?;
888
889 self.push(Action::Revision {
891 title,
892 description: description.to_string(),
893 blob,
894 parent,
895 signature,
896 })
897 }
898}
899
900pub struct IdentityMut<'a, R> {
901 pub id: ObjectId,
902
903 identity: Identity,
904 store: store::Store<'a, Identity, R>,
905}
906
907impl<R> fmt::Debug for IdentityMut<'_, R> {
908 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
909 f.debug_struct("IdentityMut")
910 .field("id", &self.id)
911 .field("identity", &self.identity)
912 .finish()
913 }
914}
915
916impl<R> IdentityMut<'_, R>
917where
918 R: WriteRepository + cob::Store<Namespace = NodeId>,
919{
920 pub fn reload(&mut self) -> Result<(), store::Error> {
922 self.identity = self
923 .store
924 .get(&self.id)?
925 .ok_or_else(|| store::Error::NotFound(TYPENAME.clone(), self.id))?;
926
927 Ok(())
928 }
929
930 pub fn transaction<G, F>(
931 &mut self,
932 message: &str,
933 signer: &Device<G>,
934 operations: F,
935 ) -> Result<EntryId, Error>
936 where
937 G: crypto::signature::Signer<crypto::Signature>,
938 F: FnOnce(&mut Transaction<Identity, R>, &R) -> Result<(), store::Error>,
939 {
940 let mut tx = Transaction::default();
941 operations(&mut tx, self.store.as_ref())?;
942
943 let (doc, commit) = tx.commit(message, self.id, &mut self.store, signer)?;
944 self.identity = doc;
945
946 Ok(commit)
947 }
948
949 pub fn update<G>(
952 &mut self,
953 title: cob::Title,
954 description: impl ToString,
955 doc: &Doc,
956 signer: &Device<G>,
957 ) -> Result<RevisionId, Error>
958 where
959 G: crypto::signature::Signer<crypto::Signature>,
960 {
961 let parent = self.current;
962 let id = self.transaction("Propose revision", signer, |tx, repo| {
963 tx.revision(title, description, doc, Some(parent), repo, signer)
964 })?;
965
966 Ok(id)
967 }
968
969 pub fn accept<G>(&mut self, revision: &RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
971 where
972 G: crypto::signature::Signer<crypto::Signature>,
973 {
974 let id = *revision;
975 let revision = self.revision(revision).ok_or(Error::NotFound(id))?;
976 let signature = revision.sign(signer)?;
977
978 self.transaction("Accept revision", signer, |tx, _| tx.accept(id, signature))
979 }
980
981 pub fn reject<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
983 where
984 G: crypto::signature::Signer<crypto::Signature>,
985 {
986 self.transaction("Reject revision", signer, |tx, _| tx.reject(revision))
987 }
988
989 pub fn redact<G>(&mut self, revision: RevisionId, signer: &Device<G>) -> Result<EntryId, Error>
991 where
992 G: crypto::signature::Signer<crypto::Signature>,
993 {
994 self.transaction("Redact revision", signer, |tx, _| tx.redact(revision))
995 }
996
997 pub fn edit<G>(
999 &mut self,
1000 revision: RevisionId,
1001 title: cob::Title,
1002 description: String,
1003 signer: &Device<G>,
1004 ) -> Result<EntryId, Error>
1005 where
1006 G: crypto::signature::Signer<crypto::Signature>,
1007 {
1008 self.transaction("Edit revision", signer, |tx, _| {
1009 tx.edit(revision, title, description)
1010 })
1011 }
1012}
1013
1014impl<R> Deref for IdentityMut<'_, R> {
1015 type Target = Identity;
1016
1017 fn deref(&self) -> &Self::Target {
1018 &self.identity
1019 }
1020}
1021
1022mod lookup {
1023 use super::*;
1024
1025 pub fn revision_mut<'a>(
1026 revisions: &'a mut BTreeMap<RevisionId, Option<Revision>>,
1027 revision: &RevisionId,
1028 ) -> Result<Option<&'a mut Revision>, ApplyError> {
1029 match revisions.get_mut(revision) {
1030 Some(Some(revision)) => Ok(Some(revision)),
1031 Some(None) => Ok(None),
1033 None => Err(ApplyError::Missing(*revision)),
1035 }
1036 }
1037
1038 pub fn revision<'a>(
1039 revisions: &'a BTreeMap<RevisionId, Option<Revision>>,
1040 revision: &RevisionId,
1041 ) -> Result<Option<&'a Revision>, ApplyError> {
1042 match revisions.get(revision) {
1043 Some(Some(revision)) => Ok(Some(revision)),
1044 Some(None) => Ok(None),
1046 None => Err(ApplyError::Missing(*revision)),
1048 }
1049 }
1050}
1051
1052#[cfg(test)]
1053#[allow(clippy::unwrap_used)]
1054mod test {
1055 use qcheck_macros::quickcheck;
1056
1057 use crate::cob::{self, Title};
1058 use crate::crypto::PublicKey;
1059 use crate::identity::did::Did;
1060 use crate::identity::doc::PayloadId;
1061 use crate::identity::Visibility;
1062 use crate::rad;
1063 use crate::storage::git::Storage;
1064 use crate::storage::ReadStorage;
1065 use crate::test::fixtures;
1066 use crate::test::setup::{Network, NodeWithRepo};
1067
1068 use super::*;
1069
1070 #[quickcheck]
1071 fn prop_json_eq_str(pk: PublicKey, proj: RepoId, did: Did) {
1072 let json = serde_json::to_string(&pk).unwrap();
1073 assert_eq!(format!("\"{pk}\""), json);
1074
1075 let json = serde_json::to_string(&proj).unwrap();
1076 assert_eq!(format!("\"{}\"", proj.urn()), json);
1077
1078 let json = serde_json::to_string(&did).unwrap();
1079 assert_eq!(format!("\"{did}\""), json);
1080 }
1081
1082 #[test]
1083 fn test_identity_updates() {
1084 let NodeWithRepo { node, repo } = NodeWithRepo::default();
1085 let bob = Device::mock();
1086 let signer = &node.signer;
1087 let mut identity = Identity::load_mut(&*repo).unwrap();
1088 let mut doc = identity.doc().clone().edit();
1089 let title = Title::new("Identity update").unwrap();
1090 let description = "";
1091 let r0 = identity.current;
1092
1093 assert!(identity.current().is_accepted());
1095 identity
1097 .update(
1098 title.clone(),
1099 description,
1100 &doc.clone().verified().unwrap(),
1101 signer,
1102 )
1103 .unwrap_err();
1104 assert_eq!(identity.current, r0);
1105
1106 doc.threshold = 2;
1109 assert!(doc.clone().verified().is_err());
1110
1111 doc.delegate(bob.public_key().into());
1113 let r1 = identity
1115 .update(
1116 title.clone(),
1117 description,
1118 &doc.clone().verified().unwrap(),
1119 signer,
1120 )
1121 .unwrap();
1122 assert!(identity.revision(&r1).unwrap().is_accepted());
1123 assert_eq!(identity.current, r1);
1124 doc.visibility = Visibility::private([]);
1128 let r2 = identity
1129 .update(
1130 title.clone(),
1131 description,
1132 &doc.clone().verified().unwrap(),
1133 signer,
1134 )
1135 .unwrap();
1136 assert_eq!(identity.current, r1);
1138 assert_eq!(identity.revision(&r2).unwrap().state, State::Active);
1139 assert_eq!(repo.canonical_identity_head().unwrap(), r1);
1140 assert_eq!(
1141 repo.identity_doc().unwrap().visibility(),
1142 &Visibility::Public
1143 );
1144 identity.accept(&r2, &bob).unwrap();
1146
1147 assert_eq!(identity.current, r2);
1149 assert_eq!(identity.revision(&r2).unwrap().state, State::Accepted);
1150 assert_eq!(repo.canonical_identity_head().unwrap(), r2);
1151 assert_eq!(
1152 repo.canonical_identity_doc().unwrap().visibility(),
1153 &Visibility::private([])
1154 );
1155 }
1156
1157 #[test]
1158 fn test_identity_update_rejected() {
1159 let NodeWithRepo { node, repo } = NodeWithRepo::default();
1160 let bob = Device::mock();
1161 let eve = Device::mock();
1162 let signer = &node.signer;
1163 let mut identity = Identity::load_mut(&*repo).unwrap();
1164 let mut doc = identity.doc().clone().edit();
1165 let description = "";
1166
1167 doc.delegate(bob.public_key().into());
1169 let r1 = identity
1170 .update(
1171 cob::Title::new("Identity update").unwrap(),
1172 description,
1173 &doc.clone().verified().unwrap(),
1174 signer,
1175 )
1176 .unwrap();
1177 assert_eq!(identity.current, r1);
1178
1179 doc.visibility = Visibility::private([]);
1180 let r2 = identity
1181 .update(
1182 cob::Title::new("Make private").unwrap(),
1183 description,
1184 &doc.clone().verified().unwrap(),
1185 &node.signer,
1186 )
1187 .unwrap();
1188
1189 identity.reject(r2, &bob).unwrap();
1191 let r2 = identity.revision(&r2).unwrap();
1192 assert_eq!(r2.state, State::Rejected);
1193
1194 doc.delegate(eve.public_key().into());
1196 let r3 = identity
1197 .update(
1198 cob::Title::new("Add Eve").unwrap(),
1199 description,
1200 &doc.clone().verified().unwrap(),
1201 &node.signer,
1202 )
1203 .unwrap();
1204 let _ = identity.accept(&r3, &bob).unwrap();
1205 assert_eq!(identity.current, r3);
1206
1207 doc.visibility = Visibility::Public;
1208 let r3 = identity
1209 .update(
1210 cob::Title::new("Make public").unwrap(),
1211 description,
1212 &doc.verified().unwrap(),
1213 &node.signer,
1214 )
1215 .unwrap();
1216
1217 identity.reject(r3, &bob).unwrap();
1219 let r3 = identity.revision(&r3).unwrap().clone();
1220 assert_eq!(r3.state, State::Active); identity.reject(r3.id, &eve).unwrap();
1224 let r3 = identity.revision(&r3.id).unwrap();
1225 assert_eq!(r3.state, State::Rejected);
1226 }
1227
1228 #[test]
1229 fn test_identity_updates_concurrent() {
1230 let network = Network::default();
1231 let alice = &network.alice;
1232 let bob = &network.bob;
1233
1234 let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1235 let mut alice_doc = alice_identity.doc().clone().edit();
1236
1237 alice_doc.delegate(bob.signer.public_key().into());
1238 let a1 = alice_identity
1239 .update(
1240 cob::Title::new("Add Bob").unwrap(),
1241 "",
1242 &alice_doc.clone().verified().unwrap(),
1243 &alice.signer,
1244 )
1245 .unwrap();
1246
1247 bob.repo.fetch(alice);
1248
1249 let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1250 let bob_doc = bob_identity.doc().clone();
1251 assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
1252
1253 alice_doc.visibility = Visibility::private([]);
1255 let a2 = alice_identity
1256 .update(
1257 cob::Title::new("Change visibility").unwrap(),
1258 "",
1259 &alice_doc.clone().clone().verified().unwrap(),
1260 &alice.signer,
1261 )
1262 .unwrap();
1263 let b1 = bob_identity
1265 .update(
1266 cob::Title::new("Make private").unwrap(),
1267 "",
1268 &alice_doc.verified().unwrap(),
1269 &bob.signer,
1270 )
1271 .unwrap();
1272
1273 bob.repo.fetch(alice);
1275 bob_identity.reload().unwrap();
1276 assert_eq!(bob_identity.current, a1);
1277
1278 alice.repo.fetch(bob);
1281 alice_identity.reload().unwrap();
1282 assert_eq!(alice_identity.current, a1);
1283 assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Active);
1284 assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Active);
1285
1286 bob_identity.accept(&a2, &bob.signer).unwrap();
1288 assert_eq!(bob_identity.current, a2);
1289 assert_eq!(bob_identity.revision(&a1).unwrap().state, State::Accepted);
1290 assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
1291 assert_eq!(bob_identity.revision(&b1).unwrap().state, State::Stale);
1292 }
1293
1294 #[test]
1295 fn test_identity_redact_revision() {
1296 let network = Network::default();
1297 let alice = &network.alice;
1298 let bob = &network.bob;
1299 let eve = &network.eve;
1300
1301 let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1302 let mut alice_doc = alice_identity.doc().clone().edit();
1303
1304 alice_doc.delegate(bob.signer.public_key().into());
1305 let a0 = alice_identity.root;
1306 let a1 = alice_identity
1307 .update(
1308 cob::Title::new("Add Bob").unwrap(),
1309 "Eh.",
1310 &alice_doc.clone().clone().verified().unwrap(),
1311 &alice.signer,
1312 )
1313 .unwrap();
1314
1315 alice_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
1316 let a2 = alice_identity
1317 .update(
1318 cob::Title::new("Change visibility").unwrap(),
1319 "Eh.",
1320 &alice_doc.verified().unwrap(),
1321 &alice.signer,
1322 )
1323 .unwrap();
1324
1325 bob.repo.fetch(alice);
1326 let a3 = alice_identity.redact(a2, &alice.signer).unwrap();
1327 assert!(alice_identity.revision(&a1).is_some());
1328 assert_eq!(alice_identity.timeline, vec![a0, a1, a2, a3]);
1329
1330 let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1331 let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();
1332
1333 assert_eq!(bob_identity.timeline, vec![a0, a1, a2, b1]);
1334 assert_eq!(bob_identity.revision(&a2).unwrap().state, State::Accepted);
1335 bob.repo.fetch(alice);
1336 bob_identity.reload().unwrap();
1337
1338 assert_eq!(bob_identity.timeline, vec![a0, a1, a2, a3, b1]);
1339 assert_eq!(bob_identity.revision(&a2), None);
1340 assert_eq!(bob_identity.current, a1);
1341 }
1342
1343 #[test]
1344 fn test_identity_remove_delegate_concurrent() {
1345 let network = Network::default();
1346 let alice = &network.alice;
1347 let bob = &network.bob;
1348 let eve = &network.eve;
1349
1350 let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1351 let mut alice_doc = alice_identity.doc().clone().edit();
1352
1353 alice_doc.delegate(bob.signer.public_key().into());
1354 alice_doc.delegate(eve.signer.public_key().into());
1355 let a0 = alice_identity.root;
1356 let a1 = alice_identity .update(
1358 cob::Title::new("Add Bob and Eve").unwrap(),
1359 "Eh#!",
1360 &alice_doc.clone().verified().unwrap(),
1361 &alice.signer,
1362 )
1363 .unwrap();
1364
1365 alice_doc.rescind(&eve.signer.public_key().into()).unwrap();
1366 let a2 = alice_identity
1367 .update(
1368 cob::Title::new("Remove Eve").unwrap(),
1369 "",
1370 &alice_doc.verified().unwrap(),
1371 &alice.signer,
1372 )
1373 .unwrap();
1374
1375 bob.repo.fetch(eve);
1376 bob.repo.fetch(alice);
1377 eve.repo.fetch(bob);
1378
1379 let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1380 let b1 = cob::git::stable::with_advanced_timestamp(|| {
1381 bob_identity.accept(&a2, &bob.signer).unwrap()
1382 });
1383 assert_eq!(bob_identity.current, a2);
1384
1385 let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
1386 let mut eve_doc = eve_identity.doc().clone().edit();
1387 eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
1388 let e1 = cob::git::stable::with_advanced_timestamp(|| {
1389 eve_identity
1390 .update(
1391 cob::Title::new("Change visibility").unwrap(),
1392 "",
1393 &eve_doc.verified().unwrap(),
1394 &eve.signer,
1395 )
1396 .unwrap()
1397 });
1398 assert!(eve_identity.revision(&e1).unwrap().is_active());
1400
1401 eve.repo.fetch(bob);
1411 eve_identity.reload().unwrap();
1412 assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1]);
1415 assert_eq!(eve_identity.revision(&e1), None);
1416 assert!(!eve_identity.is_delegate(&eve.signer.public_key().into()));
1417 }
1418
1419 #[test]
1420 fn test_identity_reject_concurrent() {
1421 let network = Network::default();
1422 let alice = &network.alice;
1423 let bob = &network.bob;
1424 let eve = &network.eve;
1425
1426 let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1427 let mut alice_doc = alice_identity.doc().clone().edit();
1428
1429 alice_doc.delegate(bob.signer.public_key().into());
1430 alice_doc.delegate(eve.signer.public_key().into());
1431 let a0 = alice_identity.root;
1432 let a1 = alice_identity
1433 .update(
1434 cob::Title::new("Add Bob and Eve").unwrap(),
1435 "Eh!#",
1436 &alice_doc.clone().verified().unwrap(),
1437 &alice.signer,
1438 )
1439 .unwrap();
1440
1441 alice_doc.visibility = Visibility::private([]);
1442 let a2 = alice_identity
1443 .update(
1444 cob::Title::new("Change visibility").unwrap(),
1445 "",
1446 &alice_doc.verified().unwrap(),
1447 &alice.signer,
1448 )
1449 .unwrap();
1450
1451 bob.repo.fetch(eve);
1452 bob.repo.fetch(alice);
1453 eve.repo.fetch(bob);
1454
1455 let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1457 let b1 = bob_identity.accept(&a2, &bob.signer).unwrap();
1458
1459 let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
1461 let e1 = eve_identity.reject(a2, &eve.signer).unwrap();
1462 assert!(eve_identity.revision(&a2).unwrap().is_active());
1463
1464 let mut eve_doc = eve_identity.doc().clone().edit();
1466 eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
1467 let e2 = eve_identity
1468 .update(
1469 cob::Title::new("Change visibility").unwrap(),
1470 "",
1471 &eve_doc.verified().unwrap(),
1472 &eve.signer,
1473 )
1474 .unwrap();
1475 assert!(eve_identity.revision(&e2).unwrap().is_active());
1476
1477 eve.repo.fetch(bob);
1493 eve_identity.reload().unwrap();
1494 assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1, e2]);
1495
1496 let e2 = eve_identity.revision(&e2).unwrap();
1499 assert_eq!(e2.state, State::Stale);
1500 assert!(eve_identity.revision(&a2).unwrap().is_accepted());
1501 }
1502
1503 #[test]
1504 fn test_identity_updates_concurrent_outdated() {
1505 let network = Network::default();
1506 let alice = &network.alice;
1507 let bob = &network.bob;
1508 let eve = &network.eve;
1509
1510 let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
1511 let mut alice_doc = alice_identity.doc().clone().edit();
1512
1513 alice.repo.fetch(bob);
1514 alice.repo.fetch(eve);
1515 alice_doc.delegate(bob.signer.public_key().into());
1516 alice_doc.delegate(eve.signer.public_key().into());
1517 let a0 = alice_identity.root;
1518 let a1 = alice_identity
1519 .update(
1520 cob::Title::new("Add Bob and Eve").unwrap(),
1521 "",
1522 &alice_doc.verified().unwrap(),
1523 &alice.signer,
1524 )
1525 .unwrap();
1526
1527 bob.repo.fetch(alice);
1528 eve.repo.fetch(alice);
1529
1530 let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
1531 let mut bob_doc = bob_identity.doc().clone().edit();
1532 assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
1533
1534 bob_doc.visibility = Visibility::private([]);
1544 let b1 = bob_identity
1545 .update(
1546 cob::Title::new("Change visibility #1").unwrap(),
1547 "",
1548 &bob_doc.verified().unwrap(),
1549 &bob.signer,
1550 )
1551 .unwrap();
1552 alice.repo.fetch(bob);
1553 eve.repo.fetch(bob);
1554
1555 let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
1557 let mut eve_doc = eve_identity.doc().clone().edit();
1558 eve_doc.visibility = Visibility::private([]);
1559 let e1 = eve_identity
1560 .update(
1561 cob::Title::new("Change visibility #2").unwrap(),
1562 "Woops",
1563 &eve_doc.verified().unwrap(),
1564 &eve.signer,
1565 )
1566 .unwrap();
1567 assert_eq!(eve_identity.revisions().count(), 4);
1568 assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);
1569
1570 alice_identity.reload().unwrap();
1571 let a2 = cob::git::stable::with_advanced_timestamp(|| {
1572 alice_identity.accept(&b1, &alice.signer).unwrap()
1573 });
1574
1575 eve.repo.fetch(alice);
1576 eve_identity.reload().unwrap();
1577
1578 assert_eq!(eve_identity.timeline, vec![a0, a1, b1, e1, a2]);
1579 assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Stale);
1580 }
1581
1582 #[test]
1583 fn test_valid_identity() {
1584 let tempdir = tempfile::tempdir().unwrap();
1585 let mut rng = fastrand::Rng::new();
1586
1587 let alice = Device::mock_rng(&mut rng);
1588 let bob = Device::mock_rng(&mut rng);
1589 let eve = Device::mock_rng(&mut rng);
1590
1591 let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1592 let (id, _, _, _) =
1593 fixtures::project(tempdir.path().join("copy"), &storage, &alice).unwrap();
1594
1595 rad::fork_remote(id, alice.public_key(), &bob, &storage).unwrap();
1597 rad::fork_remote(id, alice.public_key(), &eve, &storage).unwrap();
1598
1599 let repo = storage.repository(id).unwrap();
1600 let mut identity = Identity::load_mut(&repo).unwrap();
1601 let doc = identity.doc().clone();
1602 let prj = doc.project().unwrap();
1603 let mut doc = doc.edit();
1604
1605 let desc = prj.description().to_owned() + "!";
1607 let prj = prj.update(None, desc, None).unwrap();
1608 doc.payload.insert(PayloadId::project(), prj.clone().into());
1609 identity
1610 .update(
1611 cob::Title::new("Update description").unwrap(),
1612 "",
1613 &doc.clone().verified().unwrap(),
1614 &alice,
1615 )
1616 .unwrap();
1617
1618 doc.delegate(bob.public_key().into());
1620 doc.threshold = 2;
1621 identity
1622 .update(
1623 cob::Title::new("Add bob").unwrap(),
1624 "",
1625 &doc.clone().verified().unwrap(),
1626 &alice,
1627 )
1628 .unwrap();
1629
1630 doc.delegate(eve.public_key().into());
1632
1633 let revision = identity
1635 .update(
1636 cob::Title::new("Add eve").unwrap(),
1637 "",
1638 &doc.clone().verified().unwrap(),
1639 &alice,
1640 )
1641 .unwrap();
1642 identity.accept(&revision, &bob).unwrap();
1643
1644 let desc = prj.description().to_owned() + "?";
1646 let prj = prj.update(None, desc, None).unwrap();
1647 doc.payload.insert(PayloadId::project(), prj.into());
1648
1649 let revision = identity
1650 .update(
1651 cob::Title::new("Update description again").unwrap(),
1652 "Bob's repository",
1653 &doc.verified().unwrap(),
1654 &bob,
1655 )
1656 .unwrap();
1657 identity.accept(&revision, &eve).unwrap();
1658
1659 let identity: Identity = Identity::load(&repo).unwrap();
1660 let root = repo.identity_root().unwrap();
1661 let doc = repo.identity_doc_at(revision).unwrap();
1662
1663 assert_eq!(identity.signatures().count(), 2);
1664 assert_eq!(identity.revisions().count(), 5);
1665 assert_eq!(identity.id(), id);
1666 assert_eq!(identity.root().id, root);
1667 assert_eq!(identity.current().blob, doc.blob);
1668 assert_eq!(identity.current().description.as_str(), "Bob's repository");
1669 assert_eq!(identity.head(), revision);
1670 assert_eq!(identity.doc(), &*doc);
1671 assert_eq!(
1672 identity.doc().project().unwrap().description(),
1673 "Acme's repository!?"
1674 );
1675
1676 assert_eq!(doc.project().unwrap().description(), "Acme's repository!?");
1677 }
1678}