use std::collections::BTreeSet;
use chrono::{DateTime, Utc};
use super::model::{Level, Resource, Right, RightRow, Scope};
use super::single_admin::Waivable;
use super::store::Snapshot;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
PermissionDenied(String),
ScopeViolation(String),
Escalation(String),
MembersOnly(String),
SelfGrant(String),
}
impl std::fmt::Display for Refusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Refusal::PermissionDenied(m)
| Refusal::ScopeViolation(m)
| Refusal::Escalation(m)
| Refusal::MembersOnly(m)
| Refusal::SelfGrant(m) => f.write_str(m),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RuleSettings {
pub maintainer_grants_commit: bool,
}
#[derive(Debug, Clone)]
pub struct Held<'a> {
pub scope: &'a Scope,
pub resource: Resource,
pub row: &'a RightRow,
}
pub fn explicit_rights<'a>(snap: &'a Snapshot, did: &str, now: DateTime<Utc>) -> Vec<Held<'a>> {
let mut out = Vec::new();
for (scope, set) in &snap.rights {
let Some(resource) = snap.scope_resource(scope) else {
continue;
};
for row in &set.rows {
if row.subject == did && row.is_live(now) {
out.push(Held {
scope,
resource: resource.clone(),
row,
});
}
}
}
out
}
pub fn conferred(held: Right, held_on: &Resource, target: &Resource) -> BTreeSet<Right> {
let mut out = BTreeSet::new();
if !held_on.contains(target) {
return out;
}
let same = held_on == target;
match held {
Right::NsAdmin => {
if same {
out.extend([Right::NsAdmin, Right::RepoCreate, Right::CommitSign]);
} else {
out.extend([Right::RepoOwn, Right::RepoMaintain, Right::CommitSign]);
}
}
Right::RepoCreate => {
if same {
out.insert(Right::RepoCreate);
}
}
Right::RepoOwn => {
out.extend([Right::RepoOwn, Right::RepoMaintain, Right::CommitSign]);
}
Right::RepoMaintain => {
out.extend([Right::RepoMaintain, Right::CommitSign]);
}
Right::CommitSign => {
out.insert(Right::CommitSign);
}
}
out
}
pub fn effective_on(
snap: &Snapshot,
did: &str,
target: &Resource,
now: DateTime<Utc>,
) -> BTreeSet<Right> {
let mut out = BTreeSet::new();
for held in explicit_rights(snap, did, now) {
out.extend(conferred(held.row.right, &held.resource, target));
}
out
}
fn carries(
held: Right,
held_on: &Resource,
right: Right,
target: &Resource,
settings: RuleSettings,
) -> bool {
if !held_on.contains(target) {
return false;
}
let same = held_on == target;
match held {
Right::NsAdmin => {
if same {
matches!(
right,
Right::NsAdmin | Right::RepoCreate | Right::CommitSign
)
} else {
matches!(
right,
Right::RepoOwn | Right::RepoMaintain | Right::CommitSign
)
}
}
Right::RepoCreate => false,
Right::RepoOwn => {
same && matches!(
right,
Right::RepoOwn | Right::RepoMaintain | Right::CommitSign
)
}
Right::RepoMaintain => {
same && right == Right::CommitSign && settings.maintainer_grants_commit
}
Right::CommitSign => false,
}
}
pub fn authority_to_grant(
snap: &Snapshot,
actor: &str,
right: Right,
target: &Resource,
settings: RuleSettings,
now: DateTime<Utc>,
) -> Result<(), Refusal> {
match (right.level(), target.is_namespace()) {
(Level::Namespace, false) => {
return Err(Refusal::ScopeViolation(format!(
"{right} applies to a namespace, and {target} is a repository"
)));
}
(Level::Repository, true) => {
return Err(Refusal::ScopeViolation(format!(
"{right} applies to a repository, and {target} is a namespace"
)));
}
_ => {}
}
let held = explicit_rights(snap, actor, now);
let covering: Vec<&Held> = held
.iter()
.filter(|h| h.resource.contains(target))
.collect();
if covering.is_empty() {
let ns = target.namespace_resource();
if held.iter().any(|h| ns.contains(&h.resource)) {
return Err(Refusal::ScopeViolation(format!(
"{target} is wider than any resource you hold a right on"
)));
}
return Err(Refusal::PermissionDenied(format!(
"you hold no git right on or above {target}"
)));
}
if covering
.iter()
.any(|h| carries(h.row.right, &h.resource, right, target, settings))
{
return Ok(());
}
Err(Refusal::Escalation(format!(
"none of your rights on {target} carries the authority to grant or revoke {right}"
)))
}
pub fn authority_to_revoke(
snap: &Snapshot,
actor: &str,
row: &RightRow,
target: &Resource,
settings: RuleSettings,
now: DateTime<Utc>,
) -> Result<(), Refusal> {
if row.subject == actor {
return Ok(());
}
match authority_to_grant(snap, actor, row.right, target, settings, now) {
Ok(()) => Ok(()),
Err(Refusal::Escalation(m)) => Err(Refusal::Escalation(m)),
Err(Refusal::ScopeViolation(_)) | Err(Refusal::PermissionDenied(_)) => {
Err(Refusal::PermissionDenied(format!(
"you hold no git right on {target} that covers {}",
row.right
)))
}
Err(other) => Err(other),
}
}
pub fn members_only(
right: Right,
subject_is_member: bool,
actor_is_member: bool,
) -> Result<(), Refusal> {
if !right.is_elevated() {
return Ok(());
}
if !actor_is_member {
return Err(Refusal::MembersOnly(format!(
"{right} is granted only by a current member of the community"
)));
}
if !subject_is_member {
return Err(Refusal::MembersOnly(format!(
"{right} goes only to a current member of the community, holding standing in its \
access-control records"
)));
}
Ok(())
}
pub fn elevated_on(snap: &Snapshot, right: Right, target: &Resource) -> bool {
match snap.namespace_containing(target) {
Some(ns) if ns.mode == super::model::Mode::Manual => right.is_elevated(),
Some(ns) => right.is_elevated_in(ns, &target.to_string()),
None => right.is_elevated() || right == Right::RepoMaintain,
}
}
pub fn separation_of_duties(
actor: &str,
subject: &str,
right: Right,
elevated: bool,
target: &Resource,
) -> Result<(), Refusal> {
if actor != subject || !(elevated || right.is_elevated()) {
return Ok(());
}
if right.is_elevated() {
return Err(Refusal::SelfGrant(format!(
"{right} is an elevated right, and you cannot grant it to yourself: ask another \
administrator to grant it, or, if nobody else can, break the glass with \
git-ns/right/break-glass — `cnm git break-glass --right={right} \
--resource={target} --justification='…'` — which is audited, announced to every administrator, and \
flagged until another administrator ratifies or revokes it"
)));
}
Err(Refusal::SelfGrant(format!(
"{right} is elevated on {target}: the bridge's role map projects it to the forge's \
admin role (or the bridge has not reported its map yet), and you cannot grant it to \
yourself. Ask another owner or administrator to grant it"
)))
}
fn live_holders<'a>(
snap: &'a Snapshot,
scope: &Scope,
right: Right,
now: DateTime<Utc>,
) -> Vec<&'a str> {
snap.rows(scope)
.iter()
.filter(|r| r.right == right && r.is_live(now))
.map(|r| r.subject.as_str())
.collect()
}
fn permanent_holders<'a>(
snap: &'a Snapshot,
scope: &Scope,
right: Right,
now: DateTime<Utc>,
) -> Vec<&'a str> {
snap.rows(scope)
.iter()
.filter(|r| r.right == right && r.counts_for_invariants(now))
.map(|r| r.subject.as_str())
.collect()
}
pub fn is_last_owner(snap: &Snapshot, repo_id: &str, subject: &str, now: DateTime<Utc>) -> bool {
let owners = permanent_holders(snap, &Scope::Repo(repo_id.to_string()), Right::RepoOwn, now);
owners.contains(&subject) && owners.iter().all(|o| *o == subject)
}
pub fn is_last_admin(snap: &Snapshot, ns_id: &str, subject: &str, now: DateTime<Utc>) -> bool {
let admins = permanent_holders(
snap,
&Scope::Namespace(ns_id.to_string()),
Right::NsAdmin,
now,
);
admins.contains(&subject) && admins.iter().all(|a| *a == subject)
}
#[derive(Debug)]
pub struct RulesPassed(());
impl RulesPassed {
fn new() -> Self {
RulesPassed(())
}
#[cfg(test)]
pub fn for_test() -> Self {
RulesPassed(())
}
}
#[allow(clippy::too_many_arguments)]
pub fn grant_admitted(
snap: &Snapshot,
actor: &str,
subject: &str,
right: Right,
target: &Resource,
subject_is_member: bool,
actor_is_member: bool,
settings: RuleSettings,
now: DateTime<Utc>,
waiver: Option<&Waivable>,
) -> Result<RulesPassed, Refusal> {
authority_to_grant(snap, actor, right, target, settings, now)?;
match separation_of_duties(
actor,
subject,
right,
elevated_on(snap, right, target),
target,
) {
Err(Refusal::SelfGrant(_))
if waiver.is_some_and(|w| w.covers(actor, subject, right, target)) => {}
other => other?,
}
members_only(right, subject_is_member, actor_is_member)?;
Ok(RulesPassed::new())
}
#[allow(clippy::too_many_arguments)]
pub fn create_owners_admitted(
snap: &Snapshot,
actor: &str,
actor_is_member: bool,
ns_scope: &Scope,
ns_res: &Resource,
target: &Resource,
owners: &[(String, bool)],
settings: RuleSettings,
now: DateTime<Utc>,
waiver: Option<&Waivable>,
) -> Result<RulesPassed, Refusal> {
let explicit_create =
explicit_admitted(snap, actor, Right::RepoCreate, ns_scope, now).is_some();
let waived = waiver.is_some_and(|w| w.covers(actor, actor, Right::RepoOwn, target));
for (owner, owner_is_member) in owners {
if owner == actor {
if !explicit_create && !waived {
return Err(Refusal::SelfGrant(format!(
"your git.repo.create on this namespace is only implied by git.ns.admin, \
which does not make you the owner of what you create: name another member \
in owners, or break the glass once for git.repo.create — `cnm git \
break-glass --right=git.repo.create --resource={} \
--justification='…'` — which is audited, announced to every administrator, \
and flagged until another administrator ratifies or revokes it",
ns_res
)));
}
} else {
authority_to_grant(snap, actor, Right::RepoOwn, target, settings, now)?;
}
members_only(Right::RepoOwn, *owner_is_member, actor_is_member)?;
}
Ok(RulesPassed::new())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakGlassEntitlement {
GrantAuthority,
CommunityAdminHeadless,
}
impl BreakGlassEntitlement {
pub fn as_str(self) -> &'static str {
match self {
Self::GrantAuthority => "grantAuthority",
Self::CommunityAdminHeadless => "communityAdministratorHeadless",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BreakGlassRefusal {
Rule(Refusal),
NotHeadless(String),
}
impl From<Refusal> for BreakGlassRefusal {
fn from(r: Refusal) -> Self {
BreakGlassRefusal::Rule(r)
}
}
#[allow(clippy::too_many_arguments)]
pub fn break_glass_admitted(
snap: &Snapshot,
actor: &str,
right: Right,
target: &Resource,
actor_is_member: bool,
actor_community_admin: bool,
headless: bool,
settings: RuleSettings,
now: DateTime<Utc>,
) -> Result<(RulesPassed, BreakGlassEntitlement), BreakGlassRefusal> {
if !right.is_elevated() {
return Err(Refusal::ScopeViolation(format!(
"{right} is not elevated; grant it with git-ns/right/grant"
))
.into());
}
if matches!(
(right.level(), target.is_namespace()),
(Level::Namespace, false) | (Level::Repository, true)
) {
return Err(Refusal::ScopeViolation(format!(
"{right} does not apply to a resource at the level of {target}"
))
.into());
}
let entitlement = match authority_to_grant(snap, actor, right, target, settings, now) {
Ok(()) => BreakGlassEntitlement::GrantAuthority,
Err(refusal) => {
if right == Right::NsAdmin && target.is_namespace() && actor_community_admin {
if !headless {
return Err(BreakGlassRefusal::NotHeadless(format!(
"{target} has a live git.ns.admin, so it is not headless; its admins \
grant git.ns.admin with git-ns/right/grant"
)));
}
BreakGlassEntitlement::CommunityAdminHeadless
} else {
return Err(refusal.into());
}
}
};
members_only(right, actor_is_member, actor_is_member)?;
Ok((RulesPassed::new(), entitlement))
}
pub fn ratify_admitted(
snap: &Snapshot,
actor: &str,
row: &RightRow,
target: &Resource,
actor_community_admin: bool,
settings: RuleSettings,
now: DateTime<Utc>,
) -> Result<RulesPassed, Refusal> {
if row.subject == actor {
return Err(Refusal::PermissionDenied(
"a break-glass is ratified by someone other than its subject".into(),
));
}
if actor_community_admin {
return Ok(RulesPassed::new());
}
let confirmed = snap.without_unratified_break_glass();
match authority_to_grant(&confirmed, actor, row.right, target, settings, now) {
Ok(()) => Ok(RulesPassed::new()),
Err(Refusal::Escalation(m)) => Err(Refusal::Escalation(m)),
Err(_) => Err(Refusal::PermissionDenied(format!(
"ratifying {} on {target} needs the community-administrator capability, or a \
confirmed right on {target} that carries the authority to grant it",
row.right
))),
}
}
pub fn revoke_break_glass_admitted(
actor_community_admin: bool,
row: &RightRow,
) -> Option<RulesPassed> {
(actor_community_admin && row.is_unratified_break_glass()).then(RulesPassed::new)
}
pub fn revoke_admitted(
snap: &Snapshot,
actor: &str,
row: &RightRow,
target: &Resource,
settings: RuleSettings,
now: DateTime<Utc>,
) -> Result<RulesPassed, Refusal> {
authority_to_revoke(snap, actor, row, target, settings, now)?;
Ok(RulesPassed::new())
}
pub fn bind_admitted(community_admin: bool) -> Result<RulesPassed, Refusal> {
if !community_admin {
return Err(Refusal::PermissionDenied(
"binding a namespace needs the community-administrator capability".into(),
));
}
Ok(RulesPassed::new())
}
pub fn unbind_admitted(
snap: &Snapshot,
actor: &str,
ns_resource: &Resource,
community_admin: bool,
now: DateTime<Utc>,
) -> Result<RulesPassed, Refusal> {
if community_admin || effective_on(snap, actor, ns_resource, now).contains(&Right::NsAdmin) {
return Ok(RulesPassed::new());
}
Err(Refusal::PermissionDenied(format!(
"unbinding {ns_resource} needs git.ns.admin on it, or the community-administrator \
capability"
)))
}
pub fn holds_admitted(
snap: &Snapshot,
actor: &str,
right: Right,
on: &Resource,
now: DateTime<Utc>,
) -> Result<RulesPassed, Refusal> {
if effective_on(snap, actor, on, now).contains(&right) {
return Ok(RulesPassed::new());
}
Err(Refusal::PermissionDenied(format!(
"this needs {right} on {on}"
)))
}
pub fn explicit_admitted(
snap: &Snapshot,
actor: &str,
right: Right,
scope: &Scope,
now: DateTime<Utc>,
) -> Option<RulesPassed> {
snap.rows(scope)
.iter()
.any(|r| r.subject == actor && r.right == right && r.is_live(now))
.then(RulesPassed::new)
}
pub fn service_grant_admitted(
ns: &super::model::Namespace,
subject: &str,
right: Right,
target: &Resource,
) -> Result<RulesPassed, Refusal> {
let own_bridge = ns.bridge_did.as_deref() == Some(subject);
let bound = ns.state == super::model::NamespaceState::Bound;
if own_bridge && bound && right == Right::CommitSign && *target == ns.resource() {
return Ok(RulesPassed::new());
}
Err(Refusal::Escalation(
"a service grant is git.commit.sign on a bound namespace, to the bridge it records".into(),
))
}
pub fn drift_revert_admitted(
snap: &Snapshot,
actor: &str,
repo: &Resource,
now: DateTime<Utc>,
) -> Result<RulesPassed, Refusal> {
if effective_on(snap, actor, repo, now).contains(&Right::RepoOwn) {
Ok(RulesPassed::new())
} else {
Err(Refusal::PermissionDenied(format!(
"resolving drift on {repo} needs git.repo.own there"
)))
}
}
pub fn reseat_admitted(
actor_community_admin: bool,
headless: bool,
subject_member: bool,
) -> Option<RulesPassed> {
(actor_community_admin && headless && subject_member).then(RulesPassed::new)
}
pub fn reproject_admitted(
actor_community_admin: bool,
actor_member: bool,
explicit_ns_admin: bool,
repo_owner: bool,
) -> Option<RulesPassed> {
(actor_community_admin || (actor_member && (explicit_ns_admin || repo_owner)))
.then(RulesPassed::new)
}
pub fn owners(snap: &Snapshot, repo_id: &str, now: DateTime<Utc>) -> Vec<String> {
live_holders(snap, &Scope::Repo(repo_id.to_string()), Right::RepoOwn, now)
.into_iter()
.map(str::to_string)
.collect()
}
pub fn admins(snap: &Snapshot, ns_id: &str, now: DateTime<Utc>) -> Vec<String> {
live_holders(
snap,
&Scope::Namespace(ns_id.to_string()),
Right::NsAdmin,
now,
)
.into_iter()
.map(str::to_string)
.collect()
}
pub fn governs(snap: &Snapshot, did: &str, resource: &Resource, now: DateTime<Utc>) -> bool {
let rights = effective_on(snap, did, resource, now);
if resource.is_namespace() {
rights.contains(&Right::NsAdmin)
} else {
rights.contains(&Right::RepoOwn)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git_ns::model::{
Bootstrap, Mode, Namespace, NamespaceState, Repo, RepoState, RightsSet, SyncState,
SyncStatus, Visibility,
};
const ALICE: &str = "did:key:alice";
const BOB: &str = "did:key:bob";
const CAROL: &str = "did:key:carol";
const DAN: &str = "did:key:dan";
fn now() -> DateTime<Utc> {
"2026-09-23T12:00:00Z".parse().unwrap()
}
fn row(subject: &str, right: Right) -> RightRow {
RightRow {
subject: subject.into(),
right,
granted_by: ALICE.into(),
granted_at: now(),
expires_at: None,
reason: None,
subject_was_member: true,
granter_was_member: true,
break_glass: None,
review: None,
single_admin: None,
}
}
fn snap() -> Snapshot {
let mut s = Snapshot::default();
s.namespaces.push(Namespace {
id: "ns1".into(),
forge: "github.com".into(),
owner: "acme".into(),
mode: Mode::Manual,
state: NamespaceState::Bound,
owner_id: None,
kind: None,
bridge_did: None,
bind_job_id: None,
bound_by: ALICE.into(),
requested_at: now(),
bound_at: Some(now()),
roles_digest: None,
installation_removed: false,
forge_status: None,
role_map: None,
});
s.repos.push(Repo {
id: "r1".into(),
namespace_id: "ns1".into(),
resource: "github.com/acme/widgets".into(),
forge_id: None,
visibility: Visibility::Public,
description: None,
state: RepoState::Active,
created_by: None,
created_at: now(),
bootstrap: Bootstrap::default(),
sync: SyncStatus::new(SyncState::Unchecked),
failed_step: None,
last_error: None,
roles_digest: None,
forge_report: Default::default(),
});
s.rights.insert(
Scope::Namespace("ns1".into()),
RightsSet {
rows: vec![row(ALICE, Right::NsAdmin), row(BOB, Right::RepoCreate)],
},
);
s.rights.insert(
Scope::Repo("r1".into()),
RightsSet {
rows: vec![row(CAROL, Right::RepoOwn), row(DAN, Right::CommitSign)],
},
);
s
}
fn res(s: &str) -> Resource {
Resource::parse(s).unwrap()
}
fn grant(actor: &str, right: Right, target: &str) -> Result<(), Refusal> {
authority_to_grant(
&snap(),
actor,
right,
&res(target),
RuleSettings::default(),
now(),
)
}
#[test]
fn a_namespace_admin_may_grant_everything_in_the_namespace() {
for (right, target) in [
(Right::NsAdmin, "github.com/acme"),
(Right::RepoCreate, "github.com/acme"),
(Right::CommitSign, "github.com/acme"),
(Right::RepoOwn, "github.com/acme/widgets"),
(Right::RepoMaintain, "github.com/acme/any"),
(Right::CommitSign, "github.com/acme/widgets"),
] {
assert_eq!(grant(ALICE, right, target), Ok(()), "{right} on {target}");
}
}
#[test]
fn repo_create_is_not_re_delegable() {
assert!(matches!(
grant(BOB, Right::RepoCreate, "github.com/acme"),
Err(Refusal::Escalation(_))
));
}
#[test]
fn an_owner_grants_within_their_repository_only() {
for right in [Right::RepoOwn, Right::RepoMaintain, Right::CommitSign] {
assert_eq!(grant(CAROL, right, "github.com/acme/widgets"), Ok(()));
}
assert!(matches!(
grant(CAROL, Right::CommitSign, "github.com/acme"),
Err(Refusal::ScopeViolation(_))
));
assert!(matches!(
grant(CAROL, Right::CommitSign, "github.com/acme/gadgets"),
Err(Refusal::ScopeViolation(_))
));
assert!(matches!(
grant(CAROL, Right::NsAdmin, "github.com/acme"),
Err(Refusal::ScopeViolation(_))
));
}
#[test]
fn a_right_never_crosses_forges_or_prefix_lookalikes() {
for target in ["codeberg.org/acme/widgets", "github.com/acme-labs/x"] {
assert!(matches!(
grant(ALICE, Right::CommitSign, target),
Err(Refusal::PermissionDenied(_))
));
}
}
#[test]
fn the_level_must_match_the_right() {
assert!(matches!(
grant(ALICE, Right::RepoOwn, "github.com/acme"),
Err(Refusal::ScopeViolation(_))
));
assert!(matches!(
grant(ALICE, Right::NsAdmin, "github.com/acme/widgets"),
Err(Refusal::ScopeViolation(_))
));
}
#[test]
fn a_committer_grants_nothing_and_a_maintainer_only_when_configured() {
assert!(matches!(
grant(DAN, Right::CommitSign, "github.com/acme/widgets"),
Err(Refusal::Escalation(_))
));
let mut s = snap();
s.rights
.get_mut(&Scope::Repo("r1".into()))
.unwrap()
.rows
.push(row(BOB, Right::RepoMaintain));
let target = res("github.com/acme/widgets");
assert!(matches!(
authority_to_grant(
&s,
BOB,
Right::CommitSign,
&target,
RuleSettings::default(),
now()
),
Err(Refusal::Escalation(_))
));
let widened = RuleSettings {
maintainer_grants_commit: true,
};
assert_eq!(
authority_to_grant(&s, BOB, Right::CommitSign, &target, widened, now()),
Ok(())
);
assert!(authority_to_grant(&s, BOB, Right::RepoMaintain, &target, widened, now()).is_err());
}
#[test]
fn a_lapsed_right_carries_no_authority() {
let mut s = snap();
s.rights.get_mut(&Scope::Repo("r1".into())).unwrap().rows[0].expires_at =
Some("2026-09-01T00:00:00Z".parse().unwrap());
assert!(
authority_to_grant(
&s,
CAROL,
Right::CommitSign,
&res("github.com/acme/widgets"),
RuleSettings::default(),
now()
)
.is_err()
);
}
#[test]
fn implication_is_evaluated_and_ns_admin_implies_namespace_commit() {
let s = snap();
let on_ns = effective_on(&s, ALICE, &res("github.com/acme"), now());
assert!(on_ns.contains(&Right::CommitSign), "trust-tasks-tf#623");
assert!(on_ns.contains(&Right::RepoCreate));
let on_repo = effective_on(&s, ALICE, &res("github.com/acme/widgets"), now());
assert!(on_repo.contains(&Right::RepoOwn));
let carol = effective_on(&s, CAROL, &res("github.com/acme/widgets"), now());
assert_eq!(
carol,
BTreeSet::from([Right::RepoOwn, Right::RepoMaintain, Right::CommitSign])
);
}
#[test]
fn revoke_is_open_to_the_subject_and_to_authority_only() {
let s = snap();
let dan = row(DAN, Right::CommitSign);
let widgets = res("github.com/acme/widgets");
let st = RuleSettings::default();
assert_eq!(
authority_to_revoke(&s, DAN, &dan, &widgets, st, now()),
Ok(())
);
assert_eq!(
authority_to_revoke(&s, CAROL, &dan, &widgets, st, now()),
Ok(())
);
assert_eq!(
authority_to_revoke(&s, ALICE, &dan, &widgets, st, now()),
Ok(())
);
assert!(matches!(
authority_to_revoke(&s, BOB, &dan, &widgets, st, now()),
Err(Refusal::Escalation(_))
));
let ns_commit = row(DAN, Right::CommitSign);
assert!(matches!(
authority_to_revoke(&s, CAROL, &ns_commit, &res("github.com/acme"), st, now()),
Err(Refusal::PermissionDenied(_))
));
}
#[test]
fn last_owner_and_last_admin_are_detected() {
let s = snap();
assert!(is_last_owner(&s, "r1", CAROL, now()));
assert!(!is_last_owner(&s, "r1", DAN, now()));
assert!(is_last_admin(&s, "ns1", ALICE, now()));
assert!(!is_last_admin(&s, "ns1", BOB, now()));
}
#[test]
fn elevated_rights_go_to_and_from_members_only() {
for right in [Right::NsAdmin, Right::RepoCreate, Right::RepoOwn] {
assert!(
members_only(right, false, true).is_err(),
"{right} to a non-member"
);
assert!(
members_only(right, true, false).is_err(),
"{right} by a non-member"
);
assert!(
members_only(right, true, true).is_ok(),
"{right} member to member"
);
}
assert!(members_only(Right::CommitSign, false, false).is_ok());
assert!(members_only(Right::RepoMaintain, false, false).is_ok());
}
#[test]
fn a_waiver_lifts_rule_7_for_exactly_the_self_grant_it_names() {
let s = snap();
let widgets = res("github.com/acme/widgets");
let st = RuleSettings::default();
let admit = |subject: &str, right: Right, target: &Resource, w: Option<&Waivable>| {
grant_admitted(&s, ALICE, subject, right, target, true, true, st, now(), w)
};
assert!(matches!(
admit(ALICE, Right::RepoOwn, &widgets, None),
Err(Refusal::SelfGrant(_))
));
let w = Waivable::for_test(ALICE, Right::RepoOwn, widgets.clone());
assert!(admit(ALICE, Right::RepoOwn, &widgets, Some(&w)).is_ok());
assert!(matches!(
admit(ALICE, Right::NsAdmin, &res("github.com/acme"), Some(&w)),
Err(Refusal::SelfGrant(_))
));
assert!(matches!(
admit(
ALICE,
Right::RepoOwn,
&res("github.com/acme/gadgets"),
Some(&w)
),
Err(Refusal::SelfGrant(_))
));
let bobs = Waivable::for_test(BOB, Right::RepoOwn, widgets.clone());
assert!(matches!(
admit(ALICE, Right::RepoOwn, &widgets, Some(&bobs)),
Err(Refusal::SelfGrant(_))
));
assert!(matches!(
grant_admitted(
&s,
ALICE,
ALICE,
Right::RepoOwn,
&widgets,
false,
true,
st,
now(),
Some(&w)
),
Err(Refusal::MembersOnly(_))
));
let bob_w = Waivable::for_test(BOB, Right::RepoOwn, widgets.clone());
assert!(
grant_admitted(
&s,
BOB,
BOB,
Right::RepoOwn,
&widgets,
true,
true,
st,
now(),
Some(&bob_w)
)
.is_err()
);
}
#[test]
fn a_waived_record_counts_for_the_invariants_and_an_unratified_break_glass_does_not() {
let mut waived = row(ALICE, Right::RepoOwn);
waived.single_admin = Some(crate::git_ns::model::SingleAdminMark {
at: now(),
task: "https://trusttasks.org/spec/git-ns/repo/adopt/0.1".into(),
});
assert!(waived.counts_for_invariants(now()));
let mut bg = row(ALICE, Right::RepoOwn);
bg.break_glass = Some(crate::git_ns::model::BreakGlassMark {
by: ALICE.into(),
at: now(),
justification: "x".into(),
effective_at: None,
ratified_by: None,
ratified_at: None,
});
assert!(!bg.counts_for_invariants(now()));
}
#[test]
fn a_service_grant_is_admitted_only_to_the_recorded_bridge_of_a_bound_namespace() {
let mut ns = snap().namespaces[0].clone();
ns.bridge_did = Some(DAN.into());
let own = ns.resource();
let repo = Resource::parse("github.com/acme/widgets").unwrap();
assert!(service_grant_admitted(&ns, DAN, Right::CommitSign, &own).is_ok());
assert!(service_grant_admitted(&ns, BOB, Right::CommitSign, &own).is_err());
assert!(service_grant_admitted(&ns, DAN, Right::RepoOwn, &own).is_err());
assert!(service_grant_admitted(&ns, DAN, Right::CommitSign, &repo).is_err());
let other = Resource::parse("github.com/beta").unwrap();
assert!(service_grant_admitted(&ns, DAN, Right::CommitSign, &other).is_err());
ns.state = NamespaceState::Pending;
assert!(service_grant_admitted(&ns, DAN, Right::CommitSign, &own).is_err());
ns.state = NamespaceState::Bound;
ns.bridge_did = None;
assert!(service_grant_admitted(&ns, DAN, Right::CommitSign, &own).is_err());
}
}