use credo::{
ClaimBody, Credential, CredentialSource, MakeClaimError, PermissionKind, Scope, ScopeID,
SimpleCredentialSource, WRITE_TO_SCOPE,
};
use litl::{impl_debug_as_litl, impl_nested_tagged_data_serde, NestedTaggedData, Val};
use ridl::symm_encr::{DecryptionError, KeySecret};
use thiserror::Error;
use caro::{BlobDiff, Diff};
use futures::{future, Stream, StreamExt};
use crate::{
conventions::{
admin_permissions, claim_to_name_doc, named_doc_path, read_permissions, write_permissions,
NAMED_DOC_PREFIX, READ_CONTENT,
},
invitation::{Invitation, InvitationKind, InvitationPrivatePart, InvitationToken},
DocID,
};
use super::Jazz;
#[derive(Clone)]
pub struct Team {
pub(crate) scope: Scope,
content: caro::Node,
}
impl Team {
pub fn id(&self) -> TeamID {
TeamID(self.scope.id())
}
}
impl Jazz {
pub async fn create_team(&self) -> Team {
let initial_recipient = Credential::new_random();
self.create_team_for(initial_recipient, vec![]).await
}
pub async fn create_team_for(
&self,
initial_recipient: Credential,
extra_permissions: Vec<PermissionKind>,
) -> Team {
self.create_team_for_with_credential_source(
initial_recipient,
extra_permissions,
self.0.borrow().main_credential_source.clone_ref(),
)
.await
}
pub(crate) async fn create_team_for_with_credential_source(
&self,
initial_recipient: Credential,
extra_permissions: Vec<PermissionKind>,
credential_source: Box<dyn CredentialSource>,
) -> Team {
let credo = self.credo();
let team_scope = credo
.create_scope(
initial_recipient.clone(),
admin_permissions()
.into_iter()
.chain(extra_permissions)
.collect(),
None,
credential_source,
)
.await;
team_scope
.make_claim(ClaimBody::AddSharedSecretRecipient {
secret_kind: READ_CONTENT.to_string(),
recipient: initial_recipient.for_accepting_secrets.pub_id(),
})
.await
.unwrap();
team_scope.rotate_shared_secret(READ_CONTENT).await.unwrap();
Team {
scope: team_scope,
content: self.content(),
}
}
pub fn get_team(&self, team_id: &TeamID) -> Team {
Team {
scope: self.credo().get_scope(
&team_id.0,
self.0.borrow().main_credential_source.clone_ref(),
),
content: self.content(),
}
}
pub fn get_team_with_credential_source(
&self,
team_id: &TeamID,
credential_source: Box<dyn CredentialSource>,
) -> Team {
Team {
scope: self.credo().get_scope(&team_id.0, credential_source),
content: self.content(),
}
}
pub async fn join_team(
&self,
invitation_token: InvitationToken,
) -> Result<(Team, Invitation, InvitationPrivatePart), JoinTeamError> {
let blob_diffs = self.content().diffs(
invitation_token.invitation_id,
format!("load-{:?}", invitation_token.invitation_id),
);
let invitation_data = blob_diffs
.filter_map(|diff| match diff {
Diff::Blob(BlobDiff { data, .. }) => future::ready(data),
_ => panic!("Unexpected blob diff"),
})
.next()
.await
.ok_or(JoinTeamError::CouldntLoadInvitation)?;
let invitation: Invitation = litl::from_val(invitation_data)?;
let private_part = invitation_token.secret.decrypt(&invitation.private)?;
let invitation_credential_source = SimpleCredentialSource::new();
invitation_credential_source
.add_credential(private_part.team_id.0, private_part.credential.clone());
let team_from_invitation = self.get_team_with_credential_source(
&private_part.team_id,
Box::new(invitation_credential_source),
);
let member_credential = Credential::new_random();
let add_self_as_read_secret_recp = ClaimBody::AddSharedSecretRecipient {
secret_kind: READ_CONTENT.to_string(),
recipient: member_credential.for_accepting_secrets.pub_id(),
};
team_from_invitation
.scope
.await_credential_for(&add_self_as_read_secret_recp)
.await;
team_from_invitation
.scope
.make_claim(add_self_as_read_secret_recp)
.await
.map_err(|err| JoinTeamError::CouldntAddSelfAsReadSecretRecipient(err))?;
team_from_invitation
.scope
.re_reveal_shared_secret(READ_CONTENT)
.await
.map_err(|err| JoinTeamError::CouldntReRevealReadSecret(err))?;
let add_self_as_write_secret_recp = ClaimBody::AddSharedSecretRecipient {
secret_kind: WRITE_TO_SCOPE.to_string(),
recipient: member_credential.for_accepting_secrets.pub_id(),
};
if let InvitationKind::Writer | InvitationKind::Admin = &private_part.kind {
team_from_invitation
.scope
.await_credential_for(&add_self_as_write_secret_recp)
.await;
team_from_invitation
.scope
.make_claim(add_self_as_write_secret_recp)
.await
.map_err(|err| JoinTeamError::CouldntAddSelfAsWriteSecretRecipient(err))?;
team_from_invitation
.scope
.re_reveal_shared_secret(WRITE_TO_SCOPE)
.await
.map_err(|err| JoinTeamError::CouldntReRevealWriteSecret(err))?;
}
let invitation_permissions_to_grant_self = match &private_part.kind {
InvitationKind::Reader => read_permissions(),
InvitationKind::Writer => write_permissions(),
InvitationKind::Admin => admin_permissions(),
};
for permission in invitation_permissions_to_grant_self {
team_from_invitation
.scope
.make_claim(ClaimBody::Permission {
to: member_credential.for_making_claims.pub_id(),
permitted: permission,
as_of: ti64::now(),
})
.await
.map_err(|err| JoinTeamError::CouldntGrantSelfPermission(err))?;
}
self.0
.borrow()
.main_credential_source
.add_credential(private_part.team_id.0, member_credential);
Ok((
self.get_team(&private_part.team_id),
invitation,
private_part,
))
}
}
impl Team {
pub async fn create_invitation(
&self,
kind: InvitationKind,
doc: Option<DocID>,
public_meta: Option<Val>,
private_meta: Option<Val>,
expires_at: ti64::MsSinceEpoch,
) -> Result<InvitationToken, MakeClaimError> {
let invitation_identity = Credential::new_random();
let invitation_permissions_to_delegate = match kind {
InvitationKind::Reader => read_permissions(),
InvitationKind::Writer => write_permissions(),
InvitationKind::Admin => admin_permissions(),
};
let mut invitation_permissions = invitation_permissions_to_delegate
.into_iter()
.map(|permission_kind| PermissionKind::Delegate {
delegated: Box::new(permission_kind),
})
.collect::<Vec<_>>();
invitation_permissions.push(PermissionKind::AddSharedSecretRecipient {
secret_kind: READ_CONTENT.to_owned(),
});
invitation_permissions.push(PermissionKind::RevealSharedSecret {
secret_kind: READ_CONTENT.to_owned(),
});
if let InvitationKind::Writer | InvitationKind::Admin = kind {
invitation_permissions.push(PermissionKind::AddSharedSecretRecipient {
secret_kind: WRITE_TO_SCOPE.to_owned(),
});
invitation_permissions.push(PermissionKind::RevealSharedSecret {
secret_kind: WRITE_TO_SCOPE.to_owned(),
});
}
let mut invitation_claims = Vec::new();
for permission in invitation_permissions {
invitation_claims.push(
self.scope
.make_claim(ClaimBody::Permission {
to: invitation_identity.for_making_claims.pub_id(),
as_of: ti64::now(),
permitted: permission,
})
.await
.unwrap(),
);
}
invitation_claims.push(
self.scope
.make_claim(ClaimBody::AddSharedSecretRecipient {
secret_kind: READ_CONTENT.to_owned(),
recipient: invitation_identity.for_accepting_secrets.pub_id(),
})
.await?,
);
self.scope.re_reveal_shared_secret(READ_CONTENT).await?;
invitation_claims.push(
self.scope
.make_claim(ClaimBody::AddSharedSecretRecipient {
secret_kind: WRITE_TO_SCOPE.to_owned(),
recipient: invitation_identity.for_accepting_secrets.pub_id(),
})
.await?,
);
self.scope.re_reveal_shared_secret(WRITE_TO_SCOPE).await?;
for invitation_claim_id in invitation_claims {
self.scope
.make_claim(ClaimBody::Revocation {
revoked_claim_id: invitation_claim_id,
as_of: expires_at,
})
.await
.unwrap();
}
let secret = KeySecret::new_random();
let invitation = Invitation {
public_meta,
private: secret.encrypt(&InvitationPrivatePart {
team_id: self.id(),
kind,
credential: invitation_identity,
doc,
private_meta,
}),
};
let invitation_id = self.content.create_blob(invitation).await;
Ok(InvitationToken {
invitation_id,
secret,
})
}
pub async fn set_named_document(&self, doc_id: DocID, name: &str) {
self.scope
.make_claim(claim_to_name_doc(doc_id, name))
.await
.unwrap();
}
pub async fn resolve_named_document(&self, name: &str) -> DocID {
let mut updates = self.scope.updates(format!("named_doc_{}", name,));
while let Some(update) = updates.next().await {
if let Some(doc_id_val) =
update
.valid_claims
.iter()
.find_map(|(_, (claim, _))| match &claim.body {
ClaimBody::Statement { path, value } if path == &named_doc_path(name) => {
Some(value.clone())
}
_ => None,
})
{
return litl::from_val::<DocID>(doc_id_val).unwrap();
}
}
unreachable!("Team update stream ended before named document was found");
}
pub fn all_named_documents(&self) -> impl Stream<Item = Vec<(String, DocID)>> + 'static {
let updates = self.scope.updates("all_named_docs".to_owned());
updates.map(|update| {
update
.valid_claims
.iter()
.filter_map(|(_, (claim, _))| {
if let ClaimBody::Statement { path, value } = &claim.body {
if path.starts_with(&NAMED_DOC_PREFIX) {
Some((
path.replace(NAMED_DOC_PREFIX, ""),
litl::from_val(value.clone()).unwrap(),
))
} else {
None
}
} else {
None
}
})
.collect()
})
}
}
#[derive(Error, Debug)]
pub enum JoinTeamError {
#[error("Couldn't load invitation")]
CouldntLoadInvitation,
#[error("Couldn't deserialize invitation")]
CouldntDeserializeInvitation(#[from] litl::ValDeserializerError),
#[error("Couldn't decrypt invitation")]
CouldntDecryptInvitation(#[from] DecryptionError),
#[error("Couldn't add self as read secret recipient")]
CouldntAddSelfAsReadSecretRecipient(#[source] MakeClaimError),
#[error("Couldn't re-reveal read secret")]
CouldntReRevealReadSecret(#[source] MakeClaimError),
#[error("Couldn't add self as write secret recipient")]
CouldntAddSelfAsWriteSecretRecipient(#[source] MakeClaimError),
#[error("Couldn't re-reveal wrte secret")]
CouldntReRevealWriteSecret(#[source] MakeClaimError),
#[error("Couldn't grant self permission")]
CouldntGrantSelfPermission(#[source] MakeClaimError),
}
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct TeamID(pub ScopeID);
impl_nested_tagged_data_serde!(TeamID);
impl_debug_as_litl!(TeamID);
impl NestedTaggedData for TeamID {
const TAG: &'static str = "team";
type Inner = ScopeID;
fn as_inner(&self) -> &Self::Inner {
&self.0
}
fn from_inner(inner: Self::Inner) -> Self
where
Self: Sized,
{
TeamID(inner)
}
}