use knot_cob::{Checkpoint, CobHome, CobStore, Evaluate, KnotAuthored, Materialized, WriteContext};
use knot_cobs::{CollaboratorsChange, CollaboratorsCob, Grant, GrantChange, GrantList, Removal};
use knot_record::grant::GrantCollection;
use knot_runtime::Signer;
use knot_types::{AccountDid, Blocked, RecordCollection, RecordRkey, RepoRole, UnixSeconds};
use super::drafts::{GrantDraft, SwapInput, no_swap_commit, typed};
use super::error::WriteError;
use super::ledger::{noun_of, ready};
use super::{Done, Ledger};
fn grant_subject(
collection: &RecordCollection,
rkey: Option<RecordRkey>,
) -> Result<AccountDid, WriteError> {
let noun = noun_of(collection);
let rkey = rkey.ok_or_else(|| {
WriteError::invalid_request(format!(
"a {noun}'s record key is the subject's DID; send that DID as the rkey"
))
})?;
AccountDid::new(rkey.as_str()).map_err(|_| {
WriteError::invalid_request(format!(
"a {noun}'s record key is the subject's DID, and {} isn't one",
rkey.as_str()
))
})
}
pub(super) fn grant_list_apply<E>(
store: &CobStore,
home: &CobHome,
change: &E::Change,
signer: &dyn Signer,
now: UnixSeconds,
) -> Result<(), WriteError>
where
E: Checkpoint + Evaluate<State = GrantList>,
E::Change: KnotAuthored + Clone + GrantChange,
{
match store.list::<E>()?.as_slice() {
[] => {
if GrantList::empty().would_change(change) {
store.create_materialized(
home,
&Materialized::bare(change.clone()),
signer,
now,
)?;
}
Ok(())
}
[object] => {
store.update_maybe_checkpointed::<E, WriteError>(
home,
*object,
signer,
now,
|list| Ok(list.would_change(change).then(|| change.clone())),
)?;
Ok(())
}
many => Err(WriteError::internal(format!(
"{} collaborative objects of one type share namespace",
many.len()
))),
}
}
impl Ledger<'_> {
pub(super) fn collaborator_add(
&self,
cx: &WriteContext<'_>,
rkey: Option<RecordRkey>,
record: serde_json::Value,
) -> Result<Done, WriteError> {
let collection = GrantCollection::Collaborator.records();
let subject = grant_subject(&collection, rkey)?;
typed::<GrantDraft>(record, &collection)?.parse(&collection)?;
knot_record::grant::record_key(&subject, GrantCollection::Collaborator).map_err(
|error| {
WriteError::invalid_request(format!(
"the subject's DID doesn't fit as a record key under {collection}: {error}"
))
},
)?;
self.manage_collaborators(
cx,
CollaboratorsChange::Add(Grant {
subject,
added_by: cx.actor().clone(),
created_at: self.now,
}),
)
}
pub(super) fn collaborator_remove(
&self,
cx: &WriteContext<'_>,
swap: SwapInput,
) -> Result<Done, WriteError> {
if swap.swap_commit.is_some() {
return Err(no_swap_commit());
}
let subject = grant_subject(&swap.collection, Some(swap.rkey))?;
self.manage_collaborators(cx, CollaboratorsChange::Remove(Removal { subject }))
}
fn manage_collaborators(
&self,
cx: &WriteContext<'_>,
change: CollaboratorsChange,
) -> Result<Done, WriteError> {
let permission = cx.permission();
if permission.blocked() != Blocked::No || permission.role() != RepoRole::Owner {
return Err(WriteError::forbidden(
"only repository owner may manage collaborators",
));
}
let owner = ready(self.index.owner_of(self.repo), "registry")?;
if owner.is_some_and(|owner| owner.is(change.subject())) {
return Ok(Done::Granted);
}
grant_list_apply::<CollaboratorsCob>(
&CobStore::new(self.git),
&CobHome::from(self.repo),
&change,
self.signer,
self.now,
)?;
Ok(Done::Granted)
}
}