use std::collections::BTreeSet;
use knot_acl::{KnotAcl, RepoAccess};
use knot_index::Index;
use knot_types::{
AccountDid, AtUri, ChangeId, ContributionPermission, Oid, RecordAddress, RecordCid, RepoDid,
};
use super::COLLABORATE_REMEDY;
use super::drafts::{SubmitPatchInput, on_this_knot};
use super::error::WriteError;
use super::ledger::{decode, ready};
use super::patches::patch_opening;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
CreateRecord,
PutRecord,
DeleteRecord,
SubmitPatch,
}
impl Method {
pub const ALL: [Self; 4] = [
Self::CreateRecord,
Self::PutRecord,
Self::DeleteRecord,
Self::SubmitPatch,
];
pub fn nsid(self) -> &'static str {
match self {
Self::CreateRecord => "com.atproto.repo.createRecord",
Self::PutRecord => "com.atproto.repo.putRecord",
Self::DeleteRecord => "com.atproto.repo.deleteRecord",
Self::SubmitPatch => "org.tangled.review.submitPatch",
}
}
pub fn from_nsid(nsid: &str) -> Option<Self> {
Self::ALL.into_iter().find(|method| method.nsid() == nsid)
}
}
pub struct Session {
pub actor: AccountDid,
pub repo: RepoDid,
pub(super) permission: ContributionPermission,
}
impl Session {
pub fn require_contribution(&self) -> Result<(), WriteError> {
self.permission
.contributes()
.is_allowed()
.then_some(())
.ok_or_else(|| {
WriteError::forbidden(format!(
"`uploads` belong to contributors this repo's policy allows, and it leaves \
you out; {COLLABORATE_REMEDY}"
))
})
}
pub fn require_patch_opening(&self) -> Result<(), WriteError> {
patch_opening(&self.actor, self.permission)
}
}
pub enum Done {
Written(Written),
Deleted,
Granted,
}
pub struct Written {
pub uri: AtUri<String>,
pub cid: RecordCid,
pub change: Option<ChangeId>,
}
pub fn session(
index: &Index,
admins: &BTreeSet<AccountDid>,
actor: AccountDid,
repo: RepoDid,
) -> Result<Session, WriteError> {
if ready(index.is_fostered(&repo), "registry")? {
return Err(WriteError::forbidden(
"this repo is only fostered here. its records change on its origin knot",
));
}
let policy = ready(index.contribution_policy(&repo), "registry")?
.ok_or_else(|| WriteError::not_found("No such repository on this knot"))?;
index.ensure_collaborators(&repo)?;
let collaboration = index.collaboration_consent(&repo, &actor);
let membership = index.member_status(&actor);
let access = RepoAccess::of(&collaboration, &membership, &policy)
.ok_or_else(|| WriteError::internal("Consents answered with mismatched subjects"))?;
let acl = KnotAcl::new(admins, knot_types::AdmissionPolicy::Closed, index);
let permission = ready(knot_acl::permission_of(&acl, &access), "grant list")?;
Ok(Session {
actor,
repo,
permission,
})
}
pub fn fork_head(
method: Method,
repo: &RepoDid,
body: &[u8],
) -> Result<Option<(RepoDid, Oid)>, WriteError> {
if method != Method::SubmitPatch {
return Ok(None);
}
let input: SubmitPatchInput = decode(body)?;
on_this_knot(&input.repo, repo, "patch")?;
Ok((input.patch.source_repo != input.repo)
.then_some((input.patch.source_repo, input.patch.head.oid)))
}
pub(super) fn wrote(repo: &RepoDid, address: &RecordAddress, cid: RecordCid) -> Written {
Written {
uri: address.at_uri(repo),
cid,
change: None,
}
}