1use aion::EventFilter;
4use aion_core::{RunId, ScheduleId, WorkflowFilter, WorkflowId};
5use aion_proto::{
6 FilteredSubscription, FirehoseSubscription, PerWorkflowSubscription, ProtoCancelRequest,
7 ProtoCountWorkflowsRequest, ProtoCreateScheduleRequest, ProtoDescribeWorkflowRequest,
8 ProtoListSchedulesRequest, ProtoListWorkflowsRequest, ProtoQueryRequest, ProtoRegisterWorker,
9 ProtoScheduleIdRequest, ProtoSignalRequest, ProtoStartWorkflowRequest,
10 ProtoUpdateScheduleRequest, SubscriptionRequest, subscription_request,
11};
12
13use crate::error::ServerError;
14
15use super::resolver::{CallerIdentity, NamespaceResolver, ScopedEngine};
16
17#[derive(Clone)]
19pub struct NamespaceGuard {
20 resolver: NamespaceResolver,
21}
22
23impl NamespaceGuard {
24 #[must_use]
26 pub const fn new(resolver: NamespaceResolver) -> Self {
27 Self { resolver }
28 }
29
30 #[must_use]
32 pub const fn resolver(&self) -> &NamespaceResolver {
33 &self.resolver
34 }
35
36 pub async fn scope(
51 &self,
52 caller: &CallerIdentity,
53 operation: &NamespaceOperation<'_>,
54 ) -> Result<ScopedEngine, ServerError> {
55 let requested_namespace = operation.requested_namespace();
56 let scoped = self.resolver.resolve(caller, requested_namespace)?;
57 operation.verify(&self.resolver, scoped.namespace()).await?;
58 Ok(scoped)
59 }
60}
61
62pub enum NamespaceOperation<'a> {
64 StartWorkflow(&'a ProtoStartWorkflowRequest),
66 Signal(&'a ProtoSignalRequest, WorkflowTarget<'a>),
68 Query(&'a ProtoQueryRequest, WorkflowTarget<'a>),
70 Cancel(&'a ProtoCancelRequest, WorkflowTarget<'a>),
72 ListWorkflows(&'a ProtoListWorkflowsRequest, &'a WorkflowFilter),
74 CountWorkflows(&'a ProtoCountWorkflowsRequest),
76 Describe(&'a ProtoDescribeWorkflowRequest, WorkflowTarget<'a>),
78 CreateSchedule(&'a ProtoCreateScheduleRequest),
80 UpdateSchedule(&'a ProtoUpdateScheduleRequest, ScheduleTarget<'a>),
82 PauseSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
84 ResumeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
86 DeleteSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
88 ListSchedules(&'a ProtoListSchedulesRequest),
90 DescribeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
92 Subscribe(SubscriptionScope<'a>, &'a EventFilter),
94 RegisterWorker(&'a ProtoRegisterWorker),
96}
97
98impl<'a> NamespaceOperation<'a> {
99 #[must_use]
101 pub const fn start(request: &'a ProtoStartWorkflowRequest) -> Self {
102 Self::StartWorkflow(request)
103 }
104
105 #[must_use]
107 pub const fn signal(request: &'a ProtoSignalRequest, target: WorkflowTarget<'a>) -> Self {
108 Self::Signal(request, target)
109 }
110
111 #[must_use]
113 pub const fn query(request: &'a ProtoQueryRequest, target: WorkflowTarget<'a>) -> Self {
114 Self::Query(request, target)
115 }
116
117 #[must_use]
119 pub const fn cancel(request: &'a ProtoCancelRequest, target: WorkflowTarget<'a>) -> Self {
120 Self::Cancel(request, target)
121 }
122
123 #[must_use]
125 pub const fn list(request: &'a ProtoListWorkflowsRequest, filter: &'a WorkflowFilter) -> Self {
126 Self::ListWorkflows(request, filter)
127 }
128
129 #[must_use]
131 pub const fn count(request: &'a ProtoCountWorkflowsRequest) -> Self {
132 Self::CountWorkflows(request)
133 }
134
135 #[must_use]
137 pub const fn describe(
138 request: &'a ProtoDescribeWorkflowRequest,
139 target: WorkflowTarget<'a>,
140 ) -> Self {
141 Self::Describe(request, target)
142 }
143
144 #[must_use]
146 pub const fn create_schedule(request: &'a ProtoCreateScheduleRequest) -> Self {
147 Self::CreateSchedule(request)
148 }
149
150 #[must_use]
152 pub const fn update_schedule(
153 request: &'a ProtoUpdateScheduleRequest,
154 target: ScheduleTarget<'a>,
155 ) -> Self {
156 Self::UpdateSchedule(request, target)
157 }
158
159 #[must_use]
161 pub const fn pause_schedule(
162 request: &'a ProtoScheduleIdRequest,
163 target: ScheduleTarget<'a>,
164 ) -> Self {
165 Self::PauseSchedule(request, target)
166 }
167
168 #[must_use]
170 pub const fn resume_schedule(
171 request: &'a ProtoScheduleIdRequest,
172 target: ScheduleTarget<'a>,
173 ) -> Self {
174 Self::ResumeSchedule(request, target)
175 }
176
177 #[must_use]
179 pub const fn delete_schedule(
180 request: &'a ProtoScheduleIdRequest,
181 target: ScheduleTarget<'a>,
182 ) -> Self {
183 Self::DeleteSchedule(request, target)
184 }
185
186 #[must_use]
188 pub const fn list_schedules(request: &'a ProtoListSchedulesRequest) -> Self {
189 Self::ListSchedules(request)
190 }
191
192 #[must_use]
194 pub const fn describe_schedule(
195 request: &'a ProtoScheduleIdRequest,
196 target: ScheduleTarget<'a>,
197 ) -> Self {
198 Self::DescribeSchedule(request, target)
199 }
200
201 #[must_use]
203 pub const fn subscribe(scope: SubscriptionScope<'a>, filter: &'a EventFilter) -> Self {
204 Self::Subscribe(scope, filter)
205 }
206
207 #[must_use]
209 pub const fn register_worker(request: &'a ProtoRegisterWorker) -> Self {
210 Self::RegisterWorker(request)
211 }
212
213 fn requested_namespace(&self) -> &str {
214 match self {
215 Self::StartWorkflow(request) => request.namespace.as_str(),
216 Self::Signal(request, _target) => request.namespace.as_str(),
217 Self::Query(request, _target) => request.namespace.as_str(),
218 Self::Cancel(request, _target) => request.namespace.as_str(),
219 Self::ListWorkflows(request, _filter) => request.namespace.as_str(),
220 Self::CountWorkflows(request) => request.namespace.as_str(),
221 Self::Describe(request, _target) => request.namespace.as_str(),
222 Self::CreateSchedule(request) => request.namespace.as_str(),
223 Self::UpdateSchedule(request, _target) => request.namespace.as_str(),
224 Self::PauseSchedule(request, _target)
225 | Self::ResumeSchedule(request, _target)
226 | Self::DeleteSchedule(request, _target)
227 | Self::DescribeSchedule(request, _target) => request.namespace.as_str(),
228 Self::ListSchedules(request) => request.namespace.as_str(),
229 Self::Subscribe(scope, _filter) => scope.namespace(),
230 Self::RegisterWorker(request) => request.namespace.as_str(),
231 }
232 }
233
234 async fn verify(
235 &self,
236 resolver: &NamespaceResolver,
237 authorized_namespace: &str,
238 ) -> Result<(), ServerError> {
239 match self {
240 Self::Signal(_, target)
241 | Self::Query(_, target)
242 | Self::Cancel(_, target)
243 | Self::Describe(_, target) => target.verify(resolver, authorized_namespace).await,
244 Self::UpdateSchedule(_, target)
245 | Self::PauseSchedule(_, target)
246 | Self::ResumeSchedule(_, target)
247 | Self::DeleteSchedule(_, target)
248 | Self::DescribeSchedule(_, target) => {
249 target.verify(resolver, authorized_namespace).await
250 }
251 Self::Subscribe(scope, filter) => {
252 scope.verify(resolver, authorized_namespace, filter).await
253 }
254 Self::StartWorkflow(_)
261 | Self::ListWorkflows(_, _)
262 | Self::CountWorkflows(_)
263 | Self::CreateSchedule(_)
264 | Self::ListSchedules(_)
265 | Self::RegisterWorker(_) => Ok(()),
266 }
267 }
268}
269
270#[derive(Clone, Copy)]
272pub struct WorkflowTarget<'a> {
273 workflow_id: &'a WorkflowId,
274 run_id: Option<&'a RunId>,
275}
276
277impl<'a> WorkflowTarget<'a> {
278 #[must_use]
280 pub const fn with_run(workflow_id: &'a WorkflowId, run_id: &'a RunId) -> Self {
281 Self {
282 workflow_id,
283 run_id: Some(run_id),
284 }
285 }
286
287 #[must_use]
289 pub const fn workflow(workflow_id: &'a WorkflowId) -> Self {
290 Self {
291 workflow_id,
292 run_id: None,
293 }
294 }
295
296 #[must_use]
298 pub const fn workflow_id(&self) -> &WorkflowId {
299 self.workflow_id
300 }
301
302 #[must_use]
304 pub const fn run_id(&self) -> Option<&RunId> {
305 self.run_id
306 }
307
308 async fn verify(
309 &self,
310 resolver: &NamespaceResolver,
311 namespace: &str,
312 ) -> Result<(), ServerError> {
313 resolver
314 .verify_workflow_ownership(namespace, self.workflow_id)
315 .await
316 }
317}
318
319#[derive(Clone, Copy)]
321pub struct ScheduleTarget<'a> {
322 schedule_id: &'a ScheduleId,
323}
324
325impl<'a> ScheduleTarget<'a> {
326 #[must_use]
328 pub const fn schedule(schedule_id: &'a ScheduleId) -> Self {
329 Self { schedule_id }
330 }
331
332 #[must_use]
334 pub const fn schedule_id(&self) -> &ScheduleId {
335 self.schedule_id
336 }
337
338 async fn verify(
339 &self,
340 resolver: &NamespaceResolver,
341 namespace: &str,
342 ) -> Result<(), ServerError> {
343 resolver
344 .verify_schedule_ownership(namespace, self.schedule_id)
345 .await
346 }
347}
348
349pub enum SubscriptionScope<'a> {
351 PerWorkflow(&'a PerWorkflowSubscription, WorkflowTarget<'a>),
353 Filtered(&'a FilteredSubscription),
355 Firehose(&'a FirehoseSubscription),
357}
358
359impl<'a> SubscriptionScope<'a> {
360 pub fn from_request(
368 request: &'a SubscriptionRequest,
369 workflow_target: Option<WorkflowTarget<'a>>,
370 ) -> Result<Self, ServerError> {
371 match &request.subscription {
372 Some(subscription_request::Subscription::PerWorkflow(subscription)) => {
373 let target = workflow_target.ok_or_else(|| {
374 ServerError::namespace_denied(
375 "per-workflow subscription target must be decoded before guard scope",
376 )
377 })?;
378 Ok(Self::PerWorkflow(subscription, target))
379 }
380 Some(subscription_request::Subscription::Filtered(subscription)) => {
381 Ok(Self::Filtered(subscription))
382 }
383 Some(subscription_request::Subscription::Firehose(subscription)) => {
384 Ok(Self::Firehose(subscription))
385 }
386 None => Err(ServerError::namespace_denied(
387 "subscription request must name a namespace",
388 )),
389 }
390 }
391
392 fn namespace(&self) -> &str {
393 match self {
394 Self::PerWorkflow(subscription, _target) => subscription.namespace.as_str(),
395 Self::Filtered(subscription) => subscription.namespace.as_str(),
396 Self::Firehose(subscription) => subscription.namespace.as_str(),
397 }
398 }
399
400 async fn verify(
401 &self,
402 resolver: &NamespaceResolver,
403 namespace: &str,
404 filter: &EventFilter,
405 ) -> Result<(), ServerError> {
406 match self {
407 Self::PerWorkflow(_subscription, target) => {
408 verify_subscription_filter_target(filter, Some(*target), resolver, namespace).await
409 }
410 Self::Filtered(subscription) => {
411 verify_namespace_selector(subscription.namespace_selector.as_deref(), namespace)?;
412 verify_subscription_filter_target(filter, None, resolver, namespace).await
413 }
414 Self::Firehose(_) => {
415 verify_subscription_filter_target(filter, None, resolver, namespace).await
416 }
417 }
418 }
419}
420
421fn verify_namespace_selector(selector: Option<&str>, namespace: &str) -> Result<(), ServerError> {
422 match selector {
423 Some(selector) if selector != namespace => Err(ServerError::namespace_denied(
424 "subscription namespace selector is not authorized",
425 )),
426 Some(_) | None => Ok(()),
427 }
428}
429
430async fn verify_subscription_filter_target(
431 filter: &EventFilter,
432 explicit_target: Option<WorkflowTarget<'_>>,
433 resolver: &NamespaceResolver,
434 namespace: &str,
435) -> Result<(), ServerError> {
436 if let Some(target) = explicit_target {
437 if filter
438 .workflow_id
439 .as_ref()
440 .is_some_and(|workflow_id| workflow_id != target.workflow_id())
441 {
442 return Err(ServerError::namespace_denied(
443 "subscription filter workflow does not match decoded target",
444 ));
445 }
446 target.verify(resolver, namespace).await
447 } else if let Some(workflow_id) = &filter.workflow_id {
448 resolver
449 .verify_workflow_ownership(namespace, workflow_id)
450 .await
451 } else {
452 Ok(())
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use std::sync::atomic::{AtomicUsize, Ordering};
459 use std::sync::{Arc, Mutex};
460
461 use aion_core::{RunId, ScheduleId, WorkflowFilter, WorkflowId};
462 use aion_proto::{
463 FilteredSubscription, FirehoseSubscription, PerWorkflowSubscription, ProtoCancelRequest,
464 ProtoCreateScheduleRequest, ProtoDescribeWorkflowRequest, ProtoListSchedulesRequest,
465 ProtoListWorkflowsRequest, ProtoQueryRequest, ProtoRegisterWorker, ProtoScheduleIdRequest,
466 ProtoSignalRequest, ProtoStartWorkflowRequest, ProtoUpdateScheduleRequest,
467 };
468 use async_trait::async_trait;
469
470 use super::{
471 NamespaceGuard, NamespaceOperation, ScheduleTarget, SubscriptionScope, WorkflowTarget,
472 };
473 use crate::config::NamespaceMode;
474 use crate::error::ServerError;
475 use crate::namespace::{
476 CallerIdentity, NamespaceResolver, ScheduleNamespaceSource, StaticScheduleNamespaces,
477 StaticWorkflowNamespaces,
478 };
479
480 struct RecordingFakeEngine {
481 calls: Mutex<Vec<&'static str>>,
482 }
483
484 impl RecordingFakeEngine {
485 fn new() -> Self {
486 Self {
487 calls: Mutex::new(Vec::new()),
488 }
489 }
490
491 fn calls(&self) -> Result<Vec<&'static str>, Box<dyn std::error::Error>> {
492 let calls = self
493 .calls
494 .lock()
495 .map_err(|_| "fake engine calls lock poisoned")?;
496 Ok(calls.clone())
497 }
498 }
499
500 #[derive(Clone)]
505 struct CountingScheduleNamespaces {
506 inner: StaticScheduleNamespaces,
507 calls: Arc<AtomicUsize>,
508 }
509
510 impl CountingScheduleNamespaces {
511 fn wrapping(inner: StaticScheduleNamespaces) -> Self {
512 Self {
513 inner,
514 calls: Arc::new(AtomicUsize::new(0)),
515 }
516 }
517
518 fn calls(&self) -> usize {
519 self.calls.load(Ordering::SeqCst)
520 }
521 }
522
523 #[async_trait]
524 impl ScheduleNamespaceSource for CountingScheduleNamespaces {
525 async fn schedule_namespace(
526 &self,
527 schedule_id: &ScheduleId,
528 ) -> Result<Option<String>, ServerError> {
529 self.calls.fetch_add(1, Ordering::SeqCst);
530 self.inner.schedule_namespace(schedule_id).await
531 }
532 }
533
534 fn guard_with_ownership(ownership: StaticWorkflowNamespaces) -> NamespaceGuard {
535 let resolver = NamespaceResolver::authorization_only(
536 NamespaceMode::SharedEngine,
537 ownership,
538 StaticScheduleNamespaces::default(),
539 );
540 NamespaceGuard::new(resolver)
541 }
542
543 fn guard_with_schedule_ownership(
544 schedule_ownership: impl ScheduleNamespaceSource + 'static,
545 ) -> NamespaceGuard {
546 let resolver = NamespaceResolver::authorization_only(
547 NamespaceMode::SharedEngine,
548 StaticWorkflowNamespaces::default(),
549 schedule_ownership,
550 );
551 NamespaceGuard::new(resolver)
552 }
553
554 fn caller() -> CallerIdentity {
555 CallerIdentity::new("alice", [String::from("tenant-a")])
556 }
557
558 fn workflow_ids() -> (WorkflowId, RunId) {
559 (
560 WorkflowId::new(uuid::Uuid::from_u128(1)),
561 RunId::new(uuid::Uuid::from_u128(2)),
562 )
563 }
564
565 #[tokio::test]
566 async fn denied_targeted_operations_do_not_call_engine()
567 -> Result<(), Box<dyn std::error::Error>> {
568 let (workflow_id, run_id) = workflow_ids();
569 let ownership = StaticWorkflowNamespaces::default();
570 ownership.record(workflow_id.clone(), "tenant-b")?;
571 let guard = guard_with_ownership(ownership);
572 let fake = RecordingFakeEngine::new();
573 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
574
575 let signal = ProtoSignalRequest {
576 namespace: String::from("tenant-a"),
577 workflow_id: None,
578 run_id: None,
579 signal_name: String::from("ship"),
580 payload: None,
581 };
582 let query = ProtoQueryRequest {
583 namespace: String::from("tenant-a"),
584 workflow_id: None,
585 run_id: None,
586 query_name: String::from("state"),
587 };
588 let cancel = ProtoCancelRequest {
589 namespace: String::from("tenant-a"),
590 workflow_id: None,
591 run_id: None,
592 reason: String::from("operator"),
593 };
594 let describe = ProtoDescribeWorkflowRequest {
595 namespace: String::from("tenant-a"),
596 workflow_id: None,
597 run_id: None,
598 include_history: false,
599 };
600
601 let operations = [
602 NamespaceOperation::signal(&signal, target),
603 NamespaceOperation::query(&query, target),
604 NamespaceOperation::cancel(&cancel, target),
605 NamespaceOperation::describe(&describe, target),
606 ];
607
608 for operation in operations {
609 let result = guard.scope(&caller(), &operation).await;
610 assert_eq!(
614 result.err().map(|error| error.to_wire_error().code),
615 Some(aion_proto::WireErrorCode::NotFound)
616 );
617 }
618 assert!(fake.calls()?.is_empty());
619 Ok(())
620 }
621
622 #[tokio::test]
623 async fn denied_list_and_worker_scope_do_not_call_engine()
624 -> Result<(), Box<dyn std::error::Error>> {
625 let (workflow_id, _run_id) = workflow_ids();
626 let ownership = StaticWorkflowNamespaces::default();
627 ownership.record(workflow_id, "tenant-b")?;
628 let guard = guard_with_ownership(ownership);
629 let fake = RecordingFakeEngine::new();
630 let filter = WorkflowFilter::default();
631
632 let list = ProtoListWorkflowsRequest {
633 namespace: String::from("tenant-b"),
634 filter: None,
635 };
636 let worker = ProtoRegisterWorker {
637 namespace: String::from("tenant-b"),
638 activity_types: vec![String::from("ship")],
639 };
640
641 assert!(
642 guard
643 .scope(&caller(), &NamespaceOperation::list(&list, &filter))
644 .await
645 .is_err()
646 );
647 assert!(
648 guard
649 .scope(&caller(), &NamespaceOperation::register_worker(&worker),)
650 .await
651 .is_err()
652 );
653 assert!(fake.calls()?.is_empty());
654 Ok(())
655 }
656
657 #[tokio::test]
658 async fn denied_subscriptions_do_not_call_engine() -> Result<(), Box<dyn std::error::Error>> {
659 let (workflow_id, run_id) = workflow_ids();
660 let ownership = StaticWorkflowNamespaces::default();
661 ownership.record(workflow_id.clone(), "tenant-b")?;
662 let guard = guard_with_ownership(ownership);
663 let fake = RecordingFakeEngine::new();
664 let event_filter = aion::EventFilter::default();
665
666 let filtered = FilteredSubscription {
667 namespace: String::from("tenant-a"),
668 workflow_type: None,
669 status: None,
670 namespace_selector: Some(String::from("tenant-b")),
671 };
672 let filtered_by_workflow = FilteredSubscription {
673 namespace: String::from("tenant-a"),
674 workflow_type: None,
675 status: None,
676 namespace_selector: None,
677 };
678 let per_workflow = PerWorkflowSubscription {
679 namespace: String::from("tenant-a"),
680 workflow_id: None,
681 resume_from_seq: None,
682 };
683 let cross_namespace_filter = aion::EventFilter {
684 workflow_id: Some(workflow_id.clone()),
685 run: None,
686 family: None,
687 };
688 let firehose = FirehoseSubscription {
689 namespace: String::from("tenant-b"),
690 };
691
692 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
693 let denied_subscriptions = [
697 (
698 NamespaceOperation::subscribe(
699 SubscriptionScope::Filtered(&filtered),
700 &event_filter,
701 ),
702 aion_proto::WireErrorCode::NamespaceDenied,
703 ),
704 (
705 NamespaceOperation::subscribe(
706 SubscriptionScope::Filtered(&filtered_by_workflow),
707 &cross_namespace_filter,
708 ),
709 aion_proto::WireErrorCode::NotFound,
710 ),
711 (
712 NamespaceOperation::subscribe(
713 SubscriptionScope::PerWorkflow(&per_workflow, target),
714 &cross_namespace_filter,
715 ),
716 aion_proto::WireErrorCode::NotFound,
717 ),
718 (
719 NamespaceOperation::subscribe(
720 SubscriptionScope::Firehose(&firehose),
721 &event_filter,
722 ),
723 aion_proto::WireErrorCode::NamespaceDenied,
724 ),
725 ];
726
727 for (operation, expected_code) in &denied_subscriptions {
728 assert_eq!(
729 guard
730 .scope(&caller(), operation)
731 .await
732 .err()
733 .map(|error| error.to_wire_error().code)
734 .as_ref(),
735 Some(expected_code)
736 );
737 }
738 assert!(fake.calls()?.is_empty());
739 Ok(())
740 }
741
742 fn schedule_id() -> ScheduleId {
743 ScheduleId::new(uuid::Uuid::from_u128(9))
744 }
745
746 fn schedule_id_request(namespace: &str) -> ProtoScheduleIdRequest {
747 ProtoScheduleIdRequest {
748 namespace: namespace.to_owned(),
749 schedule_id: None,
750 }
751 }
752
753 #[tokio::test]
754 async fn schedule_ownership_misses_are_not_found_and_do_not_call_engine()
755 -> Result<(), Box<dyn std::error::Error>> {
756 let schedule_id = schedule_id();
757 let schedule_ownership = StaticScheduleNamespaces::default();
758 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
759 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
760 let guard = guard_with_schedule_ownership(counting.clone());
764 let target = ScheduleTarget::schedule(&schedule_id);
765
766 let update = ProtoUpdateScheduleRequest {
767 namespace: String::from("tenant-a"),
768 schedule_id: None,
769 config: None,
770 };
771 let id_request = schedule_id_request("tenant-a");
772
773 let operations = [
774 NamespaceOperation::update_schedule(&update, target),
775 NamespaceOperation::pause_schedule(&id_request, target),
776 NamespaceOperation::resume_schedule(&id_request, target),
777 NamespaceOperation::delete_schedule(&id_request, target),
778 NamespaceOperation::describe_schedule(&id_request, target),
779 ];
780 let operation_count = operations.len();
781
782 for operation in operations {
783 let result = guard.scope(&caller(), &operation).await;
784 let error = result
788 .err()
789 .map(|error| error.to_wire_error())
790 .ok_or("expected foreign-owned schedule to be rejected")?;
791 assert_eq!(error.code, aion_proto::WireErrorCode::NotFound);
792 assert_eq!(error.message, "schedule not found in namespace tenant-a");
793 }
794 assert_eq!(counting.calls(), operation_count);
797 Ok(())
798 }
799
800 #[tokio::test]
801 async fn ungranted_schedule_operations_are_namespace_denied()
802 -> Result<(), Box<dyn std::error::Error>> {
803 let schedule_id = schedule_id();
804 let schedule_ownership = StaticScheduleNamespaces::default();
805 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
806 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
807 let guard = guard_with_schedule_ownership(counting.clone());
808 let target = ScheduleTarget::schedule(&schedule_id);
809
810 let create = ProtoCreateScheduleRequest {
811 namespace: String::from("tenant-b"),
812 config: None,
813 };
814 let update = ProtoUpdateScheduleRequest {
815 namespace: String::from("tenant-b"),
816 schedule_id: None,
817 config: None,
818 };
819 let id_request = schedule_id_request("tenant-b");
820 let list = ProtoListSchedulesRequest {
821 namespace: String::from("tenant-b"),
822 };
823
824 let operations = [
825 NamespaceOperation::create_schedule(&create),
826 NamespaceOperation::update_schedule(&update, target),
827 NamespaceOperation::pause_schedule(&id_request, target),
828 NamespaceOperation::resume_schedule(&id_request, target),
829 NamespaceOperation::delete_schedule(&id_request, target),
830 NamespaceOperation::describe_schedule(&id_request, target),
831 NamespaceOperation::list_schedules(&list),
832 ];
833
834 for operation in operations {
838 let result = guard.scope(&caller(), &operation).await;
839 assert_eq!(
840 result.err().map(|error| error.to_wire_error().code),
841 Some(aion_proto::WireErrorCode::NamespaceDenied)
842 );
843 }
844 assert_eq!(counting.calls(), 0);
847 Ok(())
848 }
849
850 #[tokio::test]
851 async fn granted_schedule_create_and_list_return_scoped_engine()
852 -> Result<(), Box<dyn std::error::Error>> {
853 let guard = guard_with_schedule_ownership(StaticScheduleNamespaces::default());
854 let create = ProtoCreateScheduleRequest {
855 namespace: String::from("tenant-a"),
856 config: None,
857 };
858 let list = ProtoListSchedulesRequest {
859 namespace: String::from("tenant-a"),
860 };
861
862 let scoped_create = guard
863 .scope(&caller(), &NamespaceOperation::create_schedule(&create))
864 .await?;
865 let scoped_list = guard
866 .scope(&caller(), &NamespaceOperation::list_schedules(&list))
867 .await?;
868
869 assert_eq!(scoped_create.namespace(), "tenant-a");
870 assert_eq!(scoped_list.namespace(), "tenant-a");
871 Ok(())
872 }
873
874 #[tokio::test]
875 async fn authorized_start_returns_scoped_engine() -> Result<(), Box<dyn std::error::Error>> {
876 let guard = guard_with_ownership(StaticWorkflowNamespaces::default());
877 let request = ProtoStartWorkflowRequest {
878 namespace: String::from("tenant-a"),
879 workflow_type: String::from("checkout"),
880 input: None,
881 };
882
883 let scoped = guard
884 .scope(&caller(), &NamespaceOperation::start(&request))
885 .await?;
886
887 assert_eq!(scoped.namespace(), "tenant-a");
888 Ok(())
889 }
890
891 #[tokio::test]
892 async fn single_tenant_mode_authorizes_configured_namespace()
893 -> Result<(), Box<dyn std::error::Error>> {
894 let resolver = NamespaceResolver::authorization_only(
895 NamespaceMode::SingleTenant {
896 namespace: String::from("tenant-a"),
897 },
898 StaticWorkflowNamespaces::default(),
899 StaticScheduleNamespaces::default(),
900 );
901 let guard = NamespaceGuard::new(resolver);
902 let request = ProtoRegisterWorker {
903 namespace: String::from("tenant-a"),
904 activity_types: Vec::new(),
905 };
906
907 let scoped = guard
908 .scope(
909 &CallerIdentity::new("single-tenant", Vec::<String>::new()),
910 &NamespaceOperation::register_worker(&request),
911 )
912 .await?;
913
914 assert_eq!(scoped.namespace(), "tenant-a");
915 Ok(())
916 }
917}