use serde_json::{Value, json};
use tracing::warn;
use vti_common::audit::{AuditEvent, SingleAdminModeData};
use vti_common::error::AppError;
use crate::acl::admin_consent;
use crate::acl::bound_step_up::{self, EvidencedGate};
use crate::server::AppState;
use super::model::{Namespace, Resource, Right, RightRow, SingleAdminMark};
use super::ops::{self, Audit, OpError, OpResult, Standing};
use super::rules::RuleSettings;
use super::store::Snapshot;
pub const REQUIREMENT: &str = "git-ns/right/grant/0.3#rule-7";
pub const AUDIT_EVENT: &str = "selfGrantWaived";
pub const EXT_MARKER: &str = "selfGrantWaived";
pub const ACTIVITY_ACTION: &str = "gitNs.right.selfGrantWaived";
#[derive(Debug, Clone)]
pub struct Waivable {
actor: String,
right: Right,
resource: Resource,
}
impl Waivable {
pub fn covers(&self, actor: &str, subject: &str, right: Right, target: &Resource) -> bool {
self.actor == actor && subject == actor && self.right == right && self.resource == *target
}
pub fn right(&self) -> Right {
self.right
}
pub fn resource(&self) -> &Resource {
&self.resource
}
#[cfg(test)]
pub(crate) fn for_test(actor: &str, right: Right, resource: Resource) -> Self {
Self {
actor: actor.to_string(),
right,
resource,
}
}
}
pub async fn waivable(
state: &AppState,
_snap: &Snapshot,
_ns: &Namespace,
actor: &Standing,
right: Right,
target: &Resource,
_settings: RuleSettings,
) -> Result<Option<Waivable>, AppError> {
if !admin_consent::single_admin_mode(state).await || !actor.member {
return Ok(None);
}
Ok(Some(Waivable {
actor: actor.did.clone(),
right,
resource: target.clone(),
}))
}
#[derive(Debug, Clone, Copy)]
pub struct WaivedOp<'a> {
pub type_uri: &'a str,
pub payload: &'a Value,
pub kind: &'a str,
}
pub async fn authorize(
state: &AppState,
waiver: &Waivable,
op: WaivedOp<'_>,
) -> OpResult<SingleAdminMark> {
let (right, resource) = (waiver.right, &waiver.resource);
let reason = format!(
"SINGLE-ADMINISTRATOR MODE: record {right} on {resource} for yourself. Nobody else in \
this community could grant it, so your passkey gesture stands in for a second \
administrator; it is audited."
);
match bound_step_up::redeem_or_request_with_evidence(
state,
&waiver.actor,
op.type_uri,
op.payload,
&reason,
)
.await?
{
EvidencedGate::Satisfied(_) => {}
EvidencedGate::Required(request) => {
return Err(OpError::StepUpRequired {
message: format!(
"single-administrator mode: nobody else can grant {right} on {resource}, so \
recording it for yourself needs a passkey gesture bound to this request \
(VTI-APV-022) in place of another administrator"
),
request,
});
}
}
let digest = vti_common::task_consent::payload_digest(op.type_uri, op.payload)?;
warn!(
actor = %waiver.actor,
right = %right,
resource = %resource,
task = %op.type_uri,
"separation of duties waived — single-administrator mode (VTI-APV-022): nobody else \
could grant this, and the requester's operation-bound step-up stands in"
);
if let Some(writer) = state.audit_writer.as_ref() {
writer
.write(
&waiver.actor,
Some(&waiver.actor),
AuditEvent::SingleAdminMode(SingleAdminModeData {
event: AUDIT_EVENT.into(),
requirement: Some(REQUIREMENT.into()),
task: Some(op.type_uri.to_string()),
digest: Some(digest),
kind: Some(op.kind.to_string()),
right: Some(right.as_str().to_string()),
resource: Some(resource.to_string()),
}),
)
.await
.map_err(|e| {
tracing::error!(
error = %e,
"the single-administrator waiver's audit row could not be written; refusing"
);
OpError::Internal(e)
})?;
}
Ok(SingleAdminMark {
at: ops::now(),
task: op.type_uri.to_string(),
})
}
pub async fn note(state: &AppState, waiver: &Waivable, ns: &Namespace, kind: &str) {
ops::audit(
state,
&waiver.actor,
Some(&waiver.actor),
Audit {
action: ACTIVITY_ACTION,
namespace: Some(&ns.id),
resource: Some(waiver.resource.to_string()),
right: Some(waiver.right),
policy_version: None,
detail: Some(kind.to_string()),
},
)
.await;
}
pub fn response_ext(waiver: &Waivable) -> Value {
json!({
"org.openvtc": {
EXT_MARKER: {
"mode": "singleAdministrator",
"requirement": REQUIREMENT,
"right": waiver.right.as_str(),
"resource": waiver.resource.to_string(),
}
}
})
}
pub fn view_entry(row: &RightRow, resource: &Resource) -> Option<Value> {
let mark = row.single_admin.as_ref()?;
Some(json!({
"subject": row.subject,
"right": row.right.as_str(),
"resource": resource.to_string(),
"at": super::wire::timestamp(mark.at),
"task": mark.task,
}))
}