fix(shared): F-Q-004 unify error variants — Forbidden on QueueError, rename Unavailable→Backend

Brings QueueError / PubsubError / InvokeError in line with sibling
shape used by KvError / DocsError / FilesError / SecretsError:

- QueueError gains an explicit Forbidden variant (previously authz
  denial was squashed into Rejected("forbidden") in queue_service.rs:68,
  losing the structured variant a 403-translation layer would need).
- QueueError::Unavailable renamed → Backend.
- PubsubError::Unavailable renamed → Backend.
- InvokeError::Unavailable renamed → Backend.
- Call sites updated (queue_service, pubsub_service, invoke_service,
  Noop* stubs, From<PubsubRepoError> impl, one test assertion).
- New unit test verifying authed-denied → QueueError::Forbidden.

AUDIT.md anchor: F-Q-004.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-07 19:44:23 +02:00
parent 450badaabf
commit b192cf2cc9
6 changed files with 78 additions and 25 deletions

View File

@@ -65,7 +65,7 @@ impl QueueService for QueueServiceImpl {
Capability::AppQueueEnqueue(cx.app_id),
)
.await
.map_err(|_| QueueError::Rejected("forbidden".into()))?;
.map_err(|_| QueueError::Forbidden)?;
}
let deliver_after = opts
@@ -88,7 +88,7 @@ impl QueueService for QueueServiceImpl {
enqueued_by_principal,
})
.await
.map_err(|e| QueueError::Unavailable(e.to_string()))
.map_err(|e| QueueError::Backend(e.to_string()))
}
async fn depth(&self, cx: &SdkCallCx, queue_name: &str) -> Result<u64, QueueError> {
@@ -98,7 +98,7 @@ impl QueueService for QueueServiceImpl {
self.repo
.depth(cx.app_id, queue_name)
.await
.map_err(|e| QueueError::Unavailable(e.to_string()))
.map_err(|e| QueueError::Backend(e.to_string()))
}
async fn depth_pending(&self, cx: &SdkCallCx, queue_name: &str) -> Result<u64, QueueError> {
@@ -108,7 +108,7 @@ impl QueueService for QueueServiceImpl {
self.repo
.depth_pending(cx.app_id, queue_name)
.await
.map_err(|e| QueueError::Unavailable(e.to_string()))
.map_err(|e| QueueError::Backend(e.to_string()))
}
}
@@ -312,6 +312,49 @@ mod tests {
assert!(captured.deliver_after.is_none());
}
struct DenyAllAuthz;
#[async_trait]
impl AuthzRepo for DenyAllAuthz {
async fn membership(
&self,
_user_id: picloud_shared::UserId,
_app_id: AppId,
) -> Result<Option<AppRole>, AuthzError> {
Ok(None)
}
}
#[tokio::test]
async fn authed_principal_denied_returns_forbidden_variant() {
use picloud_shared::{InstanceRole, Principal, UserId};
let repo = Arc::new(CapturingRepo {
last: tokio::sync::Mutex::new(None),
});
let svc = QueueServiceImpl::new(repo, Arc::new(DenyAllAuthz));
let exec = ExecutionId::new();
let cx = SdkCallCx {
app_id: AppId::new(),
script_id: ScriptId::new(),
principal: Some(Principal {
user_id: UserId::new(),
instance_role: InstanceRole::Member,
scopes: None,
app_binding: None,
}),
execution_id: exec,
request_id: RequestId::new(),
trigger_depth: 0,
root_execution_id: exec,
is_dead_letter_handler: false,
event: None,
};
let err = svc
.enqueue(&cx, "x", serde_json::Value::Null, EnqueueOpts::default())
.await
.unwrap_err();
assert!(matches!(err, QueueError::Forbidden));
}
#[tokio::test]
async fn depth_passes_through() {
let repo = Arc::new(CapturingRepo {