use std::{
cell::RefCell,
collections::{hash_map::Entry, HashMap, HashSet},
pin::Pin,
rc::Rc,
str::FromStr,
};
use crew_rs::{
AddRole, CredentialSource, CredentialSourceHash, CrewChange, CrewID, MakeStatement, Member,
MemberCredential, RevealSecret, SimpleCredentialSource, ADMIN_ROLE, CONTENT_SECRET,
PARTICIPATION_SECRET,
};
use endr::{Remote, StorageBackend};
use futures::{future, Future, FutureExt, Stream, StreamExt};
use jmbl::{Input, JMBLViewRef, Value};
use litl::Val;
use mofo::Mofo;
use ridl::{
asymm_encr::RecipientID,
signing::{SignatureError, Signed},
};
use tracing::{debug, error};
use crate::{
conventions::{
named_doc_path, CREDENTIALS_DOC_NAME, INCLUDE_LOG_STR, INTRODUCTION_PREFIX,
NAMED_DOC_PREFIX, PROFILE_DOC_NAME,
},
doc_content::jmbl_content::JMBLContent,
team::{Team, TeamID},
Doc, DocID, Tlpt,
};
#[derive(Clone)]
pub struct AccountCredentialSource {
account_id: TeamID,
account_credential: MemberCredential,
credentials_doc: Doc,
cached: Rc<RefCell<HashMap<Val, MemberCredential>>>,
}
impl CredentialSource for AccountCredentialSource {
fn add_credential<'a>(
&'a self,
crew_id: CrewID,
credential: MemberCredential,
) -> std::pin::Pin<Box<dyn futures::Future<Output = ()> + 'a>> {
if crew_id == self.account_id.0 {
panic!("Can't add credential for account document");
}
let credentials_doc = self.credentials_doc.clone();
async move {
let credentials_doc_view = credentials_doc.content().expect_jmbl().start_writing(false);
credentials_doc_view
.get_root()
.if_map_mut()
.expect("Expected credentials doc to be a map")
.insert(
crew_id.to_string(),
Value::Plain(litl::to_val(&credential).unwrap()),
);
credentials_doc
.content()
.expect_jmbl()
.finish_writing(credentials_doc_view);
credentials_doc
.content()
.expect_jmbl()
.updates("Awaiting new credential added".to_string())
.filter(|(root, _)| {
future::ready(
root.if_map()
.unwrap()
.get(&crew_id.to_string())
.if_plain()
.unwrap()
== &litl::to_val(&credential).unwrap(),
)
})
.next()
.await;
}
.boxed_local()
}
fn current_credentials_for<'a>(&'a self, crew_id: &'a CrewID) -> Vec<MemberCredential> {
if crew_id == &self.account_id.0 {
return vec![self.account_credential.clone()];
}
if crew_id == &self.credentials_doc.doc_crew().id() {
vec![]
} else {
let root = self.credentials_doc.content().expect_jmbl().current_root();
let val = root
.if_map()
.expect("Expected root to be map")
.get(&crew_id.to_string())
.if_plain()
.expect("Expected credential to be plain val")
.clone();
if val.is_null() {
return vec![MemberCredential::literally_everyone()];
}
match self.cached.borrow_mut().entry(val.clone()) {
Entry::Occupied(entry) => vec![entry.get().clone()],
Entry::Vacant(entry) => {
match litl::from_val::<MemberCredential>(val.clone()) {
Ok(credential) => {
entry.insert(credential.clone());
vec![credential]
}
Err(err) => {
error!("Error deserializing credential: {:?}", err);
vec![MemberCredential::literally_everyone()]
}
}
}
}
}
}
fn id_hash(&self) -> CredentialSourceHash {
CredentialSourceHash::from_hashable(self.account_id)
}
fn clone_ref(&self) -> Box<dyn CredentialSource> {
Box::new(self.clone())
}
fn current_state(&self) -> std::collections::HashMap<CrewID, Vec<MemberCredential>> {
let mut map = HashMap::new();
map.insert(
self.account_id.0.clone(),
vec![self.account_credential.clone()],
);
let root = self.credentials_doc.content().expect_jmbl().current_root();
let jmbl_map = root.if_map().expect("Expected root to be map");
for (crew_id, val) in jmbl_map.entries() {
let credential = match litl::from_val(val) {
Ok(credential) => credential,
Err(err) => {
error!("Error deserializing credential: {:?}", err);
continue;
}
};
map.entry(CrewID::from_str(&crew_id).unwrap())
.or_insert_with(Vec::new)
.push(credential);
}
map
}
}
impl Tlpt {
pub async fn new_with_account<S: StorageBackend + 'static>(
name: String,
background: Mofo,
account_id: TeamID,
account_credential: MemberCredential,
storage: S,
remotes: impl Stream<Item = Remote> + Unpin + 'static,
) -> Self {
let root_credential_source = Box::new(SimpleCredentialSource::new());
root_credential_source
.add_credential(account_id.0.clone(), account_credential.clone())
.await;
let endr = endr::Node::new(background.clone(), storage.clone_ref());
let endr_for_remotes = endr.clone();
background.clone().add_background_task(
remotes
.for_each(move |remote| {
let endr_for_remotes = endr_for_remotes.clone();
async move { endr_for_remotes.add_remote(remote).await }
})
.boxed_local(),
);
let root_tlpt = Tlpt::new(
format!("{}_root", name),
background.clone(),
endr.clone_ref(),
root_credential_source,
None,
);
let account_team = root_tlpt.load_team(&account_id).await;
let account_team_for_credentials_doc = account_team.clone();
let credentials_doc_id = account_team_for_credentials_doc
.wait_for_named_doc(CREDENTIALS_DOC_NAME)
.await;
let credentials_doc = root_tlpt.load_doc(credentials_doc_id).await;
let account_credential_source = AccountCredentialSource {
account_id,
account_credential,
credentials_doc,
cached: Rc::new(RefCell::new(HashMap::new())),
};
let tlpt = Tlpt::new(
format!("{}_credential_source", name),
background.clone(),
endr,
Box::new(account_credential_source),
Some(account_id),
);
tlpt.load_team(&account_team.id())
.await
.wait_for_named_doc(PROFILE_DOC_NAME)
.await;
tlpt
}
pub async fn new_with_create_account<
C: FnOnce(JMBLViewRef) -> Value + 'static,
S: StorageBackend + 'static,
>(
name: String,
background: Mofo,
profile_constructor: C,
storage: S,
remotes: impl Stream<Item = Remote> + Unpin + 'static,
) -> (Tlpt, TeamID, MemberCredential) {
let endr = endr::Node::new(background.clone(), storage.clone_ref());
let endr_for_remotes = endr.clone();
background.clone().add_background_task(
remotes
.for_each(move |remote| {
let endr_for_remotes = endr_for_remotes.clone();
async move { endr_for_remotes.add_remote(remote).await }
})
.boxed_local(),
);
let root_credential_source = Box::new(SimpleCredentialSource::new());
let root_tlpt = Tlpt::new(
format!("{}_root", name),
background.clone(),
endr.clone_ref(),
root_credential_source,
None,
);
let (account_team, account_credential) = root_tlpt.create_team_without_intro(None).await;
let credentials_doc = root_tlpt
.create_document_without_intro(
&account_team,
JMBLContent::create(|mut view| view.create_map::<Value, &str, _>([])),
)
.await;
account_team
.crew
.make_changes([CrewChange::MakeStatement(MakeStatement {
path: named_doc_path(CREDENTIALS_DOC_NAME),
value: litl::to_val(credentials_doc.id()).unwrap(),
})])
.await
.unwrap();
let account_credential_source = Box::new(AccountCredentialSource {
account_id: account_team.id(),
account_credential: account_credential.clone(),
credentials_doc,
cached: Rc::new(RefCell::new(HashMap::new())),
});
let tlpt_for_profile = Tlpt::new(
format!("{}_create_profile", name),
background.clone(),
endr.clone_ref(),
account_credential_source.clone_ref(),
None,
);
let profile = tlpt_for_profile
.create_profile(&account_team.id(), profile_constructor)
.await;
let tlpt = Tlpt::new(
name,
background,
endr,
account_credential_source,
Some(account_team.id()),
);
tlpt.load_team(&account_team.id())
.await
.wait_for_named_doc(PROFILE_DOC_NAME)
.await;
(tlpt, account_team.id(), account_credential)
}
pub async fn create_profile<C: FnOnce(JMBLViewRef) -> Value + 'static>(
&self,
in_account: &TeamID,
constructor: C,
) -> Doc {
let (profile_team, profile_credential) = self.create_team_without_intro(None).await;
let profile_doc = self
.create_document_without_intro(&profile_team, JMBLContent::create(constructor))
.await;
let profile_team_content_secret =
profile_team.crew.get_shared_secret(CONTENT_SECRET).unwrap();
profile_team
.crew
.make_changes([
CrewChange::AddRole(AddRole {
to: Member::literally_everyone(),
role: "reader".to_owned(),
}),
CrewChange::RevealSecret(RevealSecret {
secret_kind: CONTENT_SECRET.to_owned(),
to: Member::literally_everyone().signer,
encr: Member::literally_everyone()
.encrypt_from_anon(&profile_team_content_secret),
}),
])
.await
.unwrap();
profile_team
.crew
.make_changes([CrewChange::MakeStatement(MakeStatement {
path: named_doc_path(PROFILE_DOC_NAME),
value: litl::to_val(profile_doc.id()).unwrap(),
})])
.await
.unwrap();
let account = self.load_team(in_account).await;
account
.crew
.wait_for_state(|state| state.shared_secrets.contains_key(PARTICIPATION_SECRET))
.await;
account
.crew
.make_changes([CrewChange::MakeStatement(MakeStatement {
path: named_doc_path(PROFILE_DOC_NAME),
value: litl::to_val(profile_doc.id()).unwrap(),
})])
.await
.unwrap();
profile_doc
}
pub async fn wait_for_account_profile(&self, account_id: &TeamID) -> Doc {
let account = self.load_team(account_id).await;
let profile_doc_id = account.wait_for_named_doc(PROFILE_DOC_NAME).await;
self.load_doc(profile_doc_id).await
}
pub fn get_account_profile_id(&self, account_id: &TeamID) -> Option<DocID> {
let account = self.get_loaded_team(account_id)?;
account.get_named_doc(PROFILE_DOC_NAME)
}
pub async fn introduce_in_as(&self, team: &Team, account_id: &TeamID) {
debug!("Getting profile for {:?}", account_id);
let profile = self.wait_for_account_profile(account_id).await;
debug!("Getting profile credential for {:?}", account_id);
let (profile_credential, _, _) = profile
.doc_crew()
.get_credentials_with_role(&HashSet::from_iter([ADMIN_ROLE.to_owned()]))
.into_iter()
.next()
.expect("Expected to have crendential for profile to sign intro with");
debug!("Got profile credential for {:?}", account_id);
team.crew
.make_changes([CrewChange::MakeStatement(MakeStatement {
path: format!("{}{}", INTRODUCTION_PREFIX, profile.id()),
value: litl::to_val(profile_credential.signer().sign(profile.id())).unwrap(),
})])
.await
.unwrap();
debug!(
"Made intro in {:?} for {:?} with profile {:?}",
team.id(),
account_id,
profile.id()
);
}
pub async fn async_is_introducted_in(&self, team: &Team) -> bool {
let account_id = self.account().expect("Account required");
let profile = self.wait_for_account_profile(&account_id).await;
let expected_path = format!("{}{}", INTRODUCTION_PREFIX, profile.id());
team.crew
.current_state()
.expect("Expected crew state when checking for intro")
.statements
.iter()
.any(|(path, value, _)| {
if path == &expected_path {
if let Ok(introduced_profile) = litl::from_val::<Signed<DocID>>(value.clone()) {
introduced_profile.attested == profile.id()
} else {
false
}
} else {
false
}
})
}
pub fn is_currently_introducted_in(&self, team: &Team) -> bool {
let account_id = self.account().expect("Account required");
let profile_id = self
.get_account_profile_id(&account_id)
.expect("Expected profile id to be loaded");
let expected_path = format!("{}{}", INTRODUCTION_PREFIX, profile_id);
team.crew
.current_state()
.expect("Expected crew state when checking for intro")
.statements
.iter()
.any(|(path, value, _)| {
if path == &expected_path {
if let Ok(introduced_profile) = litl::from_val::<Signed<DocID>>(value.clone()) {
introduced_profile.attested == profile_id
} else {
false
}
} else {
false
}
})
}
pub async fn ensure_introduced_in(&self, team: &Team) {
if !self.async_is_introducted_in(team).await {
let account_id = self.account().expect("Account required");
self.introduce_in_as(team, &account_id).await;
}
}
pub async fn verify_profile(
&self,
signed_id: Signed<DocID>,
) -> Result<Doc, Vec<SignatureError>> {
let profile = self.load_doc(signed_id.attested).await;
let already_verified =
VERIFIED_PROFILE_CACHE.with(|cache| cache.borrow().contains(&signed_id.attested));
if already_verified {
return Ok(profile);
}
match profile
.doc_crew()
.is_signed_by_member_with_role(&signed_id, "admin")
{
Ok(()) => {
VERIFIED_PROFILE_CACHE.with(|cache| cache.borrow_mut().insert(signed_id.attested));
Ok(profile)
}
Err(e) => Err(e),
}
}
}
thread_local! {
pub static VERIFIED_PROFILE_CACHE: RefCell<HashSet<DocID>> = RefCell::new(HashSet::new());
}