radicle: refactor doc
The aim of this change is to make the `Doc` type more safe to use by approaching the design via [Parse don't validate][[0]] approach. The problem with the previous approach was that all field were `pub` and thus a `Doc<Verified>` could easily be mutated and serialized. Granted, the code that used the serialization would tend to verify the `Doc` first, however, this approach *ensures* that only a verified `Doc` can be serialized. It also meant that trying to add new data that would follow the parse approach would require more generic parameters on top of the existing `PhantomData` parameter, i.e. we need to do something like: `Doc<RawField, V> -> Doc<ValidField, V>`. The new approach splits the type into two separate types: `RawDoc` and `Doc`. The former is allowed to be mutated at will, and uses types that are less strict. The latter is the valid type that can only be constructed by validating a `RawDoc` (or the `initial` constructor). The `Doc` type's fields can then only be accessed by read-only methods. Solves the problems above by only allowing mutations to `RawDoc`, as well as, new fields being added to `RawDoc` which are then validated via `RawDoc::verified`. [0]: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
This commit is contained in:
parent
f83c116742
commit
de1958fab0
|
|
@ -84,11 +84,10 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
|
||||||
let id = options.id;
|
let id = options.id;
|
||||||
let storage = &profile.storage;
|
let storage = &profile.storage;
|
||||||
let remote = options.remote.unwrap_or(profile.did());
|
let remote = options.remote.unwrap_or(profile.did());
|
||||||
let doc: Doc<_> = storage
|
let doc = storage
|
||||||
.repository(id)?
|
.repository(id)?
|
||||||
.identity_doc()
|
.identity_doc()
|
||||||
.context("repository could not be found in local storage")?
|
.context("repository could not be found in local storage")?;
|
||||||
.into();
|
|
||||||
let payload = doc.project()?;
|
let payload = doc.project()?;
|
||||||
let path = PathBuf::from(payload.name());
|
let path = PathBuf::from(payload.name());
|
||||||
|
|
||||||
|
|
@ -115,7 +114,8 @@ fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
|
|
||||||
let remotes = doc
|
let remotes = doc
|
||||||
.delegates
|
.delegates()
|
||||||
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|did| *did)
|
.map(|did| *did)
|
||||||
.filter(|id| id != profile.id())
|
.filter(|id| id != profile.id())
|
||||||
|
|
|
||||||
|
|
@ -153,7 +153,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
&profile.storage,
|
&profile.storage,
|
||||||
)?;
|
)?;
|
||||||
let delegates = doc
|
let delegates = doc
|
||||||
.delegates
|
.delegates()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|d| **d)
|
.map(|d| **d)
|
||||||
.filter(|id| id != profile.id())
|
.filter(|id| id != profile.id())
|
||||||
|
|
@ -240,7 +240,7 @@ pub fn clone<G: Signer>(
|
||||||
(
|
(
|
||||||
raw::Repository,
|
raw::Repository,
|
||||||
storage::git::Repository,
|
storage::git::Repository,
|
||||||
Doc<Verified>,
|
Doc,
|
||||||
Project,
|
Project,
|
||||||
),
|
),
|
||||||
CloneError,
|
CloneError,
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ use std::{ffi::OsString, io};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
|
|
||||||
use nonempty::NonEmpty;
|
|
||||||
use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
|
use radicle::cob::identity::{self, IdentityMut, Revision, RevisionId};
|
||||||
use radicle::identity::{doc, Identity, Visibility};
|
use radicle::identity::{doc, Doc, Identity, RawDoc, Visibility};
|
||||||
use radicle::prelude::{Did, Doc, RepoId, Signer};
|
use radicle::prelude::{Did, RepoId, Signer};
|
||||||
use radicle::storage::refs;
|
use radicle::storage::refs;
|
||||||
use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
|
use radicle::storage::{ReadRepository, ReadStorage as _, WriteRepository};
|
||||||
use radicle::{cob, Profile};
|
use radicle::{cob, Profile};
|
||||||
use radicle_crypto::Verified;
|
|
||||||
use radicle_surf::diff::Diff;
|
use radicle_surf::diff::Diff;
|
||||||
use radicle_term::Element;
|
use radicle_term::Element;
|
||||||
use serde_json as json;
|
use serde_json as json;
|
||||||
|
|
@ -389,7 +387,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
payload,
|
payload,
|
||||||
} => {
|
} => {
|
||||||
let proposal = {
|
let proposal = {
|
||||||
let mut proposal = current.doc.clone();
|
let mut proposal = current.doc.clone().edit();
|
||||||
proposal.threshold = threshold.unwrap_or(proposal.threshold);
|
proposal.threshold = threshold.unwrap_or(proposal.threshold);
|
||||||
|
|
||||||
if !allow.is_disjoint(&disallow) {
|
if !allow.is_disjoint(&disallow) {
|
||||||
|
|
@ -429,18 +427,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
proposal.visibility = Visibility::Public;
|
proposal.visibility = Visibility::Public;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proposal.delegates = NonEmpty::from_vec(
|
proposal.delegates = proposal
|
||||||
proposal
|
.delegates
|
||||||
.delegates
|
.into_iter()
|
||||||
.into_iter()
|
.chain(delegates)
|
||||||
.chain(delegates)
|
.filter(|d| !rescind.contains(d))
|
||||||
.filter(|d| !rescind.contains(d))
|
.collect::<Vec<_>>();
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
.ok_or(anyhow!(
|
|
||||||
"at lease one delegate must be present for the identity to be valid"
|
|
||||||
))?;
|
|
||||||
|
|
||||||
if let Some(errs) = verify_delegates(&proposal, &repo)? {
|
if let Some(errs) = verify_delegates(&proposal, &repo)? {
|
||||||
term::error(format!("failed to verify delegates for {rid}"));
|
term::error(format!("failed to verify delegates for {rid}"));
|
||||||
term::error(format!(
|
term::error(format!(
|
||||||
|
|
@ -475,6 +467,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
proposal
|
proposal
|
||||||
};
|
};
|
||||||
|
let proposal = proposal.verified()?;
|
||||||
if proposal == current.doc {
|
if proposal == current.doc {
|
||||||
if !options.quiet {
|
if !options.quiet {
|
||||||
term::print(term::format::italic(
|
term::print(term::format::italic(
|
||||||
|
|
@ -578,11 +571,7 @@ fn get<'a>(
|
||||||
Ok(revision)
|
Ok(revision)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_meta(
|
fn print_meta(revision: &Revision, previous: &Doc, profile: &Profile) -> anyhow::Result<()> {
|
||||||
revision: &Revision,
|
|
||||||
previous: &Doc<Verified>,
|
|
||||||
profile: &Profile,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut attrs = term::Table::<2, term::Label>::new(Default::default());
|
let mut attrs = term::Table::<2, term::Label>::new(Default::default());
|
||||||
|
|
||||||
attrs.push([
|
attrs.push([
|
||||||
|
|
@ -630,7 +619,7 @@ fn print_meta(
|
||||||
let accepted = revision.accepted().collect::<Vec<_>>();
|
let accepted = revision.accepted().collect::<Vec<_>>();
|
||||||
let rejected = revision.rejected().collect::<Vec<_>>();
|
let rejected = revision.rejected().collect::<Vec<_>>();
|
||||||
let unknown = previous
|
let unknown = previous
|
||||||
.delegates
|
.delegates()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|id| !accepted.contains(id) && !rejected.contains(id))
|
.filter(|id| !accepted.contains(id) && !rejected.contains(id))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
@ -712,7 +701,7 @@ and description.
|
||||||
fn update<R: WriteRepository + cob::Store, G: Signer>(
|
fn update<R: WriteRepository + cob::Store, G: Signer>(
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
doc: Doc<Verified>,
|
doc: Doc,
|
||||||
current: &mut IdentityMut<R>,
|
current: &mut IdentityMut<R>,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> anyhow::Result<Revision> {
|
) -> anyhow::Result<Revision> {
|
||||||
|
|
@ -734,14 +723,14 @@ fn print_diff(
|
||||||
repo: &radicle::storage::git::Repository,
|
repo: &radicle::storage::git::Repository,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let previous = if let Some(previous) = previous {
|
let previous = if let Some(previous) = previous {
|
||||||
let previous = Doc::<Verified>::load_at(*previous, repo)?;
|
let previous = Doc::load_at(*previous, repo)?;
|
||||||
let previous = serde_json::to_string_pretty(&previous.doc)?;
|
let previous = serde_json::to_string_pretty(&previous.doc)?;
|
||||||
|
|
||||||
Some(previous)
|
Some(previous)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let current = Doc::<Verified>::load_at(*current, repo)?;
|
let current = Doc::load_at(*current, repo)?;
|
||||||
let current = serde_json::to_string_pretty(¤t.doc)?;
|
let current = serde_json::to_string_pretty(¤t.doc)?;
|
||||||
|
|
||||||
let tmp = tempfile::tempdir()?;
|
let tmp = tempfile::tempdir()?;
|
||||||
|
|
@ -798,8 +787,8 @@ impl VerificationError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_delegates<S, V>(
|
fn verify_delegates<S>(
|
||||||
proposal: &Doc<V>,
|
proposal: &RawDoc,
|
||||||
repo: &S,
|
repo: &S,
|
||||||
) -> anyhow::Result<Option<Vec<VerificationError>>>
|
) -> anyhow::Result<Option<Vec<VerificationError>>>
|
||||||
where
|
where
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,14 @@ use std::str::FromStr;
|
||||||
use anyhow::{anyhow, bail, Context as _};
|
use anyhow::{anyhow, bail, Context as _};
|
||||||
use serde_json as json;
|
use serde_json as json;
|
||||||
|
|
||||||
use radicle::crypto::{ssh, Verified};
|
use radicle::crypto::ssh;
|
||||||
use radicle::explorer::ExplorerUrl;
|
use radicle::explorer::ExplorerUrl;
|
||||||
use radicle::git::RefString;
|
use radicle::git::RefString;
|
||||||
use radicle::identity::project::ProjectName;
|
use radicle::identity::project::ProjectName;
|
||||||
use radicle::identity::{RepoId, Visibility};
|
use radicle::identity::{RepoId, Doc, Visibility};
|
||||||
use radicle::node::events::UploadPack;
|
use radicle::node::events::UploadPack;
|
||||||
use radicle::node::policy::Scope;
|
use radicle::node::policy::Scope;
|
||||||
use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
|
use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
|
||||||
use radicle::prelude::Doc;
|
|
||||||
use radicle::storage::ReadStorage as _;
|
use radicle::storage::ReadStorage as _;
|
||||||
use radicle::{profile, Node};
|
use radicle::{profile, Node};
|
||||||
|
|
||||||
|
|
@ -302,7 +301,7 @@ pub fn init(
|
||||||
if options.seed {
|
if options.seed {
|
||||||
profile.seed(rid, options.scope, &mut node)?;
|
profile.seed(rid, options.scope, &mut node)?;
|
||||||
|
|
||||||
if doc.visibility.is_public() {
|
if doc.is_public() {
|
||||||
profile.add_inventory(rid, &mut node)?;
|
profile.add_inventory(rid, &mut node)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -529,11 +528,11 @@ fn sync(
|
||||||
|
|
||||||
pub fn announce(
|
pub fn announce(
|
||||||
rid: RepoId,
|
rid: RepoId,
|
||||||
doc: Doc<Verified>,
|
doc: Doc,
|
||||||
node: &mut Node,
|
node: &mut Node,
|
||||||
config: &profile::Config,
|
config: &profile::Config,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if doc.visibility.is_public() {
|
if doc.is_public() {
|
||||||
match sync(rid, node, config) {
|
match sync(rid, node, config) {
|
||||||
Ok(SyncResult::Synced {
|
Ok(SyncResult::Synced {
|
||||||
result: Some(url), ..
|
result: Some(url), ..
|
||||||
|
|
@ -597,7 +596,7 @@ pub fn announce(
|
||||||
} else {
|
} else {
|
||||||
term::info!(
|
term::info!(
|
||||||
"You have created a {} repository.",
|
"You have created a {} repository.",
|
||||||
term::format::visibility(&doc.visibility)
|
term::format::visibility(doc.visibility())
|
||||||
);
|
);
|
||||||
term::info!(
|
term::info!(
|
||||||
"This repository will only be visible to you, \
|
"This repository will only be visible to you, \
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Target::Payload => {
|
Target::Payload => {
|
||||||
let (_, doc) = repo(rid, storage)?;
|
let (_, doc) = repo(rid, storage)?;
|
||||||
json::to_pretty(&doc.payload, Path::new("radicle.json"))?.print();
|
json::to_pretty(&doc.payload(), Path::new("radicle.json"))?.print();
|
||||||
}
|
}
|
||||||
Target::Identity => {
|
Target::Identity => {
|
||||||
let (_, doc) = repo(rid, storage)?;
|
let (_, doc) = repo(rid, storage)?;
|
||||||
|
|
@ -195,7 +195,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
Target::Delegates => {
|
Target::Delegates => {
|
||||||
let (_, doc) = repo(rid, storage)?;
|
let (_, doc) = repo(rid, storage)?;
|
||||||
let aliases = profile.aliases();
|
let aliases = profile.aliases();
|
||||||
for did in &doc.delegates {
|
for did in doc.delegates().iter() {
|
||||||
if let Some(alias) = aliases.alias(did) {
|
if let Some(alias) = aliases.alias(did) {
|
||||||
println!(
|
println!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
|
|
@ -209,7 +209,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Target::Visibility => {
|
Target::Visibility => {
|
||||||
let (_, doc) = repo(rid, storage)?;
|
let (_, doc) = repo(rid, storage)?;
|
||||||
println!("{}", term::format::visibility(&doc.visibility));
|
println!("{}", term::format::visibility(doc.visibility()));
|
||||||
}
|
}
|
||||||
Target::History => {
|
Target::History => {
|
||||||
let (repo, _) = repo(rid, storage)?;
|
let (repo, _) = repo(rid, storage)?;
|
||||||
|
|
|
||||||
|
|
@ -105,10 +105,10 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
..
|
..
|
||||||
} in repos
|
} in repos
|
||||||
{
|
{
|
||||||
if doc.visibility.is_public() && options.private && !options.public {
|
if doc.is_public() && options.private && !options.public {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !doc.visibility.is_public() && !options.private && options.public {
|
if !doc.is_public() && !options.private && options.public {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if refs.is_none() && !options.all && !options.seeded {
|
if refs.is_none() && !options.all && !options.seeded {
|
||||||
|
|
@ -135,7 +135,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
term::format::bold(proj.name().to_owned()),
|
term::format::bold(proj.name().to_owned()),
|
||||||
term::format::tertiary(rid.urn()),
|
term::format::tertiary(rid.urn()),
|
||||||
if seeded {
|
if seeded {
|
||||||
term::format::visibility(&doc.visibility).into()
|
term::format::visibility(doc.visibility()).into()
|
||||||
} else {
|
} else {
|
||||||
term::format::dim("local").into()
|
term::format::dim("local").into()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -77,19 +77,19 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
|
|
||||||
let repo = profile.storage.repository_mut(rid)?;
|
let repo = profile.storage.repository_mut(rid)?;
|
||||||
let mut identity = Identity::load_mut(&repo)?;
|
let mut identity = Identity::load_mut(&repo)?;
|
||||||
let mut doc = identity.doc().clone();
|
let doc = identity.doc();
|
||||||
|
|
||||||
if doc.visibility.is_public() {
|
if doc.is_public() {
|
||||||
return Err(Error::WithHint {
|
return Err(Error::WithHint {
|
||||||
err: anyhow!("repository is already public"),
|
err: anyhow!("repository is already public"),
|
||||||
hint: "to announce the repository to the network, run `rad sync --inventory`",
|
hint: "to announce the repository to the network, run `rad sync --inventory`",
|
||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
if !doc.is_delegate(profile.id()) {
|
if !doc.is_delegate(&profile.id().into()) {
|
||||||
return Err(anyhow!("only the repository delegate can publish it"));
|
return Err(anyhow!("only the repository delegate can publish it"));
|
||||||
}
|
}
|
||||||
if doc.delegates.len() > 1 {
|
if doc.delegates().len() > 1 {
|
||||||
return Err(Error::WithHint {
|
return Err(Error::WithHint {
|
||||||
err: anyhow!(
|
err: anyhow!(
|
||||||
"only repositories with a single delegate can be published with this command"
|
"only repositories with a single delegate can be published with this command"
|
||||||
|
|
@ -101,7 +101,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
let signer = profile.signer()?;
|
let signer = profile.signer()?;
|
||||||
|
|
||||||
// Update identity document.
|
// Update identity document.
|
||||||
doc.visibility = Visibility::Public;
|
let doc = doc.clone().with_edits(|doc| {
|
||||||
|
doc.visibility = Visibility::Public;
|
||||||
|
})?;
|
||||||
|
|
||||||
identity.update("Publish repository", "", &doc, &signer)?;
|
identity.update("Publish repository", "", &doc, &signer)?;
|
||||||
repo.sign_refs(&signer)?;
|
repo.sign_refs(&signer)?;
|
||||||
|
|
@ -123,7 +125,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
|
|
||||||
term::success!(
|
term::success!(
|
||||||
"Repository is now {}",
|
"Repository is now {}",
|
||||||
term::format::visibility(&doc.visibility)
|
term::format::visibility(doc.visibility())
|
||||||
);
|
);
|
||||||
|
|
||||||
if !node.is_running() {
|
if !node.is_running() {
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ pub fn update(
|
||||||
let outcome = if updated { "updated" } else { "exists" };
|
let outcome = if updated { "updated" } else { "exists" };
|
||||||
|
|
||||||
if let Ok(repo) = profile.storage.repository(rid) {
|
if let Ok(repo) = profile.storage.repository(rid) {
|
||||||
if repo.identity_doc()?.visibility.is_public() {
|
if repo.identity_doc()?.is_public() {
|
||||||
profile.add_inventory(rid, node)?;
|
profile.add_inventory(rid, node)?;
|
||||||
term::success!("Inventory updated with {}", term::format::tertiary(rid));
|
term::success!("Inventory updated with {}", term::format::tertiary(rid));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ fn announce_<R: ReadRepository>(
|
||||||
let rid = repo.id();
|
let rid = repo.id();
|
||||||
let doc = repo.identity_doc()?;
|
let doc = repo.identity_doc()?;
|
||||||
let mut settings = settings.with_profile(profile);
|
let mut settings = settings.with_profile(profile);
|
||||||
let unsynced: Vec<_> = if doc.visibility.is_public() {
|
let unsynced: Vec<_> = if doc.is_public() {
|
||||||
// All seeds.
|
// All seeds.
|
||||||
let all = node.seeds(rid)?;
|
let all = node.seeds(rid)?;
|
||||||
if all.is_empty() {
|
if all.is_empty() {
|
||||||
|
|
@ -218,7 +218,7 @@ fn announce_<R: ReadRepository>(
|
||||||
} else {
|
} else {
|
||||||
node.sessions()?
|
node.sessions()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid))
|
.filter(|s| s.state.is_connected() && doc.is_visible_to(&s.nid.into()))
|
||||||
.map(|s| s.nid)
|
.map(|s| s.nid)
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ use std::sync::atomic::{self, AtomicBool};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use bstr::BString;
|
use bstr::BString;
|
||||||
use radicle::crypto::{PublicKey, Verified};
|
use radicle::crypto::PublicKey;
|
||||||
use radicle::git::Oid;
|
use radicle::git::Oid;
|
||||||
use radicle::identity::DocError;
|
use radicle::identity::{DocError, Doc};
|
||||||
use radicle::prelude::Doc;
|
|
||||||
use radicle::storage::git::Repository;
|
use radicle::storage::git::Repository;
|
||||||
use radicle::storage::ReadRepository;
|
use radicle::storage::ReadRepository;
|
||||||
|
|
||||||
|
|
@ -74,7 +73,7 @@ impl<S> Handle<S> {
|
||||||
self.interrupt.store(true, atomic::Ordering::Relaxed);
|
self.interrupt.store(true, atomic::Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verified(&self, head: Oid) -> Result<Doc<Verified>, DocError> {
|
pub fn verified(&self, head: Oid) -> Result<Doc, DocError> {
|
||||||
Ok(self.repo.identity_doc_at(head)?.doc)
|
Ok(self.repo.identity_doc_at(head)?.doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,8 +207,8 @@ impl ProtocolStage for CanonicalId {
|
||||||
remote: self.remote,
|
remote: self.remote,
|
||||||
err: Box::new(err),
|
err: Box::new(err),
|
||||||
})?;
|
})?;
|
||||||
if verified.delegates.contains(&self.remote.into()) {
|
if verified.is_delegate(&self.remote.into()) {
|
||||||
let is_delegate = |remote: &PublicKey| verified.is_delegate(remote);
|
let is_delegate = |remote: &PublicKey| verified.is_delegate(&remote.into());
|
||||||
Ok(Updates::build(
|
Ok(Updates::build(
|
||||||
refs.iter()
|
refs.iter()
|
||||||
.filter_map(|r| r.as_special_ref_update(is_delegate)),
|
.filter_map(|r| r.as_special_ref_update(is_delegate)),
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use std::time::Instant;
|
||||||
use gix_protocol::handshake;
|
use gix_protocol::handshake;
|
||||||
use radicle::crypto::PublicKey;
|
use radicle::crypto::PublicKey;
|
||||||
use radicle::git::{Oid, Qualified};
|
use radicle::git::{Oid, Qualified};
|
||||||
use radicle::identity::{Did, Doc, DocError};
|
use radicle::identity::{Did, DocError, Doc};
|
||||||
|
|
||||||
use radicle::prelude::Verified;
|
use radicle::prelude::Verified;
|
||||||
use radicle::storage;
|
use radicle::storage;
|
||||||
|
|
@ -389,11 +389,11 @@ impl FetchState {
|
||||||
.canonical()?
|
.canonical()?
|
||||||
.ok_or(error::Protocol::MissingRadId)?;
|
.ok_or(error::Protocol::MissingRadId)?;
|
||||||
|
|
||||||
let is_delegate = anchor.delegates.contains(&Did::from(handle.local()));
|
let is_delegate = anchor.is_delegate(&Did::from(handle.local()));
|
||||||
// TODO: not sure we should allow to block *any* peer from the
|
// TODO: not sure we should allow to block *any* peer from the
|
||||||
// delegate set. We could end up ignoring delegates.
|
// delegate set. We could end up ignoring delegates.
|
||||||
let delegates = anchor
|
let delegates = anchor
|
||||||
.delegates
|
.delegates()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|id| !handle.is_blocked(id))
|
.filter(|id| !handle.is_blocked(id))
|
||||||
.map(|did| PublicKey::from(*did))
|
.map(|did| PublicKey::from(*did))
|
||||||
|
|
@ -404,9 +404,9 @@ impl FetchState {
|
||||||
// The local peer does not need to count towards the threshold
|
// The local peer does not need to count towards the threshold
|
||||||
// since they must be valid already.
|
// since they must be valid already.
|
||||||
let threshold = if is_delegate {
|
let threshold = if is_delegate {
|
||||||
anchor.threshold - 1
|
anchor.threshold() - 1
|
||||||
} else {
|
} else {
|
||||||
anchor.threshold
|
anchor.threshold()
|
||||||
};
|
};
|
||||||
let signed_refs = self.run_special_refs(
|
let signed_refs = self.run_special_refs(
|
||||||
handle,
|
handle,
|
||||||
|
|
@ -637,11 +637,11 @@ impl<'a, S> Cached<'a, S> {
|
||||||
self.state.canonical_rad_id().copied()
|
self.state.canonical_rad_id().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verified(&self, head: Oid) -> Result<Doc<Verified>, DocError> {
|
pub fn verified(&self, head: Oid) -> Result<Doc, DocError> {
|
||||||
self.handle.verified(head)
|
self.handle.verified(head)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn canonical(&self) -> Result<Option<Doc<Verified>>, error::Canonical> {
|
pub fn canonical(&self) -> Result<Option<Doc>, error::Canonical> {
|
||||||
let tip = self.refname_to_id(refs::REFS_RAD_ID.clone())?;
|
let tip = self.refname_to_id(refs::REFS_RAD_ID.clone())?;
|
||||||
let cached_tip = self.canonical_rad_id();
|
let cached_tip = self.canonical_rad_id();
|
||||||
|
|
||||||
|
|
@ -736,7 +736,7 @@ impl<'a, S> ValidateRepository for Cached<'a, S> {
|
||||||
/// N.b. if the repository does not have the project payload or a
|
/// N.b. if the repository does not have the project payload or a
|
||||||
/// deserialization error occurs, then this will return `None`.
|
/// deserialization error occurs, then this will return `None`.
|
||||||
fn validate_project_default_branch(
|
fn validate_project_default_branch(
|
||||||
anchor: &Doc<Verified>,
|
anchor: &Doc,
|
||||||
sigrefs: &SignedRefs<Verified>,
|
sigrefs: &SignedRefs<Verified>,
|
||||||
) -> Option<Validation> {
|
) -> Option<Validation> {
|
||||||
let proj = anchor.project().ok()?;
|
let proj = anchor.project().ok()?;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ use localtime::{LocalDuration, LocalTime};
|
||||||
use log::*;
|
use log::*;
|
||||||
use nonempty::NonEmpty;
|
use nonempty::NonEmpty;
|
||||||
|
|
||||||
|
use radicle::identity::Doc;
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::address;
|
use radicle::node::address;
|
||||||
use radicle::node::address::Store as _;
|
use radicle::node::address::Store as _;
|
||||||
|
|
@ -36,8 +37,8 @@ use radicle::storage::refs::SIGREFS_BRANCH;
|
||||||
use radicle::storage::RepositoryError;
|
use radicle::storage::RepositoryError;
|
||||||
use radicle_fetch::policy::SeedingPolicy;
|
use radicle_fetch::policy::SeedingPolicy;
|
||||||
|
|
||||||
use crate::crypto::{Signer, Verified};
|
use crate::crypto::Signer;
|
||||||
use crate::identity::{Doc, RepoId};
|
use crate::identity::RepoId;
|
||||||
use crate::node::routing;
|
use crate::node::routing;
|
||||||
use crate::node::routing::InsertResult;
|
use crate::node::routing::InsertResult;
|
||||||
use crate::node::{
|
use crate::node::{
|
||||||
|
|
@ -678,7 +679,7 @@ where
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Add public repositories to inventory.
|
// Add public repositories to inventory.
|
||||||
if repo.doc.visibility.is_public() {
|
if repo.doc.is_public() {
|
||||||
inventory.insert(rid);
|
inventory.insert(rid);
|
||||||
} else {
|
} else {
|
||||||
private.insert(rid);
|
private.insert(rid);
|
||||||
|
|
@ -1168,7 +1169,7 @@ where
|
||||||
|
|
||||||
// Announce our new inventory if this fetch was a full clone.
|
// Announce our new inventory if this fetch was a full clone.
|
||||||
// Only update and announce inventory for public repositories.
|
// Only update and announce inventory for public repositories.
|
||||||
if clone && doc.visibility.is_public() {
|
if clone && doc.is_public() {
|
||||||
debug!(target: "service", "Updating and announcing inventory for cloned repository {rid}..");
|
debug!(target: "service", "Updating and announcing inventory for cloned repository {rid}..");
|
||||||
|
|
||||||
if let Err(e) = self.add_inventory(rid) {
|
if let Err(e) = self.add_inventory(rid) {
|
||||||
|
|
@ -2128,7 +2129,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Announce our own refs for the given repo.
|
/// Announce our own refs for the given repo.
|
||||||
fn announce_own_refs(&mut self, rid: RepoId, doc: Doc<Verified>) -> Result<Vec<RefsAt>, Error> {
|
fn announce_own_refs(&mut self, rid: RepoId, doc: Doc) -> Result<Vec<RefsAt>, Error> {
|
||||||
let (refs, timestamp) = self.announce_refs(rid, doc, [self.node_id()])?;
|
let (refs, timestamp) = self.announce_refs(rid, doc, [self.node_id()])?;
|
||||||
|
|
||||||
// Update refs database with our signed refs branches.
|
// Update refs database with our signed refs branches.
|
||||||
|
|
@ -2161,7 +2162,7 @@ where
|
||||||
fn announce_refs(
|
fn announce_refs(
|
||||||
&mut self,
|
&mut self,
|
||||||
rid: RepoId,
|
rid: RepoId,
|
||||||
doc: Doc<Verified>,
|
doc: Doc,
|
||||||
remotes: impl IntoIterator<Item = NodeId>,
|
remotes: impl IntoIterator<Item = NodeId>,
|
||||||
) -> Result<(Vec<RefsAt>, Timestamp), Error> {
|
) -> Result<(Vec<RefsAt>, Timestamp), Error> {
|
||||||
let (ann, refs) = self.refs_announcement_for(rid, remotes)?;
|
let (ann, refs) = self.refs_announcement_for(rid, remotes)?;
|
||||||
|
|
@ -2192,7 +2193,7 @@ where
|
||||||
ann,
|
ann,
|
||||||
peers.filter(|p| {
|
peers.filter(|p| {
|
||||||
// Only announce to peers who are allowed to view this repo.
|
// Only announce to peers who are allowed to view this repo.
|
||||||
doc.is_visible_to(&p.id)
|
doc.is_visible_to(&p.id.into())
|
||||||
}),
|
}),
|
||||||
self.db.gossip_mut(),
|
self.db.gossip_mut(),
|
||||||
);
|
);
|
||||||
|
|
@ -2341,7 +2342,7 @@ where
|
||||||
.get(rid)
|
.get(rid)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|doc| doc.is_visible_to(id))
|
.map(|doc| doc.is_visible_to(&(*id).into()))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
} else {
|
} else {
|
||||||
// Announcement doesn't concern a specific repository, let it through.
|
// Announcement doesn't concern a specific repository, let it through.
|
||||||
|
|
@ -2634,7 +2635,7 @@ pub trait ServiceState {
|
||||||
/// Get event emitter.
|
/// Get event emitter.
|
||||||
fn emitter(&self) -> &Emitter<Event>;
|
fn emitter(&self) -> &Emitter<Event>;
|
||||||
/// Get a repository from storage.
|
/// Get a repository from storage.
|
||||||
fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError>;
|
fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError>;
|
||||||
/// Get the clock.
|
/// Get the clock.
|
||||||
fn clock(&self) -> &LocalTime;
|
fn clock(&self) -> &LocalTime;
|
||||||
/// Get the clock mutably.
|
/// Get the clock mutably.
|
||||||
|
|
@ -2675,7 +2676,7 @@ where
|
||||||
&self.emitter
|
&self.emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
|
fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
|
||||||
self.storage.get(rid)
|
self.storage.get(rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2751,7 +2752,7 @@ impl fmt::Display for DisconnectReason {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Lookup {
|
pub struct Lookup {
|
||||||
/// Whether the project was found locally or not.
|
/// Whether the project was found locally or not.
|
||||||
pub local: Option<Doc<Verified>>,
|
pub local: Option<Doc>,
|
||||||
/// A list of remote peers on which the project is known to exist.
|
/// A list of remote peers on which the project is known to exist.
|
||||||
pub remote: Vec<NodeId>,
|
pub remote: Vec<NodeId>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -712,7 +712,13 @@ fn test_refs_announcement_relay_public() {
|
||||||
// Pretend Alice cloned Bob's repos.
|
// Pretend Alice cloned Bob's repos.
|
||||||
let repos = gen::<[MockRepository; 3]>(1);
|
let repos = gen::<[MockRepository; 3]>(1);
|
||||||
for (i, mut repo) in repos.into_iter().enumerate() {
|
for (i, mut repo) in repos.into_iter().enumerate() {
|
||||||
repo.doc.doc.visibility = Visibility::Public; // Public repos are always gossiped.
|
repo.doc.doc = repo
|
||||||
|
.doc
|
||||||
|
.doc
|
||||||
|
.with_edits(|doc| {
|
||||||
|
doc.visibility = Visibility::Public; // Public repos are always gossiped.
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
alice.storage_mut().repos.insert(bob_inv[i], repo);
|
alice.storage_mut().repos.insert(bob_inv[i], repo);
|
||||||
}
|
}
|
||||||
assert_matches!(
|
assert_matches!(
|
||||||
|
|
@ -784,14 +790,32 @@ fn test_refs_announcement_relay_private() {
|
||||||
alice.receive(eve.id(), Message::Subscribe(Subscribe::all()));
|
alice.receive(eve.id(), Message::Subscribe(Subscribe::all()));
|
||||||
|
|
||||||
// The first repo is not visible to Eve.
|
// The first repo is not visible to Eve.
|
||||||
let mut repo1 = gen::<MockRepository>(1);
|
let repo1 = {
|
||||||
repo1.doc.doc.visibility = Visibility::Private { allow: [].into() };
|
let mut repo = gen::<MockRepository>(1);
|
||||||
|
repo.doc.doc = repo
|
||||||
|
.doc
|
||||||
|
.doc
|
||||||
|
.with_edits(|doc| {
|
||||||
|
doc.visibility = Visibility::Private { allow: [].into() };
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
repo
|
||||||
|
};
|
||||||
alice.storage_mut().repos.insert(bob_inv[0], repo1);
|
alice.storage_mut().repos.insert(bob_inv[0], repo1);
|
||||||
|
|
||||||
// The second repo is visible to Eve.
|
// The second repo is visible to Eve.
|
||||||
let mut repo2 = gen::<MockRepository>(1);
|
let repo2 = {
|
||||||
repo2.doc.doc.visibility = Visibility::Private {
|
let mut repo = gen::<MockRepository>(1);
|
||||||
allow: [eve.id.into()].into(),
|
repo.doc.doc = repo
|
||||||
|
.doc
|
||||||
|
.doc
|
||||||
|
.with_edits(|doc| {
|
||||||
|
doc.visibility = Visibility::Private {
|
||||||
|
allow: [eve.id.into()].into(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
repo
|
||||||
};
|
};
|
||||||
alice.storage_mut().repos.insert(bob_inv[1], repo2);
|
alice.storage_mut().repos.insert(bob_inv[1], repo2);
|
||||||
alice.elapse(service::GOSSIP_INTERVAL);
|
alice.elapse(service::GOSSIP_INTERVAL);
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,8 @@ impl Worker {
|
||||||
}
|
}
|
||||||
let repo = self.storage.repository(rid)?;
|
let repo = self.storage.repository(rid)?;
|
||||||
let doc = repo.identity_doc()?;
|
let doc = repo.identity_doc()?;
|
||||||
if !doc.is_visible_to(&remote) {
|
|
||||||
|
if !doc.is_visible_to(&remote.into()) {
|
||||||
Err(UploadError::Unauthorized(remote, rid))
|
Err(UploadError::Unauthorized(remote, rid))
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -266,8 +266,12 @@ pub fn run(
|
||||||
if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
|
if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
|
||||||
let head = working.find_reference(src.as_str())?;
|
let head = working.find_reference(src.as_str())?;
|
||||||
let head = head.peel_to_commit()?.id();
|
let head = head.peel_to_commit()?.id();
|
||||||
let mut canonical =
|
|
||||||
Canonical::default_branch(stored, &project, &identity.delegates)?;
|
let mut canonical = Canonical::default_branch(
|
||||||
|
stored,
|
||||||
|
&project,
|
||||||
|
identity.delegates().as_ref(),
|
||||||
|
)?;
|
||||||
let converges = canonical::converges(
|
let converges = canonical::converges(
|
||||||
canonical
|
canonical
|
||||||
.tips()
|
.tips()
|
||||||
|
|
@ -279,7 +283,7 @@ pub fn run(
|
||||||
canonical.modify_vote(me, head.into());
|
canonical.modify_vote(me, head.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
match canonical.quorum(identity.threshold, &working) {
|
match canonical.quorum(identity.threshold(), &working) {
|
||||||
Ok(canonical_oid) => {
|
Ok(canonical_oid) => {
|
||||||
// Canonical head is an ancestor of head.
|
// Canonical head is an ancestor of head.
|
||||||
let is_ff = head == *canonical_oid
|
let is_ff = head == *canonical_oid
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,13 @@ use std::{fmt, ops::Deref, str::FromStr};
|
||||||
use crypto::{PublicKey, Signature};
|
use crypto::{PublicKey, Signature};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use radicle_cob::{Embed, ObjectId, TypeName};
|
use radicle_cob::{Embed, ObjectId, TypeName};
|
||||||
use radicle_crypto::{Signer, Verified};
|
use radicle_crypto::Signer;
|
||||||
use radicle_git_ext as git_ext;
|
use radicle_git_ext as git_ext;
|
||||||
use radicle_git_ext::Oid;
|
use radicle_git_ext::Oid;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::identity::doc::Doc;
|
||||||
use crate::{
|
use crate::{
|
||||||
cob,
|
cob,
|
||||||
cob::{
|
cob::{
|
||||||
|
|
@ -18,7 +19,7 @@ use crate::{
|
||||||
ActorId, Timestamp, Uri,
|
ActorId, Timestamp, Uri,
|
||||||
},
|
},
|
||||||
identity::{
|
identity::{
|
||||||
doc::{Doc, DocError, RepoId},
|
doc::{DocError, RepoId},
|
||||||
Did,
|
Did,
|
||||||
},
|
},
|
||||||
storage::{ReadRepository, RepositoryError, WriteRepository},
|
storage::{ReadRepository, RepositoryError, WriteRepository},
|
||||||
|
|
@ -171,7 +172,7 @@ impl Identity {
|
||||||
root,
|
root,
|
||||||
current: root,
|
current: root,
|
||||||
heads: revision
|
heads: revision
|
||||||
.delegates
|
.delegates()
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.map(|did| (did, root))
|
.map(|did| (did, root))
|
||||||
|
|
@ -182,7 +183,7 @@ impl Identity {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize<'a, R: WriteRepository + cob::Store, G: Signer>(
|
pub fn initialize<'a, R: WriteRepository + cob::Store, G: Signer>(
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc,
|
||||||
store: &'a R,
|
store: &'a R,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<IdentityMut<'a, R>, cob::store::Error> {
|
) -> Result<IdentityMut<'a, R>, cob::store::Error> {
|
||||||
|
|
@ -249,7 +250,7 @@ impl Identity {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The current document.
|
/// The current document.
|
||||||
pub fn doc(&self) -> &Doc<Verified> {
|
pub fn doc(&self) -> &Doc {
|
||||||
&self.current().doc
|
&self.current().doc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,7 +321,7 @@ impl store::Cob for Identity {
|
||||||
"the first operation must contain only one action",
|
"the first operation must contain only one action",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let root = Doc::<Verified>::load_at(op.id, repo)?;
|
let root = Doc::load_at(op.id, repo)?;
|
||||||
if root.blob != blob {
|
if root.blob != blob {
|
||||||
return Err(ApplyError::Init("invalid object id specified in revision"));
|
return Err(ApplyError::Init("invalid object id specified in revision"));
|
||||||
}
|
}
|
||||||
|
|
@ -331,7 +332,7 @@ impl store::Cob for Identity {
|
||||||
}
|
}
|
||||||
assert_eq!(root.commit, op.id);
|
assert_eq!(root.commit, op.id);
|
||||||
|
|
||||||
let founder = root.delegates.first();
|
let founder = root.delegates().first();
|
||||||
if founder.as_key() != &op.author {
|
if founder.as_key() != &op.author {
|
||||||
return Err(ApplyError::Init("delegate does not match committer"));
|
return Err(ApplyError::Init("delegate does not match committer"));
|
||||||
}
|
}
|
||||||
|
|
@ -411,7 +412,7 @@ impl Identity {
|
||||||
) -> Result<(), ApplyError> {
|
) -> Result<(), ApplyError> {
|
||||||
let current = self.current().clone();
|
let current = self.current().clone();
|
||||||
|
|
||||||
if !current.is_delegate(&author) {
|
if !current.is_delegate(&author.into()) {
|
||||||
return Err(ApplyError::UnexpectedState);
|
return Err(ApplyError::UnexpectedState);
|
||||||
}
|
}
|
||||||
match action {
|
match action {
|
||||||
|
|
@ -674,7 +675,7 @@ pub struct Revision {
|
||||||
/// Author of this proposed revision.
|
/// Author of this proposed revision.
|
||||||
pub author: Author,
|
pub author: Author,
|
||||||
/// New [`Doc`] that will replace `previous`' document.
|
/// New [`Doc`] that will replace `previous`' document.
|
||||||
pub doc: Doc<Verified>,
|
pub doc: Doc,
|
||||||
/// Physical timestamp of this proposal revision.
|
/// Physical timestamp of this proposal revision.
|
||||||
pub timestamp: Timestamp,
|
pub timestamp: Timestamp,
|
||||||
/// Parent revision.
|
/// Parent revision.
|
||||||
|
|
@ -685,7 +686,7 @@ pub struct Revision {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for Revision {
|
impl std::ops::Deref for Revision {
|
||||||
type Target = Doc<Verified>;
|
type Target = Doc;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.doc
|
&self.doc
|
||||||
|
|
@ -736,7 +737,7 @@ impl Revision {
|
||||||
description: String,
|
description: String,
|
||||||
author: Author,
|
author: Author,
|
||||||
blob: Oid,
|
blob: Oid,
|
||||||
doc: Doc<Verified>,
|
doc: Doc,
|
||||||
state: State,
|
state: State,
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
parent: Option<RevisionId>,
|
parent: Option<RevisionId>,
|
||||||
|
|
@ -788,7 +789,7 @@ impl Revision {
|
||||||
// Mark as rejected if it's impossible for this revision to be accepted
|
// Mark as rejected if it's impossible for this revision to be accepted
|
||||||
// with the current delegate set. Note that if the delegate set changes,
|
// with the current delegate set. Note that if the delegate set changes,
|
||||||
// this proposal will be marked as `stale` anyway.
|
// this proposal will be marked as `stale` anyway.
|
||||||
if self.is_active() && self.rejected().count() > self.delegates.len() - self.majority() {
|
if self.is_active() && self.rejected().count() > self.delegates().len() - self.majority() {
|
||||||
self.state = State::Rejected;
|
self.state = State::Rejected;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -834,7 +835,7 @@ impl<R: WriteRepository> store::Transaction<Identity, R> {
|
||||||
&mut self,
|
&mut self,
|
||||||
title: impl ToString,
|
title: impl ToString,
|
||||||
description: impl ToString,
|
description: impl ToString,
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc,
|
||||||
parent: Option<RevisionId>,
|
parent: Option<RevisionId>,
|
||||||
repo: &R,
|
repo: &R,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
|
|
@ -914,7 +915,7 @@ where
|
||||||
&mut self,
|
&mut self,
|
||||||
title: impl ToString,
|
title: impl ToString,
|
||||||
description: impl ToString,
|
description: impl ToString,
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<RevisionId, Error> {
|
) -> Result<RevisionId, Error> {
|
||||||
let parent = self.current;
|
let parent = self.current;
|
||||||
|
|
@ -1045,7 +1046,7 @@ mod test {
|
||||||
let bob = MockSigner::default();
|
let bob = MockSigner::default();
|
||||||
let signer = &node.signer;
|
let signer = &node.signer;
|
||||||
let mut identity = Identity::load_mut(&*repo).unwrap();
|
let mut identity = Identity::load_mut(&*repo).unwrap();
|
||||||
let mut doc = identity.doc().clone();
|
let mut doc = identity.doc().clone().edit();
|
||||||
let title = "Identity update";
|
let title = "Identity update";
|
||||||
let description = "";
|
let description = "";
|
||||||
let r0 = identity.current;
|
let r0 = identity.current;
|
||||||
|
|
@ -1054,33 +1055,38 @@ mod test {
|
||||||
assert!(identity.current().is_accepted());
|
assert!(identity.current().is_accepted());
|
||||||
// Using an identical document to the current one fails.
|
// Using an identical document to the current one fails.
|
||||||
identity
|
identity
|
||||||
.update(title, description, &doc, signer)
|
.update(title, description, &doc.clone().verified().unwrap(), signer)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert_eq!(identity.current, r0);
|
assert_eq!(identity.current, r0);
|
||||||
|
|
||||||
// Change threshold to `2`, even though there's only one delegate. This should
|
// Change threshold to `2`, even though there's only one delegate. This should
|
||||||
// fail as it makes the master branch immutable.
|
// fail as it makes the master branch immutable.
|
||||||
doc.threshold = 2;
|
doc.threshold = 2;
|
||||||
identity
|
assert!(doc.clone().verified().is_err());
|
||||||
.update(title, description, &doc, signer)
|
|
||||||
.unwrap_err();
|
|
||||||
assert_eq!(identity.current, r0);
|
|
||||||
// Let's add another delegate.
|
// Let's add another delegate.
|
||||||
doc.delegate(bob.public_key());
|
doc.delegate(bob.public_key().into());
|
||||||
// The update should go through now.
|
// The update should go through now.
|
||||||
let r1 = identity.update(title, description, &doc, signer).unwrap();
|
let r1 = identity
|
||||||
|
.update(title, description, &doc.clone().verified().unwrap(), signer)
|
||||||
|
.unwrap();
|
||||||
assert!(identity.revision(&r1).unwrap().is_accepted());
|
assert!(identity.revision(&r1).unwrap().is_accepted());
|
||||||
assert_eq!(identity.current, r1);
|
assert_eq!(identity.current, r1);
|
||||||
// With two delegates now, we need two signatures for any update to go through.
|
// With two delegates now, we need two signatures for any update to go through.
|
||||||
// So this next update shouldn't be accepted as canonical until the second delegate
|
// So this next update shouldn't be accepted as canonical until the second delegate
|
||||||
// signs it.
|
// signs it.
|
||||||
doc.visibility = Visibility::private([]);
|
doc.visibility = Visibility::private([]);
|
||||||
let r2 = identity.update(title, description, &doc, signer).unwrap();
|
let r2 = identity
|
||||||
|
.update(title, description, &doc.clone().verified().unwrap(), signer)
|
||||||
|
.unwrap();
|
||||||
// R1 is still the head.
|
// R1 is still the head.
|
||||||
assert_eq!(identity.current, r1);
|
assert_eq!(identity.current, r1);
|
||||||
assert_eq!(identity.revision(&r2).unwrap().state, State::Active);
|
assert_eq!(identity.revision(&r2).unwrap().state, State::Active);
|
||||||
assert_eq!(repo.canonical_identity_head().unwrap(), r1);
|
assert_eq!(repo.canonical_identity_head().unwrap(), r1);
|
||||||
assert_eq!(repo.identity_doc().unwrap().visibility, Visibility::Public);
|
assert_eq!(
|
||||||
|
repo.identity_doc().unwrap().visibility(),
|
||||||
|
&Visibility::Public
|
||||||
|
);
|
||||||
// Now let's add a signature on R2 from Bob.
|
// Now let's add a signature on R2 from Bob.
|
||||||
identity.accept(&r2, &bob).unwrap();
|
identity.accept(&r2, &bob).unwrap();
|
||||||
|
|
||||||
|
|
@ -1089,8 +1095,8 @@ mod test {
|
||||||
assert_eq!(identity.revision(&r2).unwrap().state, State::Accepted);
|
assert_eq!(identity.revision(&r2).unwrap().state, State::Accepted);
|
||||||
assert_eq!(repo.canonical_identity_head().unwrap(), r2);
|
assert_eq!(repo.canonical_identity_head().unwrap(), r2);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
repo.canonical_identity_doc().unwrap().visibility,
|
repo.canonical_identity_doc().unwrap().visibility(),
|
||||||
Visibility::private([])
|
&Visibility::private([])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1101,18 +1107,25 @@ mod test {
|
||||||
let eve = MockSigner::default();
|
let eve = MockSigner::default();
|
||||||
let signer = &node.signer;
|
let signer = &node.signer;
|
||||||
let mut identity = Identity::load_mut(&*repo).unwrap();
|
let mut identity = Identity::load_mut(&*repo).unwrap();
|
||||||
let mut doc = identity.doc().clone();
|
let mut doc = identity.doc().clone().edit();
|
||||||
let title = "Identity update";
|
let title = "Identity update";
|
||||||
let description = "";
|
let description = "";
|
||||||
|
|
||||||
// Let's add another delegate.
|
// Let's add another delegate.
|
||||||
doc.delegate(bob.public_key());
|
doc.delegate(bob.public_key().into());
|
||||||
let r1 = identity.update(title, description, &doc, signer).unwrap();
|
let r1 = identity
|
||||||
|
.update(title, description, &doc.clone().verified().unwrap(), signer)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(identity.current, r1);
|
assert_eq!(identity.current, r1);
|
||||||
|
|
||||||
doc.visibility = Visibility::private([]);
|
doc.visibility = Visibility::private([]);
|
||||||
let r2 = identity
|
let r2 = identity
|
||||||
.update("Make private", description, &doc, &node.signer)
|
.update(
|
||||||
|
"Make private",
|
||||||
|
description,
|
||||||
|
&doc.clone().verified().unwrap(),
|
||||||
|
&node.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// 1/2 rejected means that we can never reach the required 2/2 votes.
|
// 1/2 rejected means that we can never reach the required 2/2 votes.
|
||||||
|
|
@ -1121,16 +1134,26 @@ mod test {
|
||||||
assert_eq!(r2.state, State::Rejected);
|
assert_eq!(r2.state, State::Rejected);
|
||||||
|
|
||||||
// Now let's add another delegate.
|
// Now let's add another delegate.
|
||||||
doc.delegate(eve.public_key());
|
doc.delegate(eve.public_key().into());
|
||||||
let r3 = identity
|
let r3 = identity
|
||||||
.update("Add Eve", description, &doc, &node.signer)
|
.update(
|
||||||
|
"Add Eve",
|
||||||
|
description,
|
||||||
|
&doc.clone().verified().unwrap(),
|
||||||
|
&node.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let _ = identity.accept(&r3, &bob).unwrap();
|
let _ = identity.accept(&r3, &bob).unwrap();
|
||||||
assert_eq!(identity.current, r3);
|
assert_eq!(identity.current, r3);
|
||||||
|
|
||||||
doc.visibility = Visibility::Public;
|
doc.visibility = Visibility::Public;
|
||||||
let r3 = identity
|
let r3 = identity
|
||||||
.update("Make public", description, &doc, &node.signer)
|
.update(
|
||||||
|
"Make public",
|
||||||
|
description,
|
||||||
|
&doc.verified().unwrap(),
|
||||||
|
&node.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// 1/3 rejected means that we can still reach the 2/3 required votes.
|
// 1/3 rejected means that we can still reach the 2/3 required votes.
|
||||||
|
|
@ -1151,27 +1174,42 @@ mod test {
|
||||||
let bob = &network.bob;
|
let bob = &network.bob;
|
||||||
|
|
||||||
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
||||||
let mut alice_doc = alice_identity.doc().clone();
|
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||||
|
|
||||||
alice_doc.delegate(bob.signer.public_key());
|
alice_doc.delegate(bob.signer.public_key().into());
|
||||||
let a1 = alice_identity
|
let a1 = alice_identity
|
||||||
.update("Add Bob", "", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Add Bob",
|
||||||
|
"",
|
||||||
|
&alice_doc.clone().verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
bob.repo.fetch(alice);
|
bob.repo.fetch(alice);
|
||||||
|
|
||||||
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
|
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
|
||||||
let bob_doc = bob_identity.doc().clone();
|
let bob_doc = bob_identity.doc().clone();
|
||||||
assert!(bob_doc.is_delegate(bob.signer.public_key()));
|
assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
|
||||||
|
|
||||||
// Alice changes the document without making Bob aware.
|
// Alice changes the document without making Bob aware.
|
||||||
alice_doc.visibility = Visibility::private([]);
|
alice_doc.visibility = Visibility::private([]);
|
||||||
let a2 = alice_identity
|
let a2 = alice_identity
|
||||||
.update("Change visibility", "", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Change visibility",
|
||||||
|
"",
|
||||||
|
&alice_doc.clone().clone().verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Bob makes the same change without knowing Alice already did.
|
// Bob makes the same change without knowing Alice already did.
|
||||||
let b1 = bob_identity
|
let b1 = bob_identity
|
||||||
.update("Make private", "", &alice_doc, &bob.signer)
|
.update(
|
||||||
|
"Make private",
|
||||||
|
"",
|
||||||
|
&alice_doc.verified().unwrap(),
|
||||||
|
&bob.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Bob gets Alice's data.
|
// Bob gets Alice's data.
|
||||||
|
|
@ -1203,17 +1241,27 @@ mod test {
|
||||||
let eve = &network.eve;
|
let eve = &network.eve;
|
||||||
|
|
||||||
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
||||||
let mut alice_doc = alice_identity.doc().clone();
|
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||||
|
|
||||||
alice_doc.delegate(bob.signer.public_key());
|
alice_doc.delegate(bob.signer.public_key().into());
|
||||||
let a0 = alice_identity.root;
|
let a0 = alice_identity.root;
|
||||||
let a1 = alice_identity
|
let a1 = alice_identity
|
||||||
.update("Add Bob", "Eh.", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Add Bob",
|
||||||
|
"Eh.",
|
||||||
|
&alice_doc.clone().clone().verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
alice_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
|
alice_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
|
||||||
let a2 = alice_identity
|
let a2 = alice_identity
|
||||||
.update("Change visibility", "Eh.", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Change visibility",
|
||||||
|
"Eh.",
|
||||||
|
&alice_doc.verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
bob.repo.fetch(alice);
|
bob.repo.fetch(alice);
|
||||||
|
|
@ -1242,18 +1290,28 @@ mod test {
|
||||||
let eve = &network.eve;
|
let eve = &network.eve;
|
||||||
|
|
||||||
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
||||||
let mut alice_doc = alice_identity.doc().clone();
|
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||||
|
|
||||||
alice_doc.delegate(bob.signer.public_key());
|
alice_doc.delegate(bob.signer.public_key().into());
|
||||||
alice_doc.delegate(eve.signer.public_key());
|
alice_doc.delegate(eve.signer.public_key().into());
|
||||||
let a0 = alice_identity.root;
|
let a0 = alice_identity.root;
|
||||||
let a1 = alice_identity // Change description to change traversal order.
|
let a1 = alice_identity // Change description to change traversal order.
|
||||||
.update("Add Bob and Eve", "Eh#!", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Add Bob and Eve",
|
||||||
|
"Eh#!",
|
||||||
|
&alice_doc.clone().verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
alice_doc.rescind(eve.signer.public_key()).unwrap();
|
alice_doc.rescind(&eve.signer.public_key().into()).unwrap();
|
||||||
let a2 = alice_identity
|
let a2 = alice_identity
|
||||||
.update("Remove Eve", "", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Remove Eve",
|
||||||
|
"",
|
||||||
|
&alice_doc.verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
bob.repo.fetch(eve);
|
bob.repo.fetch(eve);
|
||||||
|
|
@ -1265,10 +1323,15 @@ mod test {
|
||||||
assert_eq!(bob_identity.current, a2);
|
assert_eq!(bob_identity.current, a2);
|
||||||
|
|
||||||
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
|
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
|
||||||
let mut eve_doc = eve_identity.doc().clone();
|
let mut eve_doc = eve_identity.doc().clone().edit();
|
||||||
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
|
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
|
||||||
let e1 = eve_identity
|
let e1 = eve_identity
|
||||||
.update("Change visibility", "", &eve_doc, &eve.signer)
|
.update(
|
||||||
|
"Change visibility",
|
||||||
|
"",
|
||||||
|
&eve_doc.verified().unwrap(),
|
||||||
|
&eve.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Eve's revision is active.
|
// Eve's revision is active.
|
||||||
assert!(eve_identity.revision(&e1).unwrap().is_active());
|
assert!(eve_identity.revision(&e1).unwrap().is_active());
|
||||||
|
|
@ -1288,7 +1351,7 @@ mod test {
|
||||||
// her revision is no longer valid.
|
// her revision is no longer valid.
|
||||||
assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1]);
|
assert_eq!(eve_identity.timeline, vec![a0, a1, a2, b1, e1]);
|
||||||
assert_eq!(eve_identity.revision(&e1), None);
|
assert_eq!(eve_identity.revision(&e1), None);
|
||||||
assert!(!eve_identity.is_delegate(eve.signer.public_key()));
|
assert!(!eve_identity.is_delegate(&eve.signer.public_key().into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1299,18 +1362,28 @@ mod test {
|
||||||
let eve = &network.eve;
|
let eve = &network.eve;
|
||||||
|
|
||||||
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
||||||
let mut alice_doc = alice_identity.doc().clone();
|
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||||
|
|
||||||
alice_doc.delegate(bob.signer.public_key());
|
alice_doc.delegate(bob.signer.public_key().into());
|
||||||
alice_doc.delegate(eve.signer.public_key());
|
alice_doc.delegate(eve.signer.public_key().into());
|
||||||
let a0 = alice_identity.root;
|
let a0 = alice_identity.root;
|
||||||
let a1 = alice_identity
|
let a1 = alice_identity
|
||||||
.update("Add Bob and Eve", "Eh!#", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Add Bob and Eve",
|
||||||
|
"Eh!#",
|
||||||
|
&alice_doc.clone().verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
alice_doc.visibility = Visibility::private([]);
|
alice_doc.visibility = Visibility::private([]);
|
||||||
let a2 = alice_identity
|
let a2 = alice_identity
|
||||||
.update("Change visibility", "", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Change visibility",
|
||||||
|
"",
|
||||||
|
&alice_doc.verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
bob.repo.fetch(eve);
|
bob.repo.fetch(eve);
|
||||||
|
|
@ -1327,10 +1400,15 @@ mod test {
|
||||||
assert!(eve_identity.revision(&a2).unwrap().is_active());
|
assert!(eve_identity.revision(&a2).unwrap().is_active());
|
||||||
|
|
||||||
// Then she submits a new revision.
|
// Then she submits a new revision.
|
||||||
let mut eve_doc = eve_identity.doc().clone();
|
let mut eve_doc = eve_identity.doc().clone().edit();
|
||||||
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
|
eve_doc.visibility = Visibility::private([eve.signer.public_key().into()]);
|
||||||
let e2 = eve_identity
|
let e2 = eve_identity
|
||||||
.update("Change visibility", "", &eve_doc, &eve.signer)
|
.update(
|
||||||
|
"Change visibility",
|
||||||
|
"",
|
||||||
|
&eve_doc.verified().unwrap(),
|
||||||
|
&eve.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(eve_identity.revision(&e2).unwrap().is_active());
|
assert!(eve_identity.revision(&e2).unwrap().is_active());
|
||||||
|
|
||||||
|
|
@ -1368,23 +1446,28 @@ mod test {
|
||||||
let eve = &network.eve;
|
let eve = &network.eve;
|
||||||
|
|
||||||
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
let mut alice_identity = Identity::load_mut(&*alice.repo).unwrap();
|
||||||
let mut alice_doc = alice_identity.doc().clone();
|
let mut alice_doc = alice_identity.doc().clone().edit();
|
||||||
|
|
||||||
alice.repo.fetch(bob);
|
alice.repo.fetch(bob);
|
||||||
alice.repo.fetch(eve);
|
alice.repo.fetch(eve);
|
||||||
alice_doc.delegate(bob.signer.public_key());
|
alice_doc.delegate(bob.signer.public_key().into());
|
||||||
alice_doc.delegate(eve.signer.public_key());
|
alice_doc.delegate(eve.signer.public_key().into());
|
||||||
let a0 = alice_identity.root;
|
let a0 = alice_identity.root;
|
||||||
let a1 = alice_identity
|
let a1 = alice_identity
|
||||||
.update("Add Bob and Eve", "", &alice_doc, &alice.signer)
|
.update(
|
||||||
|
"Add Bob and Eve",
|
||||||
|
"",
|
||||||
|
&alice_doc.verified().unwrap(),
|
||||||
|
&alice.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
bob.repo.fetch(alice);
|
bob.repo.fetch(alice);
|
||||||
eve.repo.fetch(alice);
|
eve.repo.fetch(alice);
|
||||||
|
|
||||||
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
|
let mut bob_identity = Identity::load_mut(&*bob.repo).unwrap();
|
||||||
let mut bob_doc = bob_identity.doc().clone();
|
let mut bob_doc = bob_identity.doc().clone().edit();
|
||||||
assert!(bob_doc.is_delegate(bob.signer.public_key()));
|
assert!(bob_doc.is_delegate(&bob.signer.public_key().into()));
|
||||||
|
|
||||||
// a2 e1
|
// a2 e1
|
||||||
// | /
|
// | /
|
||||||
|
|
@ -1397,17 +1480,27 @@ mod test {
|
||||||
// Bob and Alice change the document visibility. Eve is not aware.
|
// Bob and Alice change the document visibility. Eve is not aware.
|
||||||
bob_doc.visibility = Visibility::private([]);
|
bob_doc.visibility = Visibility::private([]);
|
||||||
let b1 = bob_identity
|
let b1 = bob_identity
|
||||||
.update("Change visibility #1", "", &bob_doc, &bob.signer)
|
.update(
|
||||||
|
"Change visibility #1",
|
||||||
|
"",
|
||||||
|
&bob_doc.verified().unwrap(),
|
||||||
|
&bob.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
alice.repo.fetch(bob);
|
alice.repo.fetch(bob);
|
||||||
eve.repo.fetch(bob);
|
eve.repo.fetch(bob);
|
||||||
|
|
||||||
// In the meantime, Eve does the same thing on her side.
|
// In the meantime, Eve does the same thing on her side.
|
||||||
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
|
let mut eve_identity = Identity::load_mut(&*eve.repo).unwrap();
|
||||||
let mut eve_doc = eve_identity.doc().clone();
|
let mut eve_doc = eve_identity.doc().clone().edit();
|
||||||
eve_doc.visibility = Visibility::private([]);
|
eve_doc.visibility = Visibility::private([]);
|
||||||
let e1 = eve_identity
|
let e1 = eve_identity
|
||||||
.update("Change visibility #2", "Woops", &eve_doc, &eve.signer)
|
.update(
|
||||||
|
"Change visibility #2",
|
||||||
|
"Woops",
|
||||||
|
&eve_doc.verified().unwrap(),
|
||||||
|
&eve.signer,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(eve_identity.revisions().count(), 4);
|
assert_eq!(eve_identity.revisions().count(), 4);
|
||||||
assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);
|
assert_eq!(eve_identity.revision(&e1).unwrap().state, State::Active);
|
||||||
|
|
@ -1441,27 +1534,37 @@ mod test {
|
||||||
|
|
||||||
let repo = storage.repository(id).unwrap();
|
let repo = storage.repository(id).unwrap();
|
||||||
let mut identity = Identity::load_mut(&repo).unwrap();
|
let mut identity = Identity::load_mut(&repo).unwrap();
|
||||||
let mut doc = identity.doc().clone();
|
let doc = identity.doc().clone();
|
||||||
let prj = doc.project().unwrap();
|
let prj = doc.project().unwrap();
|
||||||
|
let mut doc = doc.edit();
|
||||||
|
|
||||||
// Make a change to the description and sign it.
|
// Make a change to the description and sign it.
|
||||||
let desc = prj.description().to_owned() + "!";
|
let desc = prj.description().to_owned() + "!";
|
||||||
let prj = prj.update(None, desc, None).unwrap();
|
let prj = prj.update(None, desc, None).unwrap();
|
||||||
doc.payload.insert(PayloadId::project(), prj.clone().into());
|
doc.payload.insert(PayloadId::project(), prj.clone().into());
|
||||||
identity
|
identity
|
||||||
.update("Update description", "", &doc, &alice)
|
.update(
|
||||||
|
"Update description",
|
||||||
|
"",
|
||||||
|
&doc.clone().verified().unwrap(),
|
||||||
|
&alice,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Add Bob as a delegate, and sign it.
|
// Add Bob as a delegate, and sign it.
|
||||||
doc.delegate(bob.public_key());
|
doc.delegate(bob.public_key().into());
|
||||||
doc.threshold = 2;
|
doc.threshold = 2;
|
||||||
identity.update("Add bob", "", &doc, &alice).unwrap();
|
identity
|
||||||
|
.update("Add bob", "", &doc.clone().verified().unwrap(), &alice)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
// Add Eve as a delegate.
|
// Add Eve as a delegate.
|
||||||
doc.delegate(eve.public_key());
|
doc.delegate(eve.public_key().into());
|
||||||
|
|
||||||
// Update with both Bob and Alice's signature.
|
// Update with both Bob and Alice's signature.
|
||||||
let revision = identity.update("Add eve", "", &doc, &alice).unwrap();
|
let revision = identity
|
||||||
|
.update("Add eve", "", &doc.clone().verified().unwrap(), &alice)
|
||||||
|
.unwrap();
|
||||||
identity.accept(&revision, &bob).unwrap();
|
identity.accept(&revision, &bob).unwrap();
|
||||||
|
|
||||||
// Update description again with signatures by Eve and Bob.
|
// Update description again with signatures by Eve and Bob.
|
||||||
|
|
@ -1470,7 +1573,12 @@ mod test {
|
||||||
doc.payload.insert(PayloadId::project(), prj.into());
|
doc.payload.insert(PayloadId::project(), prj.into());
|
||||||
|
|
||||||
let revision = identity
|
let revision = identity
|
||||||
.update("Update description again", "Bob's repository", &doc, &bob)
|
.update(
|
||||||
|
"Update description again",
|
||||||
|
"Bob's repository",
|
||||||
|
&doc.verified().unwrap(),
|
||||||
|
&bob,
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
identity.accept(&revision, &eve).unwrap();
|
identity.accept(&revision, &eve).unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ use crate::cob::thread;
|
||||||
use crate::cob::thread::{Comment, CommentId, Thread};
|
use crate::cob::thread::{Comment, CommentId, Thread};
|
||||||
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName};
|
use crate::cob::{op, store, ActorId, Embed, EntryId, ObjectId, TypeName};
|
||||||
use crate::crypto::Signer;
|
use crate::crypto::Signer;
|
||||||
use crate::identity::doc::{Doc, DocError};
|
use crate::identity::doc::DocError;
|
||||||
use crate::prelude::{Did, ReadRepository, RepoId, Verified};
|
use crate::prelude::{Did, Doc, ReadRepository, RepoId};
|
||||||
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};
|
use crate::storage::{HasRepoId, RepositoryError, WriteRepository};
|
||||||
|
|
||||||
pub use cache::Cache;
|
pub use cache::Cache;
|
||||||
|
|
@ -325,9 +325,9 @@ impl Issue {
|
||||||
&self,
|
&self,
|
||||||
action: &Action,
|
action: &Action,
|
||||||
actor: &ActorId,
|
actor: &ActorId,
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc,
|
||||||
) -> Result<Authorization, Error> {
|
) -> Result<Authorization, Error> {
|
||||||
if doc.is_delegate(actor) {
|
if doc.is_delegate(&actor.into()) {
|
||||||
// A delegate is authorized to do all actions.
|
// A delegate is authorized to do all actions.
|
||||||
return Ok(Authorization::Allow);
|
return Ok(Authorization::Allow);
|
||||||
}
|
}
|
||||||
|
|
@ -388,7 +388,7 @@ impl Issue {
|
||||||
author: ActorId,
|
author: ActorId,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
_concurrent: &[&cob::Entry],
|
_concurrent: &[&cob::Entry],
|
||||||
_doc: &Doc<Verified>,
|
_doc: &Doc,
|
||||||
_repo: &R,
|
_repo: &R,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match action {
|
match action {
|
||||||
|
|
|
||||||
|
|
@ -630,9 +630,9 @@ impl Patch {
|
||||||
&self,
|
&self,
|
||||||
action: &Action,
|
action: &Action,
|
||||||
actor: &ActorId,
|
actor: &ActorId,
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc,
|
||||||
) -> Result<Authorization, Error> {
|
) -> Result<Authorization, Error> {
|
||||||
if doc.is_delegate(actor) {
|
if doc.is_delegate(&actor.into()) {
|
||||||
// A delegate is authorized to do all actions.
|
// A delegate is authorized to do all actions.
|
||||||
return Ok(Authorization::Allow);
|
return Ok(Authorization::Allow);
|
||||||
}
|
}
|
||||||
|
|
@ -745,7 +745,7 @@ impl Patch {
|
||||||
author: ActorId,
|
author: ActorId,
|
||||||
timestamp: Timestamp,
|
timestamp: Timestamp,
|
||||||
_concurrent: &[&cob::Entry],
|
_concurrent: &[&cob::Entry],
|
||||||
identity: &Doc<Verified>,
|
identity: &Doc,
|
||||||
repo: &R,
|
repo: &R,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match action {
|
match action {
|
||||||
|
|
@ -1046,7 +1046,7 @@ impl Patch {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// Discard revisions that weren't merged by a threshold of delegates.
|
// Discard revisions that weren't merged by a threshold of delegates.
|
||||||
merges.retain(|_, count| *count >= identity.threshold);
|
merges.retain(|_, count| *count >= identity.threshold());
|
||||||
|
|
||||||
match merges.into_keys().collect::<Vec<_>>().as_slice() {
|
match merges.into_keys().collect::<Vec<_>>().as_slice() {
|
||||||
[] => {
|
[] => {
|
||||||
|
|
@ -2970,9 +2970,9 @@ mod test {
|
||||||
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
let oid = git::Oid::from_str("518d5069f94c03427f694bb494ac1cd7d1339380").unwrap();
|
||||||
let mut alice = Actor::new(MockSigner::default());
|
let mut alice = Actor::new(MockSigner::default());
|
||||||
let rid = gen::<RepoId>(1);
|
let rid = gen::<RepoId>(1);
|
||||||
let doc = Doc::new(
|
let doc = RawDoc::new(
|
||||||
gen::<Project>(1),
|
gen::<Project>(1),
|
||||||
nonempty::NonEmpty::new(alice.did()),
|
vec![alice.did()],
|
||||||
1,
|
1,
|
||||||
identity::Visibility::Public,
|
identity::Visibility::Public,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ pub mod project;
|
||||||
|
|
||||||
pub use crypto::PublicKey;
|
pub use crypto::PublicKey;
|
||||||
pub use did::Did;
|
pub use did::Did;
|
||||||
pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RepoId, Visibility};
|
pub use doc::{Doc, DocAt, DocError, IdError, PayloadError, RawDoc, RepoId, Visibility};
|
||||||
pub use project::Project;
|
pub use project::Project;
|
||||||
|
|
||||||
pub use crate::cob::identity::{Error, Identity, IdentityMut};
|
pub use crate::cob::identity::{Error, Identity, IdentityMut};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ mod id;
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::marker::PhantomData;
|
use std::num::NonZeroUsize;
|
||||||
use std::ops::{Deref, Not};
|
use std::ops::{Deref, Not};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
@ -17,7 +17,7 @@ use thiserror::Error;
|
||||||
use crate::canonical::formatter::CanonicalFormatter;
|
use crate::canonical::formatter::CanonicalFormatter;
|
||||||
use crate::cob::identity;
|
use crate::cob::identity;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::crypto::{Signature, Unverified, Verified};
|
use crate::crypto::Signature;
|
||||||
use crate::git;
|
use crate::git;
|
||||||
use crate::identity::{project::Project, Did};
|
use crate::identity::{project::Project, Did};
|
||||||
use crate::storage;
|
use crate::storage;
|
||||||
|
|
@ -37,10 +37,10 @@ pub const MAX_DELEGATES: usize = 255;
|
||||||
pub enum DocError {
|
pub enum DocError {
|
||||||
#[error("json: {0}")]
|
#[error("json: {0}")]
|
||||||
Json(#[from] serde_json::Error),
|
Json(#[from] serde_json::Error),
|
||||||
#[error("invalid delegates: {0}")]
|
#[error(transparent)]
|
||||||
Delegates(&'static str),
|
Delegates(#[from] DelegatesError),
|
||||||
#[error("invalid threshold `{0}`: {1}")]
|
#[error(transparent)]
|
||||||
Threshold(usize, &'static str),
|
Threshold(#[from] ThresholdError),
|
||||||
#[error("git: {0}")]
|
#[error("git: {0}")]
|
||||||
GitExt(#[from] git::Error),
|
GitExt(#[from] git::Error),
|
||||||
#[error("git: {0}")]
|
#[error("git: {0}")]
|
||||||
|
|
@ -49,6 +49,14 @@ pub enum DocError {
|
||||||
Missing,
|
Missing,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("invalid delegates: {0}")]
|
||||||
|
pub struct DelegatesError(&'static str);
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("invalid threshold `{0}`: {1}")]
|
||||||
|
pub struct ThresholdError(usize, &'static str);
|
||||||
|
|
||||||
impl DocError {
|
impl DocError {
|
||||||
/// Whether this error is caused by the document not being found.
|
/// Whether this error is caused by the document not being found.
|
||||||
pub fn is_not_found(&self) -> bool {
|
pub fn is_not_found(&self) -> bool {
|
||||||
|
|
@ -99,7 +107,9 @@ pub enum PayloadError {
|
||||||
NotFound(PayloadId),
|
NotFound(PayloadId),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Payload value.
|
/// A `Payload` is a free-form JSON value that can be associated with an
|
||||||
|
/// identity's [`Doc`].
|
||||||
|
/// The payload is identified in the [`Doc`] by its corresponding [`PayloadId`].
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(transparent)]
|
#[serde(transparent)]
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
|
|
@ -137,25 +147,25 @@ pub struct DocAt {
|
||||||
/// The document blob at this commit.
|
/// The document blob at this commit.
|
||||||
pub blob: Oid,
|
pub blob: Oid,
|
||||||
/// The parsed document.
|
/// The parsed document.
|
||||||
pub doc: Doc<Verified>,
|
pub doc: Doc,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for DocAt {
|
impl Deref for DocAt {
|
||||||
type Target = Doc<Verified>;
|
type Target = Doc;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.doc
|
&self.doc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DocAt> for Doc<Verified> {
|
impl From<DocAt> for Doc {
|
||||||
fn from(value: DocAt) -> Self {
|
fn from(value: DocAt) -> Self {
|
||||||
value.doc
|
value.doc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<Doc<Verified>> for DocAt {
|
impl AsRef<Doc> for DocAt {
|
||||||
fn as_ref(&self) -> &Doc<Verified> {
|
fn as_ref(&self) -> &Doc {
|
||||||
&self.doc
|
&self.doc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -209,106 +219,55 @@ impl Visibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An identity document.
|
/// `RawDoc` is similar to the [`Doc`] type, however, it can be edited and may
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
/// not be valid.
|
||||||
|
///
|
||||||
|
/// It is expected that any changes to a [`Doc`] are made via [`RawDoc`], and
|
||||||
|
/// then verified by using [`RawDoc::verified`].
|
||||||
|
///
|
||||||
|
/// Note that `RawDoc` only implements [`Deserialize`]. This prevents us from
|
||||||
|
/// serializing an unverified document, while also making sure that any document
|
||||||
|
/// that is deserialized is verified.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Doc<V> {
|
pub struct RawDoc {
|
||||||
/// The payload section.
|
/// The payload section.
|
||||||
pub payload: BTreeMap<PayloadId, Payload>,
|
pub payload: BTreeMap<PayloadId, Payload>,
|
||||||
/// The delegates section.
|
/// The delegates section.
|
||||||
pub delegates: NonEmpty<Did>,
|
pub delegates: Vec<Did>,
|
||||||
/// The signature threshold.
|
/// The signature threshold.
|
||||||
pub threshold: usize,
|
pub threshold: usize,
|
||||||
/// Repository visibility.
|
/// Repository visibility.
|
||||||
#[serde(default, skip_serializing_if = "Visibility::is_public")]
|
#[serde(default)]
|
||||||
pub visibility: Visibility,
|
pub visibility: Visibility,
|
||||||
|
|
||||||
#[serde(skip)]
|
|
||||||
verified: PhantomData<V>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> Doc<V> {
|
impl TryFrom<RawDoc> for Doc {
|
||||||
/// Check whether this document and the associated repository is visible to the given peer.
|
type Error = DocError;
|
||||||
pub fn is_visible_to(&self, peer: &PublicKey) -> bool {
|
|
||||||
match &self.visibility {
|
|
||||||
Visibility::Public => true,
|
|
||||||
Visibility::Private { allow } => {
|
|
||||||
allow.contains(&Did::from(*peer)) || self.is_delegate(peer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate signature using this document's delegates, against a given document blob.
|
fn try_from(doc: RawDoc) -> Result<Self, Self::Error> {
|
||||||
pub fn verify_signature(
|
doc.verified()
|
||||||
&self,
|
|
||||||
key: &PublicKey,
|
|
||||||
signature: &Signature,
|
|
||||||
blob: Oid,
|
|
||||||
) -> Result<(), PublicKey> {
|
|
||||||
if !self.is_delegate(key) {
|
|
||||||
return Err(*key);
|
|
||||||
}
|
|
||||||
if key.verify(blob.as_bytes(), signature).is_err() {
|
|
||||||
return Err(*key);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_majority(&self, votes: usize) -> bool {
|
|
||||||
votes >= self.majority()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn majority(&self) -> usize {
|
|
||||||
self.delegates.len() / 2 + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blob_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
|
|
||||||
let path = Path::new("embeds").join(*PATH);
|
|
||||||
repo.blob_at(commit, path.as_path()).map_err(DocError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_delegate(&self, key: &crypto::PublicKey) -> bool {
|
|
||||||
self.delegates.contains(&key.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Doc<Verified> {
|
impl RawDoc {
|
||||||
pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
|
/// Construct a new [`RawDoc`] with an initial [`RawDoc::payload`]
|
||||||
let mut buf = Vec::new();
|
/// containing the provided [`Project`], and the given `delegates`,
|
||||||
let mut serializer =
|
/// `threshold`, and `visibility`.
|
||||||
serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());
|
pub fn new(
|
||||||
|
project: Project,
|
||||||
|
delegates: Vec<Did>,
|
||||||
|
threshold: usize,
|
||||||
|
visibility: Visibility,
|
||||||
|
) -> Self {
|
||||||
|
let project =
|
||||||
|
serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
|
||||||
|
|
||||||
self.serialize(&mut serializer)?;
|
Self {
|
||||||
let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
|
payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
|
||||||
|
delegates,
|
||||||
Ok((oid.into(), buf))
|
threshold,
|
||||||
}
|
visibility,
|
||||||
|
|
||||||
/// Attempt to add a new delegate to the document. Returns `true` if it wasn't there before.
|
|
||||||
pub fn delegate(&mut self, key: &crypto::PublicKey) -> bool {
|
|
||||||
let delegate = Did::from(key);
|
|
||||||
if self.delegates.iter().all(|id| id != &delegate) {
|
|
||||||
self.delegates.push(delegate);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rescind(&mut self, key: &crypto::PublicKey) -> Result<Option<Did>, DocError> {
|
|
||||||
let delegate = Did::from(key);
|
|
||||||
let (matches, delegates) = self.delegates.iter().partition(|d| **d == delegate);
|
|
||||||
match NonEmpty::from_vec(delegates) {
|
|
||||||
Some(delegates) => {
|
|
||||||
self.delegates = delegates;
|
|
||||||
if self.threshold > self.delegates.len() {
|
|
||||||
return Err(DocError::Threshold(
|
|
||||||
self.threshold,
|
|
||||||
"the thresholds exceeds the new delegate count after removal",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(matches.is_empty().not().then_some(delegate))
|
|
||||||
}
|
|
||||||
None => Err(DocError::Delegates("cannot remove the last delegate")),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,6 +282,389 @@ impl Doc<Verified> {
|
||||||
Ok(proj)
|
Ok(proj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if the given `did` is in the set of [`RawDoc::delegates`].
|
||||||
|
pub fn is_delegate(&self, did: &Did) -> bool {
|
||||||
|
self.delegates.contains(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new delegate to the document.
|
||||||
|
///
|
||||||
|
/// Note that if this `Did` is a duplicate, then the resulting set will only
|
||||||
|
/// show it once.
|
||||||
|
pub fn delegate(&mut self, did: Did) {
|
||||||
|
self.delegates.push(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the `did` from the set of delegates. Returns `true` if it was
|
||||||
|
/// removed.
|
||||||
|
pub fn rescind(&mut self, did: &Did) -> Result<bool, DocError> {
|
||||||
|
let (matches, delegates) = self.delegates.iter().partition(|d| *d == did);
|
||||||
|
self.delegates = delegates;
|
||||||
|
Ok(matches.is_empty().not())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct the `RawDoc` from the set of `bytes` that are expected to be
|
||||||
|
/// in JSON format.
|
||||||
|
pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
|
||||||
|
serde_json::from_slice(bytes).map_err(DocError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the `RawDoc`'s values, converting it into a valid [`Doc`].
|
||||||
|
///
|
||||||
|
/// The verifications are as follows:
|
||||||
|
///
|
||||||
|
/// - [`RawDoc::delegates`]: any duplicates are removed, and for the
|
||||||
|
/// remaining set ensure that it is non-empty and does not exceed a
|
||||||
|
/// length of [`MAX_DELEGATES`].
|
||||||
|
/// - [`RawDoc::threshold`]: ensure that it is in the range `[1, delegates.len()]`.
|
||||||
|
pub fn verified(self) -> Result<Doc, DocError> {
|
||||||
|
let RawDoc {
|
||||||
|
payload,
|
||||||
|
delegates,
|
||||||
|
threshold,
|
||||||
|
visibility,
|
||||||
|
} = self;
|
||||||
|
let delegates = Delegates::new(delegates)?;
|
||||||
|
let threshold = Threshold::new(threshold, &delegates)?;
|
||||||
|
Ok(Doc {
|
||||||
|
payload,
|
||||||
|
delegates,
|
||||||
|
threshold,
|
||||||
|
visibility,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A valid set of delegates for the identity [`Doc`].
|
||||||
|
///
|
||||||
|
/// It can only be constructed via [`Delegates::new`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(try_from = "Vec<Did>")]
|
||||||
|
pub struct Delegates(NonEmpty<Did>);
|
||||||
|
|
||||||
|
impl AsRef<NonEmpty<Did>> for Delegates {
|
||||||
|
fn as_ref(&self) -> &NonEmpty<Did> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Vec<Did>> for Delegates {
|
||||||
|
type Error = DelegatesError;
|
||||||
|
|
||||||
|
fn try_from(dids: Vec<Did>) -> Result<Self, Self::Error> {
|
||||||
|
Delegates::new(dids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Delegates {
|
||||||
|
type Item = <NonEmpty<Did> as IntoIterator>::Item;
|
||||||
|
type IntoIter = <NonEmpty<Did> as IntoIterator>::IntoIter;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Delegates {
|
||||||
|
/// Construct the set of `Delegates` by removing any duplicate [`Did`]s,
|
||||||
|
/// ensure that the set is non-empty, and check the length does not exceed
|
||||||
|
/// the [`MAX_DELEGATES`].
|
||||||
|
pub fn new(delegates: impl IntoIterator<Item = Did>) -> Result<Self, DelegatesError> {
|
||||||
|
let delegates = delegates
|
||||||
|
.into_iter()
|
||||||
|
.try_fold(Vec::<Did>::new(), |mut dids, did| {
|
||||||
|
if !dids.contains(&did) {
|
||||||
|
if dids.len() >= MAX_DELEGATES {
|
||||||
|
return Err(DelegatesError("number of delegates cannot exceed 255"));
|
||||||
|
}
|
||||||
|
dids.push(did);
|
||||||
|
}
|
||||||
|
Ok(dids)
|
||||||
|
})?;
|
||||||
|
NonEmpty::from_vec(delegates)
|
||||||
|
.map(Self)
|
||||||
|
.ok_or(DelegatesError("delegate list cannot be empty"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first delegate in the set.
|
||||||
|
pub fn first(&self) -> &Did {
|
||||||
|
self.0.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain an iterator over the [`Did`]s.
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &Did> {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the set contains the given `did`.
|
||||||
|
pub fn contains(&self, did: &Did) -> bool {
|
||||||
|
self.0.contains(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of delegates in the set.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the set is empty. Note that this always returns `false`.
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Delegates> for &'a NonEmpty<Did> {
|
||||||
|
fn from(ds: &'a Delegates) -> Self {
|
||||||
|
&ds.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Delegates> for NonEmpty<Did> {
|
||||||
|
fn from(ds: Delegates) -> Self {
|
||||||
|
ds.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Delegates> for Vec<Did> {
|
||||||
|
fn from(Delegates(ds): Delegates) -> Self {
|
||||||
|
ds.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A valid threshold for the identity [`Doc`].
|
||||||
|
///
|
||||||
|
/// It can only be constructed via [`Threshold::new`] or [`Threshold::MIN`].
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct Threshold(NonZeroUsize);
|
||||||
|
|
||||||
|
impl From<Threshold> for usize {
|
||||||
|
fn from(Threshold(t): Threshold) -> Self {
|
||||||
|
t.get()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<NonZeroUsize> for Threshold {
|
||||||
|
fn as_ref(&self) -> &NonZeroUsize {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Threshold {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Threshold {
|
||||||
|
/// A threshold of `1`.
|
||||||
|
pub const MIN: Threshold = Threshold(NonZeroUsize::MIN);
|
||||||
|
|
||||||
|
/// Construct the `Threshold` by checking that `t` is not greater than
|
||||||
|
/// [`MAX_DELEGATES`], that it does not exceed the number of delegates, and
|
||||||
|
/// is non-zero.
|
||||||
|
pub fn new(t: usize, delegates: &Delegates) -> Result<Self, ThresholdError> {
|
||||||
|
if t > MAX_DELEGATES {
|
||||||
|
Err(ThresholdError(t, "threshold cannot exceed 255"))
|
||||||
|
} else if t > delegates.len() {
|
||||||
|
Err(ThresholdError(
|
||||||
|
t,
|
||||||
|
"threshold cannot exceed number of delegates",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
NonZeroUsize::new(t)
|
||||||
|
.map(Self)
|
||||||
|
.ok_or(ThresholdError(t, "threshold cannot be zero"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Doc` is a valid identity document.
|
||||||
|
///
|
||||||
|
/// To ensure that only valid documents are used, this type is restricted to be
|
||||||
|
/// read-only. For mutating the document use [`Doc::edit`].
|
||||||
|
///
|
||||||
|
/// A valid `Doc` can be constructed in four ways:
|
||||||
|
///
|
||||||
|
/// 1. [`Doc::initial`]: a safe way to construct the initial document for an identity.
|
||||||
|
/// 2. [`RawDoc::verified`]: validates a [`RawDoc`]'s fields and converts it
|
||||||
|
/// into a `Doc`
|
||||||
|
/// 3. [`Deserialize`]: will deserialize a `Doc` by first deserializing a
|
||||||
|
/// [`RawDoc`] and use [`RawDoc::verified`] to construct the `Doc`.
|
||||||
|
/// 4. [`Doc::from_blob`]: construct a `Doc` from a Git blob by deserializing
|
||||||
|
/// its contents.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
#[serde(try_from = "RawDoc")]
|
||||||
|
pub struct Doc {
|
||||||
|
payload: BTreeMap<PayloadId, Payload>,
|
||||||
|
delegates: Delegates,
|
||||||
|
threshold: Threshold,
|
||||||
|
#[serde(default, skip_serializing_if = "Visibility::is_public")]
|
||||||
|
visibility: Visibility,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Doc {
|
||||||
|
/// Construct the initial [`Doc`] for an identity.
|
||||||
|
///
|
||||||
|
/// It will begin with the provided `project` in the [`Doc::payload`], the
|
||||||
|
/// `delegate` as the sole delegate, a threshold of 1, and the given
|
||||||
|
/// `visibility`.
|
||||||
|
pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
|
||||||
|
let project =
|
||||||
|
serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
|
||||||
|
delegates: Delegates(NonEmpty::new(delegate)),
|
||||||
|
threshold: Threshold(NonZeroUsize::MIN),
|
||||||
|
visibility,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a [`Doc`] contained in the provided Git blob.
|
||||||
|
pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
|
||||||
|
RawDoc::from_json(blob.content())?.verified()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert the [`Doc`] into a [`RawDoc`] for changing the field values and
|
||||||
|
/// re-verifying.
|
||||||
|
pub fn edit(self) -> RawDoc {
|
||||||
|
let Doc {
|
||||||
|
payload,
|
||||||
|
delegates,
|
||||||
|
threshold,
|
||||||
|
visibility,
|
||||||
|
} = self;
|
||||||
|
RawDoc {
|
||||||
|
payload,
|
||||||
|
delegates: delegates.into(),
|
||||||
|
threshold: threshold.into(),
|
||||||
|
visibility,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Using the current state of the `Doc`, perform any edits on the `RawDoc`
|
||||||
|
/// form and verify the changes.
|
||||||
|
pub fn with_edits<F>(self, f: F) -> Result<Self, DocError>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut RawDoc),
|
||||||
|
{
|
||||||
|
let mut raw = self.edit();
|
||||||
|
f(&mut raw);
|
||||||
|
raw.verified()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the associated payloads for this [`Doc`].
|
||||||
|
pub fn payload(&self) -> &BTreeMap<PayloadId, Payload> {
|
||||||
|
&self.payload
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the project payload, if it exists and is valid, out of this document.
|
||||||
|
pub fn project(&self) -> Result<Project, PayloadError> {
|
||||||
|
let value = self
|
||||||
|
.payload
|
||||||
|
.get(&PayloadId::project())
|
||||||
|
.ok_or_else(|| PayloadError::NotFound(PayloadId::project()))?;
|
||||||
|
let proj: Project = serde_json::from_value((**value).clone())?;
|
||||||
|
|
||||||
|
Ok(proj)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the associated [`Visibility`] of this document.
|
||||||
|
pub fn visibility(&self) -> &Visibility {
|
||||||
|
&self.visibility
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the visibility of the document is public.
|
||||||
|
pub fn is_public(&self) -> bool {
|
||||||
|
self.visibility.is_public()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether the visibility of the document is private.
|
||||||
|
pub fn is_private(&self) -> bool {
|
||||||
|
self.visibility.is_private()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the associated threshold of this document.
|
||||||
|
pub fn threshold(&self) -> usize {
|
||||||
|
self.threshold.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the associated threshold of this document in its non-zero format.
|
||||||
|
pub fn threshold_nonzero(&self) -> &NonZeroUsize {
|
||||||
|
&self.threshold.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the associated delegates of this document.
|
||||||
|
pub fn delegates(&self) -> &Delegates {
|
||||||
|
&self.delegates
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the `did` is part of the [`Doc::delegates`] set.
|
||||||
|
pub fn is_delegate(&self, did: &Did) -> bool {
|
||||||
|
self.delegates.contains(did)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether this document and the associated repository is visible to
|
||||||
|
/// the given peer.
|
||||||
|
pub fn is_visible_to(&self, did: &Did) -> bool {
|
||||||
|
match &self.visibility {
|
||||||
|
Visibility::Public => true,
|
||||||
|
Visibility::Private { allow } => allow.contains(did) || self.is_delegate(did),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate `signature` using this document's delegates, against a given
|
||||||
|
/// document blob.
|
||||||
|
pub fn verify_signature(
|
||||||
|
&self,
|
||||||
|
key: &PublicKey,
|
||||||
|
signature: &Signature,
|
||||||
|
blob: Oid,
|
||||||
|
) -> Result<(), PublicKey> {
|
||||||
|
if !self.is_delegate(&key.into()) {
|
||||||
|
return Err(*key);
|
||||||
|
}
|
||||||
|
if key.verify(blob.as_bytes(), signature).is_err() {
|
||||||
|
return Err(*key);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the provided `votes` passes the [`Doc::majority`].
|
||||||
|
pub fn is_majority(&self, votes: usize) -> bool {
|
||||||
|
votes >= self.majority()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the majority number based on the size of the delegates set.
|
||||||
|
pub fn majority(&self) -> usize {
|
||||||
|
self.delegates.len() / 2 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for getting an `embeds` Git blob.
|
||||||
|
pub(crate) fn blob_at<R: ReadRepository>(
|
||||||
|
commit: Oid,
|
||||||
|
repo: &R,
|
||||||
|
) -> Result<git2::Blob, DocError> {
|
||||||
|
let path = Path::new("embeds").join(*PATH);
|
||||||
|
repo.blob_at(commit, path.as_path()).map_err(DocError::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the [`Doc`] as canonical JSON, returning the set of bytes and its
|
||||||
|
/// corresponding Git [`Oid`].
|
||||||
|
pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), DocError> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut serializer =
|
||||||
|
serde_json::Serializer::with_formatter(&mut buf, CanonicalFormatter::new());
|
||||||
|
|
||||||
|
self.serialize(&mut serializer)?;
|
||||||
|
let oid = git2::Oid::hash_object(git2::ObjectType::Blob, &buf)?;
|
||||||
|
|
||||||
|
Ok((oid.into(), buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`Doc::encode`] and sign the [`Doc`], returning the set of bytes, its
|
||||||
|
/// corresponding Git [`Oid`] and the [`Signature`] over the [`Oid`].
|
||||||
pub fn sign<G: crypto::Signer>(
|
pub fn sign<G: crypto::Signer>(
|
||||||
&self,
|
&self,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
|
|
@ -333,15 +675,18 @@ impl Doc<Verified> {
|
||||||
Ok((oid, bytes, sig))
|
Ok((oid, bytes, sig))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Similar to [`Doc::sign`], but only returning the [`Signature`].
|
||||||
pub fn signature_of<G: crypto::Signer>(&self, signer: &G) -> Result<Signature, DocError> {
|
pub fn signature_of<G: crypto::Signer>(&self, signer: &G) -> Result<Signature, DocError> {
|
||||||
let (_, _, sig) = self.sign(signer)?;
|
let (_, _, sig) = self.sign(signer)?;
|
||||||
|
|
||||||
Ok(sig)
|
Ok(sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load the [`DocAt`] found at the given `commit`. The [`DocAt`] will
|
||||||
|
/// contain the corresponding [`Doc`].
|
||||||
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
|
pub fn load_at<R: ReadRepository>(commit: Oid, repo: &R) -> Result<DocAt, DocError> {
|
||||||
let blob = Self::blob_at(commit, repo)?;
|
let blob = Self::blob_at(commit, repo)?;
|
||||||
let doc = Doc::from_blob(&blob)?;
|
let doc = Self::from_blob(&blob)?;
|
||||||
|
|
||||||
Ok(DocAt {
|
Ok(DocAt {
|
||||||
commit,
|
commit,
|
||||||
|
|
@ -350,10 +695,8 @@ impl Doc<Verified> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_blob(blob: &git2::Blob) -> Result<Self, DocError> {
|
/// Initialize an [`identity::Identity`] with this [`Doc`] as the associated
|
||||||
Doc::from_json(blob.content())?.verified()
|
/// document.
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init<G: crypto::Signer>(
|
pub fn init<G: crypto::Signer>(
|
||||||
&self,
|
&self,
|
||||||
repo: &storage::git::Repository,
|
repo: &storage::git::Repository,
|
||||||
|
|
@ -378,79 +721,54 @@ impl Doc<Verified> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Doc<Unverified> {
|
|
||||||
pub fn initial(project: Project, delegate: Did, visibility: Visibility) -> Self {
|
|
||||||
Self::new(project, NonEmpty::new(delegate), 1, visibility)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
project: Project,
|
|
||||||
delegates: NonEmpty<Did>,
|
|
||||||
threshold: usize,
|
|
||||||
visibility: Visibility,
|
|
||||||
) -> Self {
|
|
||||||
let project =
|
|
||||||
serde_json::to_value(project).expect("Doc::initial: payload must be serializable");
|
|
||||||
|
|
||||||
Self {
|
|
||||||
payload: BTreeMap::from_iter([(PayloadId::project(), Payload::from(project))]),
|
|
||||||
delegates,
|
|
||||||
threshold,
|
|
||||||
visibility,
|
|
||||||
verified: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_json(bytes: &[u8]) -> Result<Self, DocError> {
|
|
||||||
serde_json::from_slice(bytes).map_err(DocError::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verified(self) -> Result<Doc<Verified>, DocError> {
|
|
||||||
if self.delegates.len() > MAX_DELEGATES {
|
|
||||||
return Err(DocError::Delegates("number of delegates cannot exceed 255"));
|
|
||||||
}
|
|
||||||
if self.delegates.is_empty() {
|
|
||||||
return Err(DocError::Delegates("delegate list cannot be empty"));
|
|
||||||
}
|
|
||||||
if self.threshold > self.delegates.len() {
|
|
||||||
return Err(DocError::Threshold(
|
|
||||||
self.threshold,
|
|
||||||
"threshold cannot exceed number of delegates",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if self.threshold == 0 {
|
|
||||||
return Err(DocError::Threshold(
|
|
||||||
self.threshold,
|
|
||||||
"threshold cannot be zero",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Doc {
|
|
||||||
payload: self.payload,
|
|
||||||
delegates: self.delegates,
|
|
||||||
threshold: self.threshold,
|
|
||||||
visibility: self.visibility,
|
|
||||||
verified: PhantomData,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod test {
|
mod test {
|
||||||
use radicle_crypto::test::signer::MockSigner;
|
use radicle_crypto::test::signer::MockSigner;
|
||||||
use radicle_crypto::Signer as _;
|
use radicle_crypto::Signer as _;
|
||||||
|
|
||||||
|
use crate::assert_matches;
|
||||||
use crate::rad;
|
use crate::rad;
|
||||||
use crate::storage::git::transport;
|
use crate::storage::git::transport;
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
|
use crate::storage::{ReadStorage as _, RemoteId, WriteStorage as _};
|
||||||
use crate::test::arbitrary;
|
use crate::test::arbitrary;
|
||||||
|
use crate::test::arbitrary::gen;
|
||||||
use crate::test::fixtures;
|
use crate::test::fixtures;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use qcheck_macros::quickcheck;
|
use qcheck_macros::quickcheck;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_duplicate_dids() {
|
||||||
|
let delegate = MockSigner::from_seed([0xff; 32]);
|
||||||
|
let did = Did::from(delegate.public_key());
|
||||||
|
let mut doc = RawDoc::new(gen::<Project>(1), vec![did], 1, Visibility::Public);
|
||||||
|
doc.delegate(did);
|
||||||
|
let doc = doc.verified().unwrap();
|
||||||
|
assert!(doc.delegates().len() == 1, "Duplicate DID was not removed");
|
||||||
|
assert!(doc.delegates().first() == &did)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_max_delegates() {
|
||||||
|
// Generate more than the max delegates
|
||||||
|
let delegates = (0..MAX_DELEGATES + 1).map(gen).collect::<Vec<Did>>();
|
||||||
|
|
||||||
|
// A document with max delegates will be fine
|
||||||
|
let doc = RawDoc::new(
|
||||||
|
gen::<Project>(1),
|
||||||
|
delegates[0..MAX_DELEGATES].into(),
|
||||||
|
1,
|
||||||
|
Visibility::Public,
|
||||||
|
);
|
||||||
|
assert_matches!(doc.verified(), Ok(_));
|
||||||
|
|
||||||
|
// A document that exceeds max delegates should fail
|
||||||
|
let doc = RawDoc::new(gen::<Project>(1), delegates, 1, Visibility::Public);
|
||||||
|
assert_matches!(doc.verified(), Err(DocError::Delegates(DelegatesError(_))));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_canonical_example() {
|
fn test_canonical_example() {
|
||||||
let tempdir = tempfile::tempdir().unwrap();
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -494,7 +812,7 @@ mod test {
|
||||||
let err = repo.identity_head_of(&remote).unwrap_err();
|
let err = repo.identity_head_of(&remote).unwrap_err();
|
||||||
matches!(err, git::ext::Error::NotFound(_));
|
matches!(err, git::ext::Error::NotFound(_));
|
||||||
|
|
||||||
let err = Doc::<Verified>::load_at(oid.into(), &repo).unwrap_err();
|
let err = Doc::load_at(oid.into(), &repo).unwrap_err();
|
||||||
assert!(err.is_not_found());
|
assert!(err.is_not_found());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -523,9 +841,9 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[quickcheck]
|
#[quickcheck]
|
||||||
fn prop_encode_decode(doc: Doc<Verified>) {
|
fn prop_encode_decode(doc: Doc) {
|
||||||
let (_, bytes) = doc.encode().unwrap();
|
let (_, bytes) = doc.encode().unwrap();
|
||||||
assert_eq!(Doc::from_json(&bytes).unwrap().verified().unwrap(), doc);
|
assert_eq!(RawDoc::from_json(&bytes).unwrap().verified().unwrap(), doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ pub mod prelude {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub use crypto::{PublicKey, Signer, Verified};
|
pub use crypto::{PublicKey, Signer, Verified};
|
||||||
pub use identity::{project::Project, Did, Doc, RepoId};
|
pub use identity::{project::Project, Did, Doc, RawDoc, RepoId};
|
||||||
pub use node::{Alias, NodeId, Timestamp};
|
pub use node::{Alias, NodeId, Timestamp};
|
||||||
pub use profile::Profile;
|
pub use profile::Profile;
|
||||||
pub use storage::{
|
pub use storage::{
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ pub fn init<G: Signer, S: WriteStorage>(
|
||||||
visibility: Visibility,
|
visibility: Visibility,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
storage: S,
|
storage: S,
|
||||||
) -> Result<(RepoId, identity::Doc<Verified>, SignedRefs<Verified>), InitError> {
|
) -> Result<(RepoId, identity::Doc, SignedRefs<Verified>), InitError> {
|
||||||
// TODO: Better error when project id already exists in storage, but remote doesn't.
|
// TODO: Better error when project id already exists in storage, but remote doesn't.
|
||||||
let delegate: identity::Did = signer.public_key().into();
|
let delegate: identity::Did = signer.public_key().into();
|
||||||
let proj = Project::new(
|
let proj = Project::new(
|
||||||
|
|
@ -68,7 +68,7 @@ pub fn init<G: Signer, S: WriteStorage>(
|
||||||
.join(", "),
|
.join(", "),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let doc = identity::Doc::initial(proj, delegate, visibility).verified()?;
|
let doc = identity::Doc::initial(proj, delegate, visibility);
|
||||||
let (project, identity) = Repository::init(&doc, &storage, signer)?;
|
let (project, identity) = Repository::init(&doc, &storage, signer)?;
|
||||||
let url = git::Url::from(project.id);
|
let url = git::Url::from(project.id);
|
||||||
|
|
||||||
|
|
@ -452,7 +452,7 @@ mod tests {
|
||||||
assert_eq!(project.name(), "acme");
|
assert_eq!(project.name(), "acme");
|
||||||
assert_eq!(project.description(), "Acme's repo");
|
assert_eq!(project.description(), "Acme's repo");
|
||||||
assert_eq!(project.default_branch(), &git::refname!("master"));
|
assert_eq!(project.default_branch(), &git::refname!("master"));
|
||||||
assert_eq!(doc.delegates.first(), &Did::from(public_key));
|
assert_eq!(doc.delegates().first(), &Did::from(public_key));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,13 @@ pub type BranchName = git::RefString;
|
||||||
|
|
||||||
/// Basic repository information.
|
/// Basic repository information.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct RepositoryInfo<V> {
|
pub struct RepositoryInfo {
|
||||||
/// Repository identifier.
|
/// Repository identifier.
|
||||||
pub rid: RepoId,
|
pub rid: RepoId,
|
||||||
/// Head of default branch.
|
/// Head of default branch.
|
||||||
pub head: Oid,
|
pub head: Oid,
|
||||||
/// Identity document.
|
/// Identity document.
|
||||||
pub doc: Doc<V>,
|
pub doc: Doc,
|
||||||
/// Local signed refs, if any.
|
/// Local signed refs, if any.
|
||||||
/// Repositories with this set to `None` are ones that are seeded but not forked.
|
/// Repositories with this set to `None` are ones that are seeded but not forked.
|
||||||
pub refs: Option<refs::SignedRefsAt>,
|
pub refs: Option<refs::SignedRefsAt>,
|
||||||
|
|
@ -412,11 +412,11 @@ pub trait ReadStorage {
|
||||||
/// Check whether storage contains a repository.
|
/// Check whether storage contains a repository.
|
||||||
fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError>;
|
fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError>;
|
||||||
/// Return all repositories (public and private).
|
/// Return all repositories (public and private).
|
||||||
fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error>;
|
fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error>;
|
||||||
/// Open or create a read-only repository.
|
/// Open or create a read-only repository.
|
||||||
fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError>;
|
fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError>;
|
||||||
/// Get a repository's identity if it exists.
|
/// Get a repository's identity if it exists.
|
||||||
fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
|
fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
|
||||||
match self.repository(rid) {
|
match self.repository(rid) {
|
||||||
Ok(repo) => Ok(Some(repo.identity_doc()?.into())),
|
Ok(repo) => Ok(Some(repo.identity_doc()?.into())),
|
||||||
Err(e) if e.is_not_found() => Ok(None),
|
Err(e) if e.is_not_found() => Ok(None),
|
||||||
|
|
@ -569,9 +569,9 @@ pub trait ReadRepository: Sized + ValidateRepository {
|
||||||
|
|
||||||
/// Get repository delegates.
|
/// Get repository delegates.
|
||||||
fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
|
fn delegates(&self) -> Result<NonEmpty<Did>, RepositoryError> {
|
||||||
let doc: Doc<_> = self.identity_doc()?.into();
|
let doc = self.identity_doc()?;
|
||||||
|
|
||||||
Ok(doc.delegates)
|
Ok(doc.delegates().clone().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the repository's identity document.
|
/// Get the repository's identity document.
|
||||||
|
|
@ -683,7 +683,7 @@ where
|
||||||
self.deref().contains(rid)
|
self.deref().contains(rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, rid: RepoId) -> Result<Option<Doc<Verified>>, RepositoryError> {
|
fn get(&self, rid: RepoId) -> Result<Option<Doc>, RepositoryError> {
|
||||||
self.deref().get(rid)
|
self.deref().get(rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -691,7 +691,7 @@ where
|
||||||
self.deref().repository(rid)
|
self.deref().repository(rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
|
fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
|
||||||
self.deref().repositories()
|
self.deref().repositories()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,9 @@ use crypto::{Signer, Verified};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::crypto::Unverified;
|
|
||||||
use crate::git::canonical::Canonical;
|
use crate::git::canonical::Canonical;
|
||||||
use crate::identity::doc::DocError;
|
use crate::identity::doc::DocError;
|
||||||
use crate::identity::{doc::DocAt, Doc, RepoId};
|
use crate::identity::{Doc, DocAt, RepoId};
|
||||||
use crate::identity::{Identity, Project};
|
use crate::identity::{Identity, Project};
|
||||||
use crate::node::SyncedAt;
|
use crate::node::SyncedAt;
|
||||||
use crate::storage::refs;
|
use crate::storage::refs;
|
||||||
|
|
@ -106,7 +105,7 @@ impl ReadStorage for Storage {
|
||||||
Repository::open(paths::repository(self, &rid), rid)
|
Repository::open(paths::repository(self, &rid), rid)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
|
fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
|
||||||
let mut repos = Vec::new();
|
let mut repos = Vec::new();
|
||||||
|
|
||||||
for result in fs::read_dir(&self.path)? {
|
for result in fs::read_dir(&self.path)? {
|
||||||
|
|
@ -238,7 +237,7 @@ impl Storage {
|
||||||
pub fn repositories_by_id<'a>(
|
pub fn repositories_by_id<'a>(
|
||||||
&self,
|
&self,
|
||||||
mut rids: impl Iterator<Item = &'a RepoId>,
|
mut rids: impl Iterator<Item = &'a RepoId>,
|
||||||
) -> Result<Vec<RepositoryInfo<Verified>>, RepositoryError> {
|
) -> Result<Vec<RepositoryInfo>, RepositoryError> {
|
||||||
rids.try_fold(Vec::new(), |mut infos, rid| {
|
rids.try_fold(Vec::new(), |mut infos, rid| {
|
||||||
let repo = self.repository(*rid)?;
|
let repo = self.repository(*rid)?;
|
||||||
let (_, head) = repo.head()?;
|
let (_, head) = repo.head()?;
|
||||||
|
|
@ -391,7 +390,7 @@ impl Repository {
|
||||||
let delegates = self
|
let delegates = self
|
||||||
.delegates()?
|
.delegates()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(RemoteId::from)
|
.map(|did| *did)
|
||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
let mut deleted = Vec::new();
|
let mut deleted = Vec::new();
|
||||||
for id in self.remote_ids()? {
|
for id in self.remote_ids()? {
|
||||||
|
|
@ -435,7 +434,7 @@ impl Repository {
|
||||||
|
|
||||||
/// Create the repository's identity branch.
|
/// Create the repository's identity branch.
|
||||||
pub fn init<G: Signer, S: WriteStorage>(
|
pub fn init<G: Signer, S: WriteStorage>(
|
||||||
doc: &Doc<Verified>,
|
doc: &Doc,
|
||||||
storage: &S,
|
storage: &S,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<(Self, git::Oid), RepositoryError> {
|
) -> Result<(Self, git::Oid), RepositoryError> {
|
||||||
|
|
@ -491,7 +490,7 @@ impl Repository {
|
||||||
Ok(proj)
|
Ok(proj)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc<Verified>, DocError> {
|
pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc, DocError> {
|
||||||
let oid = self.identity_head_of(remote)?;
|
let oid = self.identity_head_of(remote)?;
|
||||||
Doc::load_at(oid, self).map(|d| d.into())
|
Doc::load_at(oid, self).map(|d| d.into())
|
||||||
}
|
}
|
||||||
|
|
@ -730,7 +729,7 @@ impl ReadRepository for Repository {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError> {
|
fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError> {
|
||||||
Doc::<Verified>::load_at(head, self)
|
Doc::load_at(head, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn head(&self) -> Result<(Qualified, Oid), RepositoryError> {
|
fn head(&self) -> Result<(Qualified, Oid), RepositoryError> {
|
||||||
|
|
@ -748,8 +747,8 @@ impl ReadRepository for Repository {
|
||||||
let project = doc.project()?;
|
let project = doc.project()?;
|
||||||
let branch_ref = git::refs::branch(project.default_branch());
|
let branch_ref = git::refs::branch(project.default_branch());
|
||||||
let raw = self.raw();
|
let raw = self.raw();
|
||||||
let oid = Canonical::default_branch(self, &project, &doc.delegates)?
|
let oid = Canonical::default_branch(self, &project, doc.delegates().into())?
|
||||||
.quorum(doc.threshold, raw)?;
|
.quorum(doc.threshold(), raw)?;
|
||||||
Ok((branch_ref, oid))
|
Ok((branch_ref, oid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -794,7 +793,7 @@ impl ReadRepository for Repository {
|
||||||
let Ok(root) = self.identity_root_of(&remote) else {
|
let Ok(root) = self.identity_root_of(&remote) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let blob = Doc::<Unverified>::blob_at(root, self)?;
|
let blob = Doc::blob_at(root, self)?;
|
||||||
|
|
||||||
// We've got an identity that goes back to the correct root.
|
// We've got an identity that goes back to the correct root.
|
||||||
if blob.id() == **self.id {
|
if blob.id() == **self.id {
|
||||||
|
|
|
||||||
|
|
@ -498,7 +498,7 @@ mod tests {
|
||||||
|
|
||||||
// Alice creates "paris" repo.
|
// Alice creates "paris" repo.
|
||||||
let (paris_repo, paris_head) = fixtures::repository(tmp.path().join("paris"));
|
let (paris_repo, paris_head) = fixtures::repository(tmp.path().join("paris"));
|
||||||
let (paris_rid, mut paris_doc, _) = rad::init(
|
let (paris_rid, paris_doc, _) = rad::init(
|
||||||
&paris_repo,
|
&paris_repo,
|
||||||
"paris".try_into().unwrap(),
|
"paris".try_into().unwrap(),
|
||||||
"Paris repository",
|
"Paris repository",
|
||||||
|
|
@ -511,7 +511,7 @@ mod tests {
|
||||||
|
|
||||||
// Alice creates "london" repo.
|
// Alice creates "london" repo.
|
||||||
let (london_repo, _london_head) = fixtures::repository(tmp.path().join("london"));
|
let (london_repo, _london_head) = fixtures::repository(tmp.path().join("london"));
|
||||||
let (london_rid, mut london_doc, _) = rad::init(
|
let (london_rid, london_doc, _) = rad::init(
|
||||||
&london_repo,
|
&london_repo,
|
||||||
"london".try_into().unwrap(),
|
"london".try_into().unwrap(),
|
||||||
"London repository",
|
"London repository",
|
||||||
|
|
@ -532,8 +532,16 @@ mod tests {
|
||||||
|
|
||||||
// Bob is added to both repos as a delegate, by Alice.
|
// Bob is added to both repos as a delegate, by Alice.
|
||||||
{
|
{
|
||||||
paris_doc.delegates.push(bob.public_key().into());
|
let paris_doc = paris_doc
|
||||||
london_doc.delegates.push(bob.public_key().into());
|
.with_edits(|doc| {
|
||||||
|
doc.delegates.push(bob.public_key().into());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let london_doc = london_doc
|
||||||
|
.with_edits(|doc| {
|
||||||
|
doc.delegates.push(bob.public_key().into());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut paris_ident = Identity::load_mut(&paris).unwrap();
|
let mut paris_ident = Identity::load_mut(&paris).unwrap();
|
||||||
let mut london_ident = Identity::load_mut(&london).unwrap();
|
let mut london_ident = Identity::load_mut(&london).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,16 @@ use std::str::FromStr;
|
||||||
use std::{iter, net};
|
use std::{iter, net};
|
||||||
|
|
||||||
use crypto::test::signer::MockSigner;
|
use crypto::test::signer::MockSigner;
|
||||||
use crypto::{PublicKey, Unverified, Verified};
|
use crypto::{PublicKey, Unverified};
|
||||||
use cyphernet::addr::tor::OnionAddrV3;
|
use cyphernet::addr::tor::OnionAddrV3;
|
||||||
use cyphernet::EcPk;
|
use cyphernet::EcPk;
|
||||||
use nonempty::NonEmpty;
|
|
||||||
use qcheck::Arbitrary;
|
use qcheck::Arbitrary;
|
||||||
|
|
||||||
use crate::collections::RandomMap;
|
use crate::collections::RandomMap;
|
||||||
use crate::identity::doc::Visibility;
|
use crate::identity::doc::Visibility;
|
||||||
use crate::identity::project::ProjectName;
|
use crate::identity::project::ProjectName;
|
||||||
use crate::identity::{
|
use crate::identity::{
|
||||||
doc::{Doc, DocAt, RepoId},
|
doc::{Doc, DocAt, RawDoc, RepoId},
|
||||||
project::Project,
|
project::Project,
|
||||||
Did,
|
Did,
|
||||||
};
|
};
|
||||||
|
|
@ -139,28 +138,26 @@ impl Arbitrary for Visibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Arbitrary for Doc<Unverified> {
|
impl Arbitrary for RawDoc {
|
||||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
let proj = Project::arbitrary(g);
|
let proj = Project::arbitrary(g);
|
||||||
let delegate = Did::arbitrary(g);
|
let delegate = Did::arbitrary(g);
|
||||||
let visibility = Visibility::arbitrary(g);
|
let visibility = Visibility::arbitrary(g);
|
||||||
|
|
||||||
Self::initial(proj, delegate, visibility)
|
Self::new(proj, vec![delegate], 1, visibility)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Arbitrary for Doc<Verified> {
|
impl Arbitrary for Doc {
|
||||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
let mut rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
let mut rng = fastrand::Rng::with_seed(u64::arbitrary(g));
|
||||||
let project = Project::arbitrary(g);
|
let project = Project::arbitrary(g);
|
||||||
let delegates: NonEmpty<_> = iter::repeat_with(|| Did::arbitrary(g))
|
let delegates = iter::repeat_with(|| Did::arbitrary(g))
|
||||||
.take(rng.usize(1..6))
|
.take(rng.usize(1..6))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>();
|
||||||
.try_into()
|
|
||||||
.unwrap();
|
|
||||||
let threshold = delegates.len() / 2 + 1;
|
let threshold = delegates.len() / 2 + 1;
|
||||||
let visibility = Visibility::arbitrary(g);
|
let visibility = Visibility::arbitrary(g);
|
||||||
let doc: Doc<Unverified> = Doc::new(project, delegates, threshold, visibility);
|
let doc = RawDoc::new(project, delegates, threshold, visibility);
|
||||||
|
|
||||||
doc.verified().unwrap()
|
doc.verified().unwrap()
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +165,7 @@ impl Arbitrary for Doc<Verified> {
|
||||||
|
|
||||||
impl Arbitrary for DocAt {
|
impl Arbitrary for DocAt {
|
||||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
let doc = Doc::<Verified>::arbitrary(g);
|
let doc = Doc::arbitrary(g);
|
||||||
|
|
||||||
DocAt {
|
DocAt {
|
||||||
commit: self::oid(),
|
commit: self::oid(),
|
||||||
|
|
@ -238,7 +235,7 @@ impl Arbitrary for MockStorage {
|
||||||
impl Arbitrary for MockRepository {
|
impl Arbitrary for MockRepository {
|
||||||
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
fn arbitrary(g: &mut qcheck::Gen) -> Self {
|
||||||
let rid = RepoId::arbitrary(g);
|
let rid = RepoId::arbitrary(g);
|
||||||
let doc = Doc::<Verified>::arbitrary(g);
|
let doc = Doc::arbitrary(g);
|
||||||
|
|
||||||
Self::new(rid, doc)
|
Self::new(rid, doc)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::str::FromStr;
|
||||||
use git_ext::ref_format as fmt;
|
use git_ext::ref_format as fmt;
|
||||||
|
|
||||||
use crate::crypto::{Signer, Verified};
|
use crate::crypto::{Signer, Verified};
|
||||||
use crate::identity::doc::{Doc, DocAt, DocError, RepoId};
|
use crate::identity::doc::{Doc, DocAt, DocError, RawDoc, RepoId};
|
||||||
use crate::node::NodeId;
|
use crate::node::NodeId;
|
||||||
|
|
||||||
pub use crate::storage::*;
|
pub use crate::storage::*;
|
||||||
|
|
@ -51,9 +51,9 @@ impl MockStorage {
|
||||||
.expect("MockStorage::repo_mut: repository does not exist")
|
.expect("MockStorage::repo_mut: repository does not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map(mut self, f: impl Fn(&mut Doc<Verified>)) -> Self {
|
pub fn map(mut self, f: impl Fn(&mut RawDoc)) -> Self {
|
||||||
for repo in self.repos.values_mut() {
|
for repo in self.repos.values_mut() {
|
||||||
f(&mut repo.doc.doc);
|
repo.doc.doc = repo.doc.doc.clone().with_edits(|doc| f(doc)).unwrap();
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +91,7 @@ impl ReadStorage for MockStorage {
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repositories(&self) -> Result<Vec<RepositoryInfo<Verified>>, Error> {
|
fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.repos
|
.repos
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -135,7 +135,7 @@ pub struct MockRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockRepository {
|
impl MockRepository {
|
||||||
pub fn new(id: RepoId, doc: Doc<Verified>) -> Self {
|
pub fn new(id: RepoId, doc: Doc) -> Self {
|
||||||
let (blob, _) = doc.encode().unwrap();
|
let (blob, _) = doc.encode().unwrap();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue