fix(relay): preserve refusal accounting across live admission

Restore audio stored-owner failure accounting and keep live policy revalidation separate from deny-set revocation. Preserve denied-only priority over weak removal claims; cover denied and unavailable root closes.

Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz>
Signed-off-by: Brad Seiler <seiler@squareup.com>
This commit is contained in:
Brad Seiler
2026-11-04 10:52:35 -05:00
parent 8b8321c280
commit 548952b87d
2 changed files with 85 additions and 3 deletions

View File

@@ -1108,6 +1108,12 @@ async fn run_active_audio_connection(
let admitted_owner = match admitted_owner {
Ok(owner) => owner,
Err(denial) => {
crate::handlers::auth::record_enforce_refusal(
&control,
Some(&denial),
nip_fi_assertion.as_ref(),
);
auth_line.finish(AuthResult::from_outcome(denial.outcome));
shadow_attempt.refused();
exit_authorization_refusal(
&mut ws_send,

View File

@@ -392,7 +392,13 @@ impl CommunityConnectionControl {
CommunityDisconnectReason::AuthorizationDenied
};
let won = self.reason_tx.send_if_modified(|current| match current {
None | Some(CommunityDisconnectReason::Removed) => {
None => {
*current = Some(reason);
true
}
Some(CommunityDisconnectReason::Removed)
if class == buzz_auth::DenialClass::AuthorizationDenied =>
{
*current = Some(reason);
true
}
@@ -444,6 +450,7 @@ impl CommunityConnectionControl {
self.manager_disconnect_nip_fi_with_class(
frame_tx,
buzz_auth::DenialClass::AuthorizationDenied,
crate::nip_fi_shadow::EnforceSession::revoke,
);
}
@@ -451,12 +458,13 @@ impl CommunityConnectionControl {
&self,
frame_tx: &mpsc::Sender<WsMessage>,
class: buzz_auth::DenialClass,
enforce: fn(&crate::nip_fi_shadow::EnforceSession),
) {
self.publish_nip_fi_denial(
crate::nip_fi_session::NipFiWsRoute::Root,
class,
Some(frame_tx),
crate::nip_fi_shadow::EnforceSession::revoke,
enforce,
);
self.cancel.cancel();
}
@@ -1609,7 +1617,7 @@ impl ConnectionManager {
if nip_fi {
entry
.community_control
.manager_disconnect_nip_fi_with_class(&entry.terminal_ctrl_tx, class);
.manager_disconnect_nip_fi_with_class(&entry.terminal_ctrl_tx, class, |_| {});
} else {
entry
.community_control
@@ -3663,6 +3671,74 @@ pub(crate) mod tests {
);
}
/// Live policy failures close root sockets without attributing them to
/// the NIP-FI deny set, just as the audio registry does.
#[test]
fn live_revalidation_does_not_record_deny_set_revocation() {
for class in [
buzz_auth::DenialClass::AuthorizationDenied,
buzz_auth::DenialClass::AuthorizationUnavailable,
] {
let (records, lines, _, ()) =
crate::nip_fi_shadow::tests::enforce_observed(async {}, |()| async move {
let manager = ConnectionManager::new();
let community = buzz_core::CommunityId::from_uuid(Uuid::new_v4());
let conn_id = Uuid::new_v4();
let key = nostr::Keys::generate().public_key();
let (tx, _rx) = mpsc::channel(8);
let (ctrl_tx, _ctrl_rx) = mpsc::channel(8);
let (terminal_tx, mut terminal_rx) = mpsc::channel(1);
let cancel = CancellationToken::new();
let control = CommunityConnectionControl::new(cancel.clone());
let reason = control.disconnect_reason();
control.attach_nip_fi_enforce(crate::nip_fi_shadow::EnforceSession::new(
"ws",
crate::nip_fi_shadow::BOUND.into(),
Some(key),
));
control.nip_fi_enforce().expect("attached").admit(key);
manager.register(
conn_id,
tx,
ctrl_tx,
terminal_tx,
None,
cancel.clone(),
community,
Arc::new(AtomicU8::new(0)),
Arc::new(Mutex::new(HashMap::new())),
3,
control,
);
manager.set_authenticated_identity(
conn_id,
key.to_bytes().to_vec(),
Some(NipFiSession::new("test-issuer", None)),
);
assert!(manager.disconnect_live_authorization(community, conn_id, class));
assert!(cancel.is_cancelled());
assert_eq!(
*reason.borrow(),
Some(
if class == buzz_auth::DenialClass::AuthorizationUnavailable {
CommunityDisconnectReason::AuthorizationUnavailable
} else {
CommunityDisconnectReason::AuthorizationDenied
}
),
);
assert!(terminal_rx.try_recv().is_ok(), "terminal denial frame");
});
assert_eq!(records.iter().filter(|r| *r == "ws admit/admit").count(), 1);
assert!(
records
.iter()
.all(|r| !r.contains("end/") && !r.contains("deny/")),
"{class:?} must not be attributed to the deny set: {lines:?}"
);
}
}
#[test]
fn nip_fi_disconnect_audio_is_issuer_scoped() {
let registry = CommunityConnectionRegistry::new();