radicle/identity/
doc.rs

1pub mod update;
2
3mod id;
4
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt;
7use std::num::{NonZeroU32, NonZeroUsize};
8use std::ops::{Deref, Not};
9use std::path::Path;
10use std::str::FromStr;
11use std::sync::LazyLock;
12
13use nonempty::NonEmpty;
14use radicle_cob::type_name::{TypeName, TypeNameParse};
15use radicle_git_ext::Oid;
16use serde::{de, Deserialize, Serialize};
17use thiserror::Error;
18
19use crate::canonical::formatter::CanonicalFormatter;
20use crate::cob::identity;
21use crate::crypto;
22use crate::crypto::Signature;
23use crate::git;
24use crate::git::canonical::rules;
25use crate::identity::{project::Project, Did};
26use crate::node::device::Device;
27use crate::storage;
28use crate::storage::{ReadRepository, RepositoryError};
29
30pub use crypto::PublicKey;
31pub use id::*;
32
33use super::crefs::{self, RawCanonicalRefs};
34use super::CanonicalRefs;
35
36/// Path to the identity document in the identity branch.
37pub static PATH: LazyLock<&Path> = LazyLock::new(|| Path::new("radicle.json"));
38/// Maximum length of a string in the identity document.
39pub const MAX_STRING_LENGTH: usize = 255;
40/// Maximum number of a delegates in the identity document.
41pub const MAX_DELEGATES: usize = 255;
42/// The current, most recent version of the identity document.
43// SAFETY: identity version should never be 0, so we can use `unsafe` here
44pub const IDENTITY_VERSION: Version = Version(unsafe { NonZeroU32::new_unchecked(1) });
45
46#[derive(Error, Debug)]
47pub enum DocError {
48    #[error("json: {0}")]
49    Json(#[from] serde_json::Error),
50    #[error(transparent)]
51    Delegates(#[from] DelegatesError),
52    #[error(transparent)]
53    Threshold(#[from] ThresholdError),
54    #[error("git: {0}")]
55    GitExt(#[from] git::Error),
56    #[error("git: {0}")]
57    Git(#[from] git2::Error),
58    #[error("missing identity document")]
59    Missing,
60}
61
62#[derive(Debug, Error)]
63#[error("invalid delegates: {0}")]
64pub struct DelegatesError(&'static str);
65
66#[derive(Debug, Error)]
67#[error("invalid threshold `{0}`: {1}")]
68pub struct ThresholdError(usize, &'static str);
69
70impl DocError {
71    /// Whether this error is caused by the document not being found.
72    pub fn is_not_found(&self) -> bool {
73        match self {
74            Self::GitExt(git::Error::NotFound(_)) => true,
75            Self::GitExt(git::Error::Git(e)) if git::is_not_found_err(e) => true,
76            Self::Git(err) if git::is_not_found_err(err) => true,
77            _ => false,
78        }
79    }
80}
81
82#[derive(Debug, Error)]
83pub enum DefaultBranchRuleError {
84    #[error("could not create rule due to the reference name being invalid: {0}")]
85    Pattern(#[from] rules::PatternError),
86    #[error("could not load `xyz.radicle.project` to get default branch name: {0}")]
87    Payload(#[from] PayloadError),
88}
89
90/// The version number of the identity document.
91///
92/// It is used to ensure compatibility when parsing identity documents.
93///
94/// If an invalid version is found – either the `0` version, or an unrecognized
95/// future version – the parsing of a version will fail.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
97pub struct Version(NonZeroU32);
98
99impl Version {
100    /// Construct a [`Version`].
101    ///
102    /// # Errors
103    ///
104    ///   - `n` is 0
105    ///   - `n` is greater than the latest version, specified by
106    ///     [`IDENTITY_VERSION`].
107    pub fn new(n: u32) -> Result<Version, VersionError> {
108        match NonZeroU32::new(n) {
109            None => Err(VersionError::ZeroVersion),
110            Some(n) if n > IDENTITY_VERSION.into() => Err(VersionError::UnkownVersion(n)),
111            Some(n) => Ok(Version(n)),
112        }
113    }
114
115    /// Return the underlying [`NonZeroU32`] number of the `Version`.
116    pub fn number(&self) -> NonZeroU32 {
117        self.0
118    }
119
120    /// Check if the provided version is part of the set of accepted versions.
121    pub fn is_valid_version(v: &u32) -> bool {
122        0 < *v && *v <= IDENTITY_VERSION.into()
123    }
124
125    /// Helper for skipping the serialization of the version if `version <= 1`.
126    ///
127    /// Note that we shouldn't allow `version: 0`, but there is no harm in
128    /// skipping it anyway.
129    fn skip_serializing(&self) -> bool {
130        u32::from(*self) <= 1
131    }
132}
133
134impl From<Version> for NonZeroU32 {
135    fn from(Version(n): Version) -> Self {
136        n
137    }
138}
139
140impl From<Version> for u32 {
141    fn from(Version(n): Version) -> Self {
142        n.into()
143    }
144}
145
146#[derive(Debug, Error)]
147#[non_exhaustive]
148pub enum VersionError {
149    #[error("the version 0 is not supported")]
150    ZeroVersion,
151    #[error("unknown identity document version {0}, only version {IDENTITY_VERSION} is supported")]
152    UnkownVersion(NonZeroU32),
153}
154
155impl VersionError {
156    /// Provide a verbose error.
157    ///
158    /// This will give a user more information on how to upgrade to a newer
159    /// version of an identity document, if there is one.
160    pub fn verbose(&self) -> String {
161        const UNKOWN_VERSION_ERROR: &str = r#"
162Perhaps a new version of the identity document is released which is not supported by the current client.
163See https://radicle.xyz for the latest versions of Radicle.
164The CLI command `rad id migrate` will help to migrate to an up-to-date versions."#;
165
166        match self {
167            err @ Self::ZeroVersion => err.to_string(),
168            err @ Self::UnkownVersion(_) => format!("{err}{UNKOWN_VERSION_ERROR}"),
169        }
170    }
171}
172
173impl TryFrom<u32> for Version {
174    type Error = VersionError;
175
176    fn try_from(n: u32) -> Result<Self, Self::Error> {
177        Version::new(n)
178    }
179}
180
181impl fmt::Display for Version {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        write!(f, "{}", self.0)
184    }
185}
186
187impl<'de> Deserialize<'de> for Version {
188    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
189    where
190        D: serde::Deserializer<'de>,
191    {
192        u32::deserialize(deserializer)
193            .and_then(|v| Version::new(v).map_err(|e| de::Error::custom(e.to_string())))
194    }
195}
196
197/// Used for [`Deserialize`] of a [`Version`] in [`RawDoc`], so that
198/// deserializing a missing version results in `Version(1)`.
199fn missing_version() -> Version {
200    // N.B. the default version is `1` which is non-zero so unsafe is fine here
201    unsafe { Version(NonZeroU32::new_unchecked(1)) }
202}
203
204/// Identifies an identity document payload type.
205#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
206#[serde(transparent)]
207pub struct PayloadId(TypeName);
208
209impl fmt::Display for PayloadId {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        self.0.fmt(f)
212    }
213}
214
215impl FromStr for PayloadId {
216    type Err = TypeNameParse;
217
218    fn from_str(s: &str) -> Result<Self, Self::Err> {
219        TypeName::from_str(s).map(Self)
220    }
221}
222
223impl PayloadId {
224    /// Project payload type.
225    pub fn project() -> Self {
226        Self(
227            // SAFETY: We know this is valid.
228            TypeName::from_str("xyz.radicle.project")
229                .expect("PayloadId::project: type name is valid"),
230        )
231    }
232
233    pub fn canonical_refs() -> Self {
234        Self(
235            // SAFETY: We know this is valid.
236            TypeName::from_str("xyz.radicle.crefs")
237                .expect("PayloadId::canonical_refs: type name is valid"),
238        )
239    }
240}
241
242#[derive(Debug, Error)]
243pub enum PayloadError {
244    #[error("json: {0}")]
245    Json(#[from] serde_json::Error),
246    #[error("payload '{0}' not found in identity document")]
247    NotFound(PayloadId),
248}
249
250/// A `Payload` is a free-form JSON value that can be associated with an
251/// identity's [`Doc`].
252/// The payload is identified in the [`Doc`] by its corresponding [`PayloadId`].
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(transparent)]
255pub struct Payload {
256    value: serde_json::Value,
257}
258
259impl Payload {
260    /// Get a mutable reference to the JSON map, or `None` if the payload is not a map.
261    pub fn as_object_mut(
262        &mut self,
263    ) -> Option<&mut serde_json::value::Map<String, serde_json::Value>> {
264        self.value.as_object_mut()
265    }
266}
267
268impl From<serde_json::Value> for Payload {
269    fn from(value: serde_json::Value) -> Self {
270        Self { value }
271    }
272}
273
274impl Deref for Payload {
275    type Target = serde_json::Value;
276
277    fn deref(&self) -> &Self::Target {
278        &self.value
279    }
280}
281
282/// A verified identity document at a specific commit.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct DocAt {
285    /// The commit at which this document exists.
286    pub commit: Oid,
287    /// The document blob at this commit.
288    pub blob: Oid,
289    /// The parsed document.
290    pub doc: Doc,
291}
292
293impl Deref for DocAt {
294    type Target = Doc;
295
296    fn deref(&self) -> &Self::Target {
297        &self.doc
298    }
299}
300
301impl From<DocAt> for Doc {
302    fn from(value: DocAt) -> Self {
303        value.doc
304    }
305}
306
307impl AsRef<Doc> for DocAt {
308    fn as_ref(&self) -> &Doc {
309        &self.doc
310    }
311}
312
313/// Repository visibility.
314#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase", tag = "type")]
316pub enum Visibility {
317    /// Anyone and everyone.
318    #[default]
319    Public,
320    /// Delegates plus the allowed DIDs.
321    Private {
322        #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
323        allow: BTreeSet<Did>,
324    },
325}
326
327#[derive(Error, Debug)]
328#[error("'{0}' is not a valid visibility type")]
329pub struct VisibilityParseError(String);
330
331impl FromStr for Visibility {
332    type Err = VisibilityParseError;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        match s {
336            "public" => Ok(Visibility::Public),
337            "private" => Ok(Visibility::private([])),
338            _ => Err(VisibilityParseError(s.to_owned())),
339        }
340    }
341}
342
343impl Visibility {
344    /// Check whether the visibility is public.
345    pub fn is_public(&self) -> bool {
346        matches!(self, Self::Public)
347    }
348
349    /// Check whether the visibility is private.
350    pub fn is_private(&self) -> bool {
351        matches!(self, Self::Private { .. })
352    }
353
354    /// Private visibility with list of allowed DIDs beyond the repository delegates.
355    pub fn private(allow: impl IntoIterator<Item = Did>) -> Self {
356        Self::Private {
357            allow: BTreeSet::from_iter(allow),
358        }
359    }
360}
361
362/// `RawDoc` is similar to the [`Doc`] type, however, it can be edited and may
363/// not be valid.
364///
365/// It is expected that any changes to a [`Doc`] are made via [`RawDoc`], and
366/// then verified by using [`RawDoc::verified`].
367///
368/// Note that `RawDoc` only implements [`Deserialize`]. This prevents us from
369/// serializing an unverified document, while also making sure that any document
370/// that is deserialized is verified.
371#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct RawDoc {
374    /// Version of the identity document.
375    #[serde(default = "missing_version")]
376    version: Version,
377    /// The payload section.
378    pub payload: BTreeMap<PayloadId, Payload>,
379    /// The delegates section.
380    pub delegates: Vec<Did>,
381    /// The signature threshold.
382    pub threshold: usize,
383    /// Repository visibility.
384    #[serde(default)]
385    pub visibility: Visibility,
386}
387
388impl TryFrom<RawDoc> for Doc {
389    type Error = DocError;
390
391    fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
392        doc.verified()
393    }
394}
395
396impl RawDoc {
397    /// Construct a new [`RawDoc`] with an initial [`RawDoc::payload`]
398    /// containing the provided [`Project`], and the given `delegates`,
399    /// `threshold`, and `visibility`.
400    pub fn new(
401        project: Project,
402        delegates: Vec<Did>,
403        threshold: usize,
404        visibility: Visibility,
405    ) -> Self {
406        let project =
407            serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
408
409        Self {
410            version: IDENTITY_VERSION,
411            payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
412            delegates,
413            threshold,
414            visibility,
415        }
416    }
417
418    /// Get the version of the document.
419    pub fn version(&self) -> &Version {
420        &self.version
421    }
422
423    /// Get the project payload, if it exists and is valid, out of this document.
424    pub fn project(&self) -> Result<Project, PayloadError> {
425        let value = self
426            .payload
427            .get(&PayloadId::project())
428            .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
429        let proj: Project = serde_json::from_value((**value).clone())?;
430
431        Ok(proj)
432    }
433
434    /// Check if the given `did` is in the set of [`RawDoc::delegates`].
435    pub fn is_delegate(&self, did: &Did) -> bool {
436        self.delegates.contains(did)
437    }
438
439    /// Add a new delegate to the document.
440    ///
441    /// Note that if this `Did` is a duplicate, then the resulting set will only
442    /// show it once.
443    pub fn delegate(&mut self, did: Did) {
444        self.delegates.push(did)
445    }
446
447    /// Remove the `did` from the set of delegates. Returns `true` if it was
448    /// removed.
449    pub fn rescind(&mut self, did: &Did) -> Result<bool, DocError> {
450        let (matches, delegates) = self.delegates.iter().partition(|d| *d == did);
451        self.delegates = delegates;
452        Ok(matches.is_empty().not())
453    }
454
455    /// Construct the `RawDoc` from the set of `bytes` that are expected to be
456    /// in JSON format.
457    pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
458        serde_json::from_slice(bytes).map_err(DocError::from)
459    }
460
461    /// Verify the `RawDoc`'s values, converting it into a valid [`Doc`].
462    ///
463    /// The verifications are as follows:
464    ///
465    ///  - [`RawDoc::delegates`]: any duplicates are removed, and for the
466    ///    remaining set ensure that it is non-empty and does not exceed a
467    ///    length of [`MAX_DELEGATES`].
468    ///  - [`RawDoc::threshold`]: ensure that it is in the range `[1, delegates.len()]`.
469    pub fn verified(self) -> Result<Doc, DocError> {
470        let RawDoc {
471            version,
472            payload,
473            delegates,
474            threshold,
475            visibility,
476        } = self;
477        let delegates = Delegates::new(delegates)?;
478        let threshold = Threshold::new(threshold, &delegates)?;
479        Ok(Doc {
480            version,
481            payload,
482            delegates,
483            threshold,
484            visibility,
485        })
486    }
487}
488
489/// A valid set of delegates for the identity [`Doc`].
490///
491/// It can only be constructed via [`Delegates::new`].
492#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(try_from = "Vec<Did>")]
494pub struct Delegates(NonEmpty<Did>);
495
496impl AsRef<NonEmpty<Did>> for Delegates {
497    fn as_ref(&self) -> &NonEmpty<Did> {
498        &self.0
499    }
500}
501
502impl From<Did> for Delegates {
503    fn from(did: Did) -> Self {
504        Self(NonEmpty::new(did))
505    }
506}
507
508impl TryFrom<Vec<Did>> for Delegates {
509    type Error = DelegatesError;
510
511    fn try_from(dids: Vec<Did>) -> Result<Self, Self::Error> {
512        Delegates::new(dids)
513    }
514}
515
516impl IntoIterator for Delegates {
517    type Item = <NonEmpty<Did> as IntoIterator>::Item;
518    type IntoIter = <NonEmpty<Did> as IntoIterator>::IntoIter;
519
520    fn into_iter(self) -> Self::IntoIter {
521        self.0.into_iter()
522    }
523}
524
525impl Delegates {
526    /// Construct the set of `Delegates` by removing any duplicate [`Did`]s,
527    /// ensure that the set is non-empty, and check the length does not exceed
528    /// the [`MAX_DELEGATES`].
529    pub fn new(delegates: impl IntoIterator<Item = Did>) -> Result<Self, DelegatesError> {
530        let delegates = delegates
531            .into_iter()
532            .try_fold(Vec::<Did>::new(), |mut dids, did| {
533                if !dids.contains(&did) {
534                    if dids.len() >= MAX_DELEGATES {
535                        return Err(DelegatesError("number of delegates cannot exceed 255"));
536                    }
537                    dids.push(did);
538                }
539                Ok(dids)
540            })?;
541        NonEmpty::from_vec(delegates)
542            .map(Self)
543            .ok_or(DelegatesError("delegate list cannot be empty"))
544    }
545
546    /// Get the first delegate in the set.
547    pub fn first(&self) -> &Did {
548        self.0.first()
549    }
550
551    /// Obtain an iterator over the [`Did`]s.
552    pub fn iter(&self) -> impl Iterator<Item = &Did> {
553        self.0.iter()
554    }
555
556    /// Check if the set contains the given `did`.
557    pub fn contains(&self, did: &Did) -> bool {
558        self.0.contains(did)
559    }
560
561    /// Check if the `did` is the only delegate of the repository.
562    pub fn is_only(&self, did: &Did) -> bool {
563        self.0.tail.is_empty() && &self.0.head == did
564    }
565
566    /// Get the number of delegates in the set.
567    pub fn len(&self) -> usize {
568        self.0.len()
569    }
570
571    /// Check if the set is empty. Note that this always returns `false`.
572    pub fn is_empty(&self) -> bool {
573        false
574    }
575}
576
577impl<'a> From<&'a Delegates> for &'a NonEmpty<Did> {
578    fn from(ds: &'a Delegates) -> Self {
579        &ds.0
580    }
581}
582
583impl From<Delegates> for NonEmpty<Did> {
584    fn from(ds: Delegates) -> Self {
585        ds.0
586    }
587}
588
589impl From<Delegates> for Vec<Did> {
590    fn from(Delegates(ds): Delegates) -> Self {
591        ds.into()
592    }
593}
594
595/// A valid threshold for the identity [`Doc`].
596///
597/// It can only be constructed via [`Threshold::new`] or [`Threshold::MIN`].
598#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
599#[serde(transparent)]
600pub struct Threshold(NonZeroUsize);
601
602impl From<Threshold> for usize {
603    fn from(Threshold(t): Threshold) -> Self {
604        t.get()
605    }
606}
607
608impl AsRef<NonZeroUsize> for Threshold {
609    fn as_ref(&self) -> &NonZeroUsize {
610        &self.0
611    }
612}
613
614impl fmt::Display for Threshold {
615    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616        write!(f, "{}", self.0)
617    }
618}
619
620impl Threshold {
621    /// A threshold of `1`.
622    pub const MIN: Threshold = Threshold(NonZeroUsize::MIN);
623
624    /// Construct the `Threshold` by checking that `t` is not greater than
625    /// [`MAX_DELEGATES`], that it does not exceed the number of delegates, and
626    /// is non-zero.
627    pub fn new(t: usize, delegates: &Delegates) -> Result<Self, ThresholdError> {
628        if t > MAX_DELEGATES {
629            Err(ThresholdError(t, "threshold cannot exceed 255"))
630        } else if t > delegates.len() {
631            Err(ThresholdError(
632                t,
633                "threshold cannot exceed number of delegates",
634            ))
635        } else {
636            NonZeroUsize::new(t)
637                .map(Self)
638                .ok_or(ThresholdError(t, "threshold cannot be zero"))
639        }
640    }
641}
642
643/// `Doc` is a valid identity document.
644///
645/// To ensure that only valid documents are used, this type is restricted to be
646/// read-only. For mutating the document use [`Doc::edit`].
647///
648/// A valid `Doc` can be constructed in four ways:
649///
650///   1. [`Doc::initial`]: a safe way to construct the initial document for an identity.
651///   2. [`RawDoc::verified`]: validates a [`RawDoc`]'s fields and converts it
652///      into a `Doc`
653///   3. [`Deserialize`]: will deserialize a `Doc` by first deserializing a
654///      [`RawDoc`] and use [`RawDoc::verified`] to construct the `Doc`.
655///   4. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
656///      its contents.
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
658#[serde(rename_all = "camelCase")]
659#[serde(try_from = "RawDoc")]
660pub struct Doc {
661    #[serde(skip_serializing_if = "Version::skip_serializing")]
662    version: Version,
663    payload: BTreeMap<PayloadId, Payload>,
664    delegates: Delegates,
665    threshold: Threshold,
666    #[serde(default, skip_serializing_if = "Visibility::is_public")]
667    visibility: Visibility,
668}
669
670impl Doc {
671    /// Construct the initial [`Doc`] for an identity.
672    ///
673    /// It will begin with the provided `project` in the [`Doc::payload`], the
674    /// `delegate` as the sole delegate, a threshold of 1, and the given
675    /// `visibility`.
676    pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
677        let project =
678            serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
679
680        Self {
681            version: IDENTITY_VERSION,
682            payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
683            delegates: Delegates(NonEmpty::new(delegate)),
684            threshold: Threshold(NonZeroUsize::MIN),
685            visibility,
686        }
687    }
688
689    /// Construct a [`Doc`] contained in the provided Git blob.
690    pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
691        RawDoc::from_json(blob.content())?.verified()
692    }
693
694    /// Convert the [`Doc`] into a [`RawDoc`] for changing the field values and
695    /// re-verifying.
696    pub fn edit(self) -> RawDoc {
697        let Doc {
698            version,
699            payload,
700            delegates,
701            threshold,
702            visibility,
703        } = self;
704        RawDoc {
705            version,
706            payload,
707            delegates: delegates.into(),
708            threshold: threshold.into(),
709            visibility,
710        }
711    }
712
713    /// Using the current state of the `Doc`, perform any edits on the `RawDoc`
714    /// form and verify the changes.
715    pub fn with_edits<F>(self, f: F) -> Result<Self, DocError>
716    where
717        F: FnOnce(&mut RawDoc),
718    {
719        let mut raw = self.edit();
720        f(&mut raw);
721        raw.verified()
722    }
723
724    /// Get the version of the document.
725    pub fn version(&self) -> &Version {
726        &self.version
727    }
728
729    /// Return the associated payloads for this [`Doc`].
730    pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
731        &self.payload
732    }
733
734    /// Get the project payload, if it exists and is valid, out of this document.
735    pub fn project(&self) -> Result<Project, PayloadError> {
736        let value = self
737            .payload
738            .get(&PayloadId::project())
739            .ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
740        let proj: Project = serde_json::from_value((**value).clone())?;
741
742        Ok(proj)
743    }
744
745    pub fn default_branch_rule(
746        &self,
747    ) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
748        let proj = self.project()?;
749        let refname = proj.default_branch();
750        let pattern = rules::Pattern::try_from(git::refs::branch(refname).to_owned())?;
751        let rule = rules::Rule::new(
752            rules::ResolvedDelegates::Delegates(self.delegates.clone()),
753            self.threshold,
754        );
755        Ok((pattern, rule))
756    }
757
758    /// Return the associated [`Visibility`] of this document.
759    pub fn visibility(&self) -> &Visibility {
760        &self.visibility
761    }
762
763    /// Check whether the visibility of the document is public.
764    pub fn is_public(&self) -> bool {
765        self.visibility.is_public()
766    }
767
768    /// Check whether the visibility of the document is private.
769    pub fn is_private(&self) -> bool {
770        self.visibility.is_private()
771    }
772
773    /// Return the associated threshold of this document.
774    pub fn threshold(&self) -> usize {
775        self.threshold.into()
776    }
777
778    /// Return the associated threshold of this document in its non-zero format.
779    pub fn threshold_nonzero(&self) -> &NonZeroUsize {
780        &self.threshold.0
781    }
782
783    /// Return the associated delegates of this document.
784    pub fn delegates(&self) -> &Delegates {
785        &self.delegates
786    }
787
788    /// Check if the `did` is part of the [`Doc::delegates`] set.
789    pub fn is_delegate(&self, did: &Did) -> bool {
790        self.delegates.contains(did)
791    }
792
793    /// Check whether this document and the associated repository is visible to
794    /// the given peer.
795    pub fn is_visible_to(&self, did: &Did) -> bool {
796        match &self.visibility {
797            Visibility::Public => true,
798            Visibility::Private { allow } => allow.contains(did) || self.is_delegate(did),
799        }
800    }
801
802    /// Validate `signature` using this document's delegates, against a given
803    /// document blob.
804    pub fn verify_signature(
805        &self,
806        key: &PublicKey,
807        signature: &Signature,
808        blob: Oid,
809    ) -> Result<(), PublicKey> {
810        if !self.is_delegate(&key.into()) {
811            return Err(*key);
812        }
813        if key.verify(blob.as_bytes(), signature).is_err() {
814            return Err(*key);
815        }
816        Ok(())
817    }
818
819    /// Check the provided `votes` passes the [`Doc::majority`].
820    pub fn is_majority(&self, votes: usize) -> bool {
821        votes >= self.majority()
822    }
823
824    /// Return the majority number based on the size of the delegates set.
825    pub fn majority(&self) -> usize {
826        self.delegates.len() / 2 + 1
827    }
828
829    /// Helper for getting an `embeds` Git blob.
830    pub(crate) fn blob_at<R: ReadRepository>(
831        commit: Oid,
832        repo: &R,
833    ) -> Result<git2::Blob, DocError> {
834        let path = Path::new("embeds").join(*PATH);
835        repo.blob_at(commit, path.as_path()).map_err(DocError::from)
836    }
837
838    /// Encode the [`Doc`] as canonical JSON, returning the set of bytes and its
839    /// corresponding Git [`Oid`].
840    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
841        let mut buf = Vec::new();
842        let mut serializer =
843            serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());
844
845        self.serialize(&mut serializer)?;
846        let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
847
848        Ok((oid.into(), buf))
849    }
850
851    /// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
852    /// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
853    pub fn sign<G>(&self, signer: &G) -> Result<(git::Oid, Vec<u8>, Signature), DocError>
854    where
855        G: crypto::signature::Signer<crypto::Signature>,
856    {
857        let (oid, bytes) = self.encode()?;
858        let sig = signer.sign(oid.as_bytes());
859
860        Ok((oid, bytes, sig))
861    }
862
863    /// Similar to [`Doc::sign`], but only returning the [`Signature`].
864    pub fn signature_of<G>(&self, signer: &G) -> Result<Signature, DocError>
865    where
866        G: crypto::signature::Signer<crypto::Signature>,
867    {
868        let (_, _, sig) = self.sign(signer)?;
869
870        Ok(sig)
871    }
872
873    /// Load the [`DocAt`] found at the given `commit`. The [`DocAt`] will
874    /// contain the corresponding [`Doc`].
875    pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
876        let blob = Self::blob_at(commit, repo)?;
877        let doc = Self::from_blob(&blob)?;
878
879        Ok(DocAt {
880            commit,
881            doc,
882            blob: blob.id().into(),
883        })
884    }
885
886    /// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
887    /// document.
888    pub fn init<G>(
889        &self,
890        repo: &storage::git::Repository,
891        signer: &Device<G>,
892    ) -> Result<git::Oid, RepositoryError>
893    where
894        G: crypto::signature::Signer<crypto::Signature>,
895    {
896        let cob = identity::Identity::initialize(self, repo, signer)?;
897        let id_ref = git::refs::storage::id(signer.public_key());
898        let cob_ref = git::refs::storage::cob(
899            signer.public_key(),
900            &crate::cob::identity::TYPENAME,
901            &cob.id,
902        );
903        // Set `.../refs/rad/id` -> `.../refs/cobs/xyz.radicle.id/<id>`
904        repo.backend.reference_symbolic(
905            id_ref.as_str(),
906            cob_ref.as_str(),
907            false,
908            "Create `rad/id` reference to point to new identity COB",
909        )?;
910
911        Ok(*cob.id)
912    }
913}
914
915#[derive(Debug, Error)]
916pub enum CanonicalRefsError {
917    #[error(transparent)]
918    Json(#[from] serde_json::Error),
919    #[error(transparent)]
920    CanonicalRefs(#[from] rules::ValidationError),
921    #[error(transparent)]
922    DefaultBranch(#[from] DefaultBranchRuleError),
923}
924
925impl crefs::GetCanonicalRefs for Doc {
926    type Error = CanonicalRefsError;
927
928    fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
929        self.raw_canonical_refs().and_then(|raw| {
930            raw.map(|raw| {
931                raw.try_into_canonical_refs(&mut || self.delegates.clone())
932                    .map_err(CanonicalRefsError::from)
933                    .and_then(|mut crefs| {
934                        self.default_branch_rule()
935                            .map_err(CanonicalRefsError::from)
936                            .map(|rule| {
937                                crefs.extend([rule]);
938                                crefs
939                            })
940                    })
941            })
942            .transpose()
943        })
944    }
945
946    fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
947        let value = self.payload.get(&PayloadId::canonical_refs());
948        let crefs = value
949            .map(|value| {
950                serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
951            })
952            .transpose()?;
953        Ok(crefs)
954    }
955}
956
957impl crefs::GetCanonicalRefs for RawDoc {
958    type Error = CanonicalRefsError;
959
960    fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
961        Ok(None)
962    }
963
964    fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
965        let value = self.payload.get(&PayloadId::canonical_refs());
966        let crefs = value
967            .map(|value| {
968                serde_json::from_value((**value).clone()).map_err(CanonicalRefsError::from)
969            })
970            .transpose()?;
971        Ok(crefs)
972    }
973}
974
975#[cfg(test)]
976#[allow(clippy::unwrap_used)]
977mod test {
978    use serde_json::json;
979
980    use crate::assert_matches;
981    use crate::rad;
982    use crate::storage::git::transport;
983    use crate::storage::git::Storage;
984    use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
985    use crate::test::arbitrary;
986    use crate::test::arbitrary::gen;
987    use crate::test::fixtures;
988
989    use super::*;
990    use qcheck_macros::quickcheck;
991
992    #[test]
993    fn test_duplicate_dids() {
994        let delegate = Device::mock_from_seed([0xff; 32]);
995        let did = Did::from(delegate.public_key());
996        let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
997        doc.delegate(did);
998        let doc = doc.verified().unwrap();
999        assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
1000        assert!(doc.delegates().first() == &did)
1001    }
1002
1003    #[test]
1004    fn test_max_delegates() {
1005        // Generate more than the max delegates
1006        let delegates = (0..MAX_DELEGATES + 1).map(gen).collect::<Vec<Did>>();
1007
1008        // A document with max delegates will be fine
1009        let doc = RawDoc::new(
1010            gen::<Project>(1),
1011            delegates[0..MAX_DELEGATES].into(),
1012            1,
1013            Visibility::Public,
1014        );
1015        assert_matches!(doc.verified(), Ok(_));
1016
1017        // A document that exceeds max delegates should fail
1018        let doc = RawDoc::new(gen::<Project>(1), delegates, 1, Visibility::Public);
1019        assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
1020    }
1021
1022    #[test]
1023    fn test_is_valid_version() {
1024        // 0 is not a valid version
1025        assert!(!Version::is_valid_version(&0));
1026
1027        // Ensures that the latest version is always valid
1028        let current = IDENTITY_VERSION.number();
1029        assert!(Version::is_valid_version(&current.into()));
1030
1031        // Ensures that the next version is not valid because we have not
1032        // defined it yet
1033        let next = current.checked_add(1).unwrap();
1034        assert!(!Version::is_valid_version(&next.into()));
1035    }
1036
1037    #[test]
1038    fn test_future_version_error() {
1039        let v = Version(NonZeroU32::MAX).to_string();
1040        assert_eq!(
1041            serde_json::from_str::<Version>(&v)
1042                .expect_err("should fail to deserialize")
1043                .to_string(),
1044            VersionError::UnkownVersion(NonZeroU32::MAX).to_string(),
1045        )
1046    }
1047
1048    #[test]
1049    fn test_parse_version() {
1050        // Original document before introducing the version field
1051        let v1 = json!(
1052            {
1053                "payload": {
1054                    "xyz.radicle.project": {
1055                        "defaultBranch": "master",
1056                        "description": "Radicle Heartwood Protocol & Stack",
1057                        "name": "heartwood"
1058                    }
1059                },
1060                "delegates": [
1061                    "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
1062                    "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
1063                    "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
1064                ],
1065                "threshold": 1
1066            }
1067        );
1068
1069        // Deserializing the `RawDoc` should not fail and should include the
1070        // `IDENTITY_VERSION`.
1071        let doc = serde_json::from_str::<RawDoc>(&v1.to_string()).unwrap();
1072        let payload = [(
1073            PayloadId::project(),
1074            Payload {
1075                value: json!({
1076                    "name": "heartwood",
1077                    "description": "Radicle Heartwood Protocol & Stack",
1078                    "defaultBranch": "master",
1079                }),
1080            },
1081        )]
1082        .into_iter()
1083        .collect::<BTreeMap<_, _>>();
1084        let delegates = vec![
1085            "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
1086                .parse::<Did>()
1087                .unwrap(),
1088            "did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW"
1089                .parse::<Did>()
1090                .unwrap(),
1091            "did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM"
1092                .parse::<Did>()
1093                .unwrap(),
1094        ];
1095        // And this is the expected outcome of the deserialization
1096        assert_eq!(
1097            doc,
1098            RawDoc {
1099                version: IDENTITY_VERSION,
1100                payload: payload.clone(),
1101                delegates: delegates.clone(),
1102                threshold: 1,
1103                visibility: Visibility::Public,
1104            }
1105        );
1106
1107        // Deserializing into `Doc` should also succeed.
1108        let verified = serde_json::from_str::<Doc>(&v1.to_string()).unwrap();
1109        let delegates = Delegates(NonEmpty::from_vec(delegates).unwrap());
1110        assert_eq!(
1111            verified,
1112            Doc {
1113                version: IDENTITY_VERSION,
1114                threshold: Threshold::new(1, &delegates).unwrap(),
1115                payload: payload.clone(),
1116                delegates,
1117                visibility: Visibility::Public,
1118            }
1119        );
1120    }
1121
1122    #[test]
1123    fn test_canonical_example() {
1124        let tempdir = tempfile::tempdir().unwrap();
1125        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1126
1127        transport::local::register(storage.clone());
1128
1129        let delegate = Device::mock_from_seed([0xff; 32]);
1130        let (repo, _) = fixtures::repository(tempdir.path().join("working"));
1131        let (id, _, _) = rad::init(
1132            &repo,
1133            "heartwood".try_into().unwrap(),
1134            "Radicle Heartwood Protocol & Stack",
1135            git::refname!("master"),
1136            Visibility::default(),
1137            &delegate,
1138            &storage,
1139        )
1140        .unwrap();
1141
1142        assert_eq!(
1143            delegate.public_key().to_human(),
1144            String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi")
1145        );
1146        assert_eq!(
1147            (*id).to_string(),
1148            "d96f425412c9f8ad5d9a9a05c9831d0728e2338d"
1149        );
1150        assert_eq!(id.urn(), String::from("rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji"));
1151    }
1152
1153    #[test]
1154    fn test_not_found() {
1155        let tempdir = tempfile::tempdir().unwrap();
1156        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1157        let remote = arbitrary::gen::<RemoteId>(1);
1158        let proj = arbitrary::gen::<RepoId>(1);
1159        let repo = storage.create(proj).unwrap();
1160        let oid = git2::Oid::from_str("2d52a53ce5e4f141148a5f770cfd3ead2d6a45b8").unwrap();
1161
1162        let err = repo.identity_head_of(&remote).unwrap_err();
1163        matches!(err, git::ext::Error::NotFound(_));
1164
1165        let err = Doc::load_at(oid.into(), &repo).unwrap_err();
1166        assert!(err.is_not_found());
1167    }
1168
1169    #[test]
1170    fn test_canonical_doc() {
1171        let tempdir = tempfile::tempdir().unwrap();
1172        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1173        transport::local::register(storage.clone());
1174
1175        let (working, _) = fixtures::repository(tempdir.path().join("working"));
1176
1177        let delegate = Device::mock_from_seed([0xff; 32]);
1178        let (rid, doc, _) = rad::init(
1179            &working,
1180            "heartwood".try_into().unwrap(),
1181            "Radicle Heartwood Protocol & Stack",
1182            git::refname!("master"),
1183            Visibility::default(),
1184            &delegate,
1185            &storage,
1186        )
1187        .unwrap();
1188        let repo = storage.repository(rid).unwrap();
1189
1190        assert_eq!(doc, repo.identity_doc().unwrap().doc);
1191    }
1192
1193    #[quickcheck]
1194    fn prop_encode_decode(doc: Doc) {
1195        let (_, bytes) = doc.encode().unwrap();
1196        assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
1197    }
1198
1199    #[test]
1200    fn test_visibility_json() {
1201        use std::str::FromStr;
1202
1203        assert_eq!(
1204            serde_json::to_value(Visibility::Public).unwrap(),
1205            serde_json::json!({ "type": "public" })
1206        );
1207        assert_eq!(
1208            serde_json::to_value(Visibility::private([])).unwrap(),
1209            serde_json::json!({ "type": "private" })
1210        );
1211        assert_eq!(
1212            serde_json::to_value(Visibility::private([Did::from_str(
1213                "did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"
1214            )
1215            .unwrap()]))
1216            .unwrap(),
1217            serde_json::json!({ "type": "private", "allow": ["did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT"] })
1218        );
1219    }
1220}