use mkit_attest::grant::{RefFlags, VerifiedGrant, head_packmap, packmap_head};
use mkit_core::refs::RefWriteCondition;
use super::ff::FastForward;
use crate::error::ServerError;
use crate::op::{OpKind, PresenceRequirement, RefUpdate};
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct ScopeOutcome {
pub presence: Option<PresenceRequirement>,
pub fast_forward: Option<FastForward>,
}
impl ScopeOutcome {
fn presence(requirement: Option<PresenceRequirement>) -> Self {
Self {
presence: requirement,
fast_forward: None,
}
}
}
fn denied() -> ServerError {
ServerError::permission_denied("write grant rejected: ref scope")
}
pub(crate) fn authorize(
grant: &VerifiedGrant,
kind: &OpKind,
indexed_mode: bool,
) -> Result<ScopeOutcome, ServerError> {
gate_with(kind, indexed_mode, |name| grant.effective_flags(name))
}
fn gate_with(
kind: &OpKind,
indexed_mode: bool,
flags: impl Fn(&str) -> RefFlags,
) -> Result<ScopeOutcome, ServerError> {
match kind {
OpKind::UpdateRef(update) => {
if update.name == "refs/mkit/packmap" || update.name.starts_with("refs/mkit/packmap/") {
return Err(denied());
}
check_change(update, flags(&update.name), indexed_mode)
}
OpKind::AdvanceRefs { head, packmap, .. } => {
if head_packmap(&head.name).as_deref() != Some(packmap.name.as_str())
|| packmap_head(&packmap.name).as_deref() != Some(head.name.as_str())
{
return Err(denied());
}
check_change(head, flags(&head.name), indexed_mode)
}
OpKind::BeginUpload { ref_name, .. } if !flags(ref_name).is_empty() => {
Ok(ScopeOutcome::default())
}
_ => Err(denied()),
}
}
fn check_change(
update: &RefUpdate,
flags: RefFlags,
indexed_mode: bool,
) -> Result<ScopeOutcome, ServerError> {
if update.new.is_none() {
return flags
.contains(RefFlags::DELETE)
.then(ScopeOutcome::default)
.ok_or_else(denied);
}
match (update.condition, update.new) {
(RefWriteCondition::Missing, _) => flags
.contains(RefFlags::CREATE)
.then(ScopeOutcome::default)
.ok_or_else(denied),
(RefWriteCondition::Match(from), Some(to)) => {
if flags.contains(RefFlags::FORCE) {
return Ok(ScopeOutcome::default());
}
if flags.contains(RefFlags::UPDATE) {
if !indexed_mode {
return Err(ServerError::permission_denied(
"update without force needs indexed mode",
));
}
return Ok(ScopeOutcome {
presence: None,
fast_forward: Some(FastForward {
name: update.name.clone(),
from,
to,
}),
});
}
Err(denied())
}
(RefWriteCondition::Match(_), None) => Err(denied()),
(RefWriteCondition::Any, _) => {
let create = flags.contains(RefFlags::CREATE);
let force = flags.contains(RefFlags::FORCE);
match (create, force) {
(false, false) => Err(denied()),
(true, true) => Ok(ScopeOutcome::default()),
(true, false) => Ok(ScopeOutcome::presence(Some(PresenceRequirement::Absent(
update.name.clone(),
)))),
(false, true) => Ok(ScopeOutcome::presence(Some(PresenceRequirement::Present(
update.name.clone(),
)))),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::op::RefUpdate;
use mkit_core::protocol::PackKey;
fn change(name: &str, condition: RefWriteCondition, delete: bool) -> OpKind {
OpKind::UpdateRef(RefUpdate {
name: name.into(),
condition,
new: (!delete).then_some([1; 32]),
})
}
fn advance(head: &str, packmap: &str) -> OpKind {
let OpKind::UpdateRef(head) = change(head, RefWriteCondition::Any, false) else {
unreachable!()
};
let OpKind::UpdateRef(packmap) = change(packmap, RefWriteCondition::Any, false) else {
unreachable!()
};
OpKind::AdvanceRefs {
head,
packmap,
tickets: vec![],
}
}
#[test]
fn opaque_required_flag_table_all_subsets() {
let name = "refs/heads/main";
for bits in 0..16 {
let mut flags = RefFlags::EMPTY;
for (bit, flag) in [
RefFlags::CREATE,
RefFlags::UPDATE,
RefFlags::FORCE,
RefFlags::DELETE,
]
.into_iter()
.enumerate()
{
if bits & (1 << bit) != 0 {
flags = flags.union(flag);
}
}
let c = flags.contains(RefFlags::CREATE);
let f = flags.contains(RefFlags::FORCE);
let d = flags.contains(RefFlags::DELETE);
let missing = change(name, RefWriteCondition::Missing, false);
let matched = change(name, RefWriteCondition::Match([2; 32]), false);
let deleted = change(name, RefWriteCondition::Match([2; 32]), true);
let any = change(name, RefWriteCondition::Any, false);
assert_eq!(
gate_with(&missing, false, |_| flags).is_ok(),
c,
"bits={bits}"
);
assert_eq!(
gate_with(&matched, false, |_| flags).is_ok(),
f,
"bits={bits}"
);
assert_eq!(
gate_with(&deleted, false, |_| flags).is_ok(),
d,
"bits={bits}"
);
assert_eq!(
gate_with(&any, false, |_| flags).is_ok(),
c || f,
"bits={bits}"
);
let requirement = gate_with(&any, false, |_| flags)
.ok()
.and_then(|o| o.presence);
assert_eq!(
requirement,
match (c, f) {
(true, false) => Some(PresenceRequirement::Absent(name.into())),
(false, true) => Some(PresenceRequirement::Present(name.into())),
_ => None,
},
"bits={bits}"
);
}
}
#[test]
fn indexed_required_flag_table_all_subsets() {
let name = "refs/heads/main";
let matched = change(name, RefWriteCondition::Match([2; 32]), false);
for bits in 0..16 {
let mut flags = RefFlags::EMPTY;
for (bit, flag) in [
RefFlags::CREATE,
RefFlags::UPDATE,
RefFlags::FORCE,
RefFlags::DELETE,
]
.into_iter()
.enumerate()
{
if bits & (1 << bit) != 0 {
flags = flags.union(flag);
}
}
let (u, f) = (
flags.contains(RefFlags::UPDATE),
flags.contains(RefFlags::FORCE),
);
let outcome = gate_with(&matched, true, |_| flags);
assert_eq!(outcome.is_ok(), u || f, "bits={bits}");
assert_eq!(
outcome.ok().and_then(|o| o.fast_forward).is_some(),
u && !f,
"bits={bits}"
);
for kind in [
change(name, RefWriteCondition::Missing, false),
change(name, RefWriteCondition::Match([2; 32]), true),
change(name, RefWriteCondition::Any, false),
] {
assert_eq!(
gate_with(&kind, true, |_| flags).ok(),
gate_with(&kind, false, |_| flags).ok(),
"bits={bits}"
);
}
}
}
#[test]
fn update_only_needs_indexed_mode_and_yields_the_ancestry_requirement() {
let matched = change("refs/heads/main", RefWriteCondition::Match([2; 32]), false);
assert_eq!(
gate_with(&matched, false, |_| RefFlags::UPDATE)
.unwrap_err()
.public_message(),
"update without force needs indexed mode"
);
assert_eq!(
gate_with(&matched, true, |_| RefFlags::UPDATE).unwrap(),
ScopeOutcome {
presence: None,
fast_forward: Some(FastForward {
name: "refs/heads/main".into(),
from: [2; 32],
to: [1; 32],
}),
}
);
let forced = RefFlags::UPDATE.union(RefFlags::FORCE);
assert_eq!(
gate_with(&matched, true, |_| forced).unwrap(),
ScopeOutcome::default()
);
assert!(gate_with(&matched, true, |_| RefFlags::CREATE).is_err());
}
#[test]
fn begin_upload_and_packmap_pairing() {
let head = "refs/heads/main";
let packmap = "refs/mkit/packmap/main";
let begin = OpKind::BeginUpload {
ref_name: head.into(),
key: PackKey::new([1; 32]),
bytes: 1,
};
assert!(gate_with(&begin, false, |_| RefFlags::DELETE).is_ok());
assert!(gate_with(&begin, false, |_| RefFlags::EMPTY).is_err());
assert!(
gate_with(
&change(packmap, RefWriteCondition::Any, false),
false,
|_| RefFlags::CREATE
)
.is_err()
);
assert!(gate_with(&advance(head, packmap), false, |_| RefFlags::CREATE).is_ok());
assert!(
gate_with(&advance(head, "refs/mkit/packmap/other"), false, |_| {
RefFlags::CREATE
})
.is_err()
);
assert!(
gate_with(&advance("refs/tags/main", packmap), false, |_| {
RefFlags::CREATE
})
.is_err()
);
}
}