Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/development/routing-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ state and the remaining validation work.
| Surface | Current implementation | Target status |
| --- | --- | --- |
| Queue concrete route depth | Rejects trailing segments | Aligned |
| Lease concrete route depth | Uses the first three segments and tolerates a trailing suffix | Gap: reject trailing segments |
| Lease concrete route depth | Rejects trailing segments | Aligned |
| Stream selector classification | All eight literal/`*` kinds and two aliases | Aligned |
| Resource, area, and realm reads | Dedicated offset spaces and visibility frontiers | Aligned |
| Global and global-filter reads | Direct family-global pages in assigned commit order | Aligned |
Expand Down
20 changes: 10 additions & 10 deletions src/api/runtime_ingress/domain_frame_dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -722,18 +722,18 @@ impl DomainFrameDispatcher<'_> {
"RPC request parse failed",
);
}
// Domain validation owns Lease observation selector errors:
// SUBSCRIBE/UNSUBSCRIBE return 5010 and LIST returns 5012,
// including on auth-required brokers. Dispatching these
// malformed selectors is safe because the Lease sink compiles
// and rejects them before retaining subscription state or
// inspecting inventory. Other auth-required domains keep the
// fail-closed behavior below.
let lease_observation_validation = matches!(
// Domain validation owns malformed Lease request errors:
// exact operations return 5008, SUBSCRIBE/UNSUBSCRIBE return
// 5010, and LIST returns 5012, including on auth-required
// brokers. Dispatching these malformed routes is safe because
// the Lease decoder rejects them before ownership, retained
// subscription state, or inventory can be touched. Other
// auth-required domains keep the fail-closed behavior below.
let lease_request_validation = matches!(
(dispatch.domain, dispatch.msg_type.as_u16()),
(DispatchDomain::Lease, 407 | 408 | 410)
(DispatchDomain::Lease, 400..=403 | 407 | 408 | 410)
);
if lease_observation_validation
if lease_request_validation
|| (!self.ingress.auth_required
&& is_subscription_registration_message(
dispatch.domain,
Expand Down
82 changes: 82 additions & 0 deletions src/api/runtime_ingress/tests/authorization_routes_lease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,88 @@ async fn malformed_authenticated_lease_observation(msg_type: u16) -> (IngressDec
)
}

async fn malformed_authenticated_lease_operation(
msg_type: u16,
payload: Bytes,
) -> (IngressDecision, u16) {
let family = RouteFamily::new(1);
let session_id = 700 + u64::from(msg_type);
let router = Arc::new(crate::runtime::Router::new());
let lease_sink = Arc::new(crate::domains::lease::sink::LeaseDomainSink::new(
router.clone(),
crate::control::admin::read_model::AdminReadModel::new(),
));
let inbox_mailbox = Arc::new(Mailbox::new(8));
router.register_domain_pattern("lease", lease_sink);
router.register(
RouteAddress::new(family, Route::new(format!("inbox://session/{session_id}"))),
inbox_mailbox.clone(),
);
let ingress = runtime_ingress_with_jwks_auth().with_router(router);
let session =
make_authenticated_session_info(session_id, TransportKind::Tcp, family, &["lease://**#*"]);
ingress.on_open(session).await.unwrap();

let decision = ingress
.on_frame(
session_id,
ChannelId::Lease,
MessageType::new(msg_type),
payload,
)
.await;
let response = tokio::time::timeout(std::time::Duration::from_secs(1), async {
loop {
if !inbox_mailbox.receiver().is_empty() {
break receive_frame(&inbox_mailbox, "typed malformed Lease operation response");
}
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
})
.await
.expect("typed malformed Lease operation response");
(
decision,
decode_domain_error_code(response.payload.as_ref()),
)
}

#[tokio::test]
async fn should_return_typed_error_for_authenticated_malformed_lease_operations() {
// Arrange
let malformed = "lease://acme/locks/resource/extra";
let mut renew = PayloadEncoder::new();
renew.put_string(malformed);
renew.put_string("owner");
renew.put_u64(1);
renew.put_u64(30);
let mut release = PayloadEncoder::new();
release.put_string(malformed);
release.put_string("owner");
release.put_u64(1);
let mut query = PayloadEncoder::new();
query.put_string(malformed);
let requests = [
(400, encode_lease_acquire(malformed, "owner", 30)),
(401, Bytes::from(renew.finish())),
(402, Bytes::from(release.finish())),
(403, Bytes::from(query.finish())),
];

for (msg_type, payload) in requests {
// Act
let (decision, code) = malformed_authenticated_lease_operation(msg_type, payload).await;

// Assert
assert_eq!(decision, IngressDecision::Accept);
assert_eq!(
code,
crate::protocol::error_codes::lease::ERR_BAD_REQUEST,
"message type {msg_type}"
);
}
}

#[tokio::test]
async fn should_return_typed_error_for_authenticated_malformed_lease_subscribe() {
// Arrange
Expand Down
8 changes: 4 additions & 4 deletions src/domains/lease/sink/ingress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ impl LeaseDomainRuntime<'_> {
channel: meta.channel,
route_family: meta.route_family,
}),
None => LeaseResponse::NotFound,
None => Self::error_response("invalid lease route"),
}
}
LeaseMessage::Extend {
Expand All @@ -332,7 +332,7 @@ impl LeaseDomainRuntime<'_> {
*fencing_token,
*ttl_secs,
),
None => LeaseResponse::NotFound,
None => Self::error_response("invalid lease route"),
},
LeaseMessage::Release {
family_id,
Expand All @@ -345,12 +345,12 @@ impl LeaseDomainRuntime<'_> {
scoped_owner_id.expect("release owner must be scoped before dispatch"),
*fencing_token,
),
None => LeaseResponse::NotFound,
None => Self::error_response("invalid lease route"),
},
LeaseMessage::Query { family_id, route } => {
match LeaseKey::from_route(*family_id, route) {
Some(key) => self.handle_query(&key),
None => LeaseResponse::NotFound,
None => Self::error_response("invalid lease route"),
}
}
LeaseMessage::List {
Expand Down