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, ProtoPauseRequest, ProtoQueryRequest,
9 ProtoRegisterWorker, ProtoReopenRequest, ProtoResumeRequest, ProtoScheduleIdRequest,
10 ProtoSignalRequest, ProtoStartWorkflowRequest, ProtoUpdateScheduleRequest, SubscriptionRequest,
11 subscription_request,
12};
13
14use crate::error::ServerError;
15
16use super::resolver::{CallerIdentity, NamespaceResolver, ScopedEngine};
17
18#[derive(Clone)]
20pub struct NamespaceGuard {
21 resolver: NamespaceResolver,
22}
23
24impl NamespaceGuard {
25 #[must_use]
27 pub const fn new(resolver: NamespaceResolver) -> Self {
28 Self { resolver }
29 }
30
31 #[must_use]
33 pub const fn resolver(&self) -> &NamespaceResolver {
34 &self.resolver
35 }
36
37 pub async fn scope(
52 &self,
53 caller: &CallerIdentity,
54 operation: &NamespaceOperation<'_>,
55 ) -> Result<ScopedEngine, ServerError> {
56 let requested_namespace = operation.requested_namespace();
57 let scoped = self.resolver.resolve(caller, requested_namespace)?;
58 operation.verify(&self.resolver, scoped.namespace()).await?;
59 Ok(scoped)
60 }
61
62 pub fn authorize_namespace(
78 &self,
79 caller: &CallerIdentity,
80 namespace: &str,
81 ) -> Result<String, ServerError> {
82 let scoped = self.resolver.resolve(caller, namespace)?;
83 Ok(scoped.namespace().to_owned())
84 }
85
86 pub fn scope_worker_namespaces(
100 &self,
101 caller: &CallerIdentity,
102 namespaces: &[String],
103 ) -> Result<Vec<String>, ServerError> {
104 if namespaces.is_empty() {
105 return Err(ServerError::namespace_denied(
106 "worker registration must name at least one namespace",
107 ));
108 }
109 let mut authorized: Vec<String> = Vec::with_capacity(namespaces.len());
110 for namespace in namespaces {
111 let scoped = self.resolver.resolve(caller, namespace)?;
112 let resolved = scoped.namespace().to_owned();
113 if !authorized.contains(&resolved) {
114 authorized.push(resolved);
115 }
116 }
117 Ok(authorized)
118 }
119}
120
121pub enum NamespaceOperation<'a> {
123 StartWorkflow(&'a ProtoStartWorkflowRequest),
125 Signal(&'a ProtoSignalRequest, WorkflowTarget<'a>),
127 Query(&'a ProtoQueryRequest, WorkflowTarget<'a>),
129 Cancel(&'a ProtoCancelRequest, WorkflowTarget<'a>),
131 Reopen(&'a ProtoReopenRequest, WorkflowTarget<'a>),
133 PauseWorkflow(&'a ProtoPauseRequest, WorkflowTarget<'a>),
135 ResumeWorkflow(&'a ProtoResumeRequest, WorkflowTarget<'a>),
137 ListWorkflows(&'a ProtoListWorkflowsRequest, &'a WorkflowFilter),
139 CountWorkflows(&'a ProtoCountWorkflowsRequest),
141 Describe(&'a ProtoDescribeWorkflowRequest, WorkflowTarget<'a>),
143 CreateSchedule(&'a ProtoCreateScheduleRequest),
145 UpdateSchedule(&'a ProtoUpdateScheduleRequest, ScheduleTarget<'a>),
147 PauseSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
149 ResumeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
151 DeleteSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
153 ListSchedules(&'a ProtoListSchedulesRequest),
155 DescribeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
157 Subscribe(SubscriptionScope<'a>, &'a EventFilter),
159 RegisterWorker(&'a ProtoRegisterWorker),
161 Intervene {
165 namespace: &'a str,
167 target: WorkflowTarget<'a>,
169 },
170}
171
172impl<'a> NamespaceOperation<'a> {
173 #[must_use]
175 pub const fn start(request: &'a ProtoStartWorkflowRequest) -> Self {
176 Self::StartWorkflow(request)
177 }
178
179 #[must_use]
181 pub const fn signal(request: &'a ProtoSignalRequest, target: WorkflowTarget<'a>) -> Self {
182 Self::Signal(request, target)
183 }
184
185 #[must_use]
187 pub const fn query(request: &'a ProtoQueryRequest, target: WorkflowTarget<'a>) -> Self {
188 Self::Query(request, target)
189 }
190
191 #[must_use]
193 pub const fn cancel(request: &'a ProtoCancelRequest, target: WorkflowTarget<'a>) -> Self {
194 Self::Cancel(request, target)
195 }
196
197 #[must_use]
199 pub const fn reopen(request: &'a ProtoReopenRequest, target: WorkflowTarget<'a>) -> Self {
200 Self::Reopen(request, target)
201 }
202
203 #[must_use]
205 pub const fn pause_workflow(
206 request: &'a ProtoPauseRequest,
207 target: WorkflowTarget<'a>,
208 ) -> Self {
209 Self::PauseWorkflow(request, target)
210 }
211
212 #[must_use]
214 pub const fn resume_workflow(
215 request: &'a ProtoResumeRequest,
216 target: WorkflowTarget<'a>,
217 ) -> Self {
218 Self::ResumeWorkflow(request, target)
219 }
220
221 #[must_use]
223 pub const fn list(request: &'a ProtoListWorkflowsRequest, filter: &'a WorkflowFilter) -> Self {
224 Self::ListWorkflows(request, filter)
225 }
226
227 #[must_use]
229 pub const fn count(request: &'a ProtoCountWorkflowsRequest) -> Self {
230 Self::CountWorkflows(request)
231 }
232
233 #[must_use]
235 pub const fn describe(
236 request: &'a ProtoDescribeWorkflowRequest,
237 target: WorkflowTarget<'a>,
238 ) -> Self {
239 Self::Describe(request, target)
240 }
241
242 #[must_use]
244 pub const fn create_schedule(request: &'a ProtoCreateScheduleRequest) -> Self {
245 Self::CreateSchedule(request)
246 }
247
248 #[must_use]
250 pub const fn update_schedule(
251 request: &'a ProtoUpdateScheduleRequest,
252 target: ScheduleTarget<'a>,
253 ) -> Self {
254 Self::UpdateSchedule(request, target)
255 }
256
257 #[must_use]
259 pub const fn pause_schedule(
260 request: &'a ProtoScheduleIdRequest,
261 target: ScheduleTarget<'a>,
262 ) -> Self {
263 Self::PauseSchedule(request, target)
264 }
265
266 #[must_use]
268 pub const fn resume_schedule(
269 request: &'a ProtoScheduleIdRequest,
270 target: ScheduleTarget<'a>,
271 ) -> Self {
272 Self::ResumeSchedule(request, target)
273 }
274
275 #[must_use]
277 pub const fn delete_schedule(
278 request: &'a ProtoScheduleIdRequest,
279 target: ScheduleTarget<'a>,
280 ) -> Self {
281 Self::DeleteSchedule(request, target)
282 }
283
284 #[must_use]
286 pub const fn list_schedules(request: &'a ProtoListSchedulesRequest) -> Self {
287 Self::ListSchedules(request)
288 }
289
290 #[must_use]
292 pub const fn describe_schedule(
293 request: &'a ProtoScheduleIdRequest,
294 target: ScheduleTarget<'a>,
295 ) -> Self {
296 Self::DescribeSchedule(request, target)
297 }
298
299 #[must_use]
301 pub const fn subscribe(scope: SubscriptionScope<'a>, filter: &'a EventFilter) -> Self {
302 Self::Subscribe(scope, filter)
303 }
304
305 #[must_use]
307 pub const fn register_worker(request: &'a ProtoRegisterWorker) -> Self {
308 Self::RegisterWorker(request)
309 }
310
311 #[must_use]
313 pub const fn intervene(namespace: &'a str, target: WorkflowTarget<'a>) -> Self {
314 Self::Intervene { namespace, target }
315 }
316
317 fn requested_namespace(&self) -> &str {
318 match self {
319 Self::StartWorkflow(request) => request.namespace.as_str(),
320 Self::Signal(request, _target) => request.namespace.as_str(),
321 Self::Query(request, _target) => request.namespace.as_str(),
322 Self::Cancel(request, _target) => request.namespace.as_str(),
323 Self::Reopen(request, _target) => request.namespace.as_str(),
324 Self::PauseWorkflow(request, _target) => request.namespace.as_str(),
325 Self::ResumeWorkflow(request, _target) => request.namespace.as_str(),
326 Self::ListWorkflows(request, _filter) => request.namespace.as_str(),
327 Self::CountWorkflows(request) => request.namespace.as_str(),
328 Self::Describe(request, _target) => request.namespace.as_str(),
329 Self::CreateSchedule(request) => request.namespace.as_str(),
330 Self::UpdateSchedule(request, _target) => request.namespace.as_str(),
331 Self::PauseSchedule(request, _target)
332 | Self::ResumeSchedule(request, _target)
333 | Self::DeleteSchedule(request, _target)
334 | Self::DescribeSchedule(request, _target) => request.namespace.as_str(),
335 Self::ListSchedules(request) => request.namespace.as_str(),
336 Self::Subscribe(scope, _filter) => scope.namespace(),
337 Self::RegisterWorker(request) => request.namespaces.first().map_or("", String::as_str),
344 Self::Intervene { namespace, .. } => namespace,
345 }
346 }
347
348 async fn verify(
349 &self,
350 resolver: &NamespaceResolver,
351 authorized_namespace: &str,
352 ) -> Result<(), ServerError> {
353 match self {
354 Self::Signal(_, target)
357 | Self::Query(_, target)
358 | Self::Cancel(_, target)
359 | Self::Reopen(_, target)
360 | Self::PauseWorkflow(_, target)
361 | Self::ResumeWorkflow(_, target)
362 | Self::Describe(_, target)
363 | Self::Intervene { target, .. } => target.verify(resolver, authorized_namespace).await,
364 Self::UpdateSchedule(_, target)
365 | Self::PauseSchedule(_, target)
366 | Self::ResumeSchedule(_, target)
367 | Self::DeleteSchedule(_, target)
368 | Self::DescribeSchedule(_, target) => {
369 target.verify(resolver, authorized_namespace).await
370 }
371 Self::Subscribe(scope, filter) => {
372 scope.verify(resolver, authorized_namespace, filter).await
373 }
374 Self::StartWorkflow(_)
381 | Self::ListWorkflows(_, _)
382 | Self::CountWorkflows(_)
383 | Self::CreateSchedule(_)
384 | Self::ListSchedules(_)
385 | Self::RegisterWorker(_) => Ok(()),
386 }
387 }
388}
389
390#[derive(Clone, Copy)]
392pub struct WorkflowTarget<'a> {
393 workflow_id: &'a WorkflowId,
394 run_id: Option<&'a RunId>,
395}
396
397impl<'a> WorkflowTarget<'a> {
398 #[must_use]
400 pub const fn with_run(workflow_id: &'a WorkflowId, run_id: &'a RunId) -> Self {
401 Self {
402 workflow_id,
403 run_id: Some(run_id),
404 }
405 }
406
407 #[must_use]
409 pub const fn workflow(workflow_id: &'a WorkflowId) -> Self {
410 Self {
411 workflow_id,
412 run_id: None,
413 }
414 }
415
416 #[must_use]
418 pub const fn workflow_id(&self) -> &WorkflowId {
419 self.workflow_id
420 }
421
422 #[must_use]
424 pub const fn run_id(&self) -> Option<&RunId> {
425 self.run_id
426 }
427
428 async fn verify(
429 &self,
430 resolver: &NamespaceResolver,
431 namespace: &str,
432 ) -> Result<(), ServerError> {
433 resolver
434 .verify_workflow_ownership(namespace, self.workflow_id)
435 .await
436 }
437}
438
439#[derive(Clone, Copy)]
441pub struct ScheduleTarget<'a> {
442 schedule_id: &'a ScheduleId,
443}
444
445impl<'a> ScheduleTarget<'a> {
446 #[must_use]
448 pub const fn schedule(schedule_id: &'a ScheduleId) -> Self {
449 Self { schedule_id }
450 }
451
452 #[must_use]
454 pub const fn schedule_id(&self) -> &ScheduleId {
455 self.schedule_id
456 }
457
458 async fn verify(
459 &self,
460 resolver: &NamespaceResolver,
461 namespace: &str,
462 ) -> Result<(), ServerError> {
463 resolver
464 .verify_schedule_ownership(namespace, self.schedule_id)
465 .await
466 }
467}
468
469pub enum SubscriptionScope<'a> {
471 PerWorkflow(&'a PerWorkflowSubscription, WorkflowTarget<'a>),
473 Filtered(&'a FilteredSubscription),
475 Firehose(&'a FirehoseSubscription),
477}
478
479impl<'a> SubscriptionScope<'a> {
480 pub fn from_request(
488 request: &'a SubscriptionRequest,
489 workflow_target: Option<WorkflowTarget<'a>>,
490 ) -> Result<Self, ServerError> {
491 match &request.subscription {
492 Some(subscription_request::Subscription::PerWorkflow(subscription)) => {
493 let target = workflow_target.ok_or_else(|| {
494 ServerError::namespace_denied(
495 "per-workflow subscription target must be decoded before guard scope",
496 )
497 })?;
498 Ok(Self::PerWorkflow(subscription, target))
499 }
500 Some(subscription_request::Subscription::Filtered(subscription)) => {
501 Ok(Self::Filtered(subscription))
502 }
503 Some(subscription_request::Subscription::Firehose(subscription)) => {
504 Ok(Self::Firehose(subscription))
505 }
506 Some(subscription_request::Subscription::Cluster(_)) => {
511 Err(ServerError::namespace_denied(
512 "cluster subscription is deployment-scoped and not served by the namespace guard",
513 ))
514 }
515 Some(subscription_request::Subscription::Transcript(_)) => {
519 Err(ServerError::namespace_denied(
520 "transcript subscription is served by the transcript channel, not this mapper",
521 ))
522 }
523 None => Err(ServerError::namespace_denied(
524 "subscription request must name a namespace",
525 )),
526 }
527 }
528
529 fn namespace(&self) -> &str {
530 match self {
531 Self::PerWorkflow(subscription, _target) => subscription.namespace.as_str(),
532 Self::Filtered(subscription) => subscription.namespace.as_str(),
533 Self::Firehose(subscription) => subscription.namespace.as_str(),
534 }
535 }
536
537 async fn verify(
538 &self,
539 resolver: &NamespaceResolver,
540 namespace: &str,
541 filter: &EventFilter,
542 ) -> Result<(), ServerError> {
543 match self {
544 Self::PerWorkflow(_subscription, target) => {
545 verify_subscription_filter_target(filter, Some(*target), resolver, namespace).await
546 }
547 Self::Filtered(subscription) => {
548 verify_namespace_selector(subscription.namespace_selector.as_deref(), namespace)?;
549 verify_subscription_filter_target(filter, None, resolver, namespace).await
550 }
551 Self::Firehose(_) => {
552 verify_subscription_filter_target(filter, None, resolver, namespace).await
553 }
554 }
555 }
556}
557
558fn verify_namespace_selector(selector: Option<&str>, namespace: &str) -> Result<(), ServerError> {
559 match selector {
560 Some(selector) if selector != namespace => Err(ServerError::namespace_denied(
561 "subscription namespace selector is not authorized",
562 )),
563 Some(_) | None => Ok(()),
564 }
565}
566
567async fn verify_subscription_filter_target(
568 filter: &EventFilter,
569 explicit_target: Option<WorkflowTarget<'_>>,
570 resolver: &NamespaceResolver,
571 namespace: &str,
572) -> Result<(), ServerError> {
573 if let Some(target) = explicit_target {
574 if filter
575 .workflow_id
576 .as_ref()
577 .is_some_and(|workflow_id| workflow_id != target.workflow_id())
578 {
579 return Err(ServerError::namespace_denied(
580 "subscription filter workflow does not match decoded target",
581 ));
582 }
583 target.verify(resolver, namespace).await
584 } else if let Some(workflow_id) = &filter.workflow_id {
585 resolver
586 .verify_workflow_ownership(namespace, workflow_id)
587 .await
588 } else {
589 Ok(())
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use std::sync::atomic::{AtomicUsize, Ordering};
596 use std::sync::{Arc, Mutex};
597
598 use aion_core::{RunId, ScheduleId, WorkflowFilter, WorkflowId};
599 use aion_proto::{
600 FilteredSubscription, FirehoseSubscription, PerWorkflowSubscription, ProtoCancelRequest,
601 ProtoCreateScheduleRequest, ProtoDescribeWorkflowRequest, ProtoListSchedulesRequest,
602 ProtoListWorkflowsRequest, ProtoQueryRequest, ProtoRegisterWorker, ProtoReopenRequest,
603 ProtoScheduleIdRequest, ProtoSignalRequest, ProtoStartWorkflowRequest,
604 ProtoUpdateScheduleRequest,
605 };
606 use async_trait::async_trait;
607
608 use super::{
609 NamespaceGuard, NamespaceOperation, ScheduleTarget, SubscriptionScope, WorkflowTarget,
610 };
611 use crate::config::NamespaceMode;
612 use crate::error::ServerError;
613 use crate::namespace::{
614 CallerIdentity, NamespaceResolver, ScheduleNamespaceSource, StaticScheduleNamespaces,
615 StaticWorkflowNamespaces,
616 };
617
618 struct RecordingFakeEngine {
619 calls: Mutex<Vec<&'static str>>,
620 }
621
622 impl RecordingFakeEngine {
623 fn new() -> Self {
624 Self {
625 calls: Mutex::new(Vec::new()),
626 }
627 }
628
629 fn calls(&self) -> Result<Vec<&'static str>, Box<dyn std::error::Error>> {
630 let calls = self
631 .calls
632 .lock()
633 .map_err(|_| "fake engine calls lock poisoned")?;
634 Ok(calls.clone())
635 }
636 }
637
638 #[derive(Clone)]
643 struct CountingScheduleNamespaces {
644 inner: StaticScheduleNamespaces,
645 calls: Arc<AtomicUsize>,
646 }
647
648 impl CountingScheduleNamespaces {
649 fn wrapping(inner: StaticScheduleNamespaces) -> Self {
650 Self {
651 inner,
652 calls: Arc::new(AtomicUsize::new(0)),
653 }
654 }
655
656 fn calls(&self) -> usize {
657 self.calls.load(Ordering::SeqCst)
658 }
659 }
660
661 #[async_trait]
662 impl ScheduleNamespaceSource for CountingScheduleNamespaces {
663 async fn schedule_namespace(
664 &self,
665 schedule_id: &ScheduleId,
666 ) -> Result<Option<String>, ServerError> {
667 self.calls.fetch_add(1, Ordering::SeqCst);
668 self.inner.schedule_namespace(schedule_id).await
669 }
670 }
671
672 fn guard_with_ownership(ownership: StaticWorkflowNamespaces) -> NamespaceGuard {
673 let resolver = NamespaceResolver::authorization_only(
674 NamespaceMode::SharedEngine,
675 ownership,
676 StaticScheduleNamespaces::default(),
677 );
678 NamespaceGuard::new(resolver)
679 }
680
681 fn guard_with_schedule_ownership(
682 schedule_ownership: impl ScheduleNamespaceSource + 'static,
683 ) -> NamespaceGuard {
684 let resolver = NamespaceResolver::authorization_only(
685 NamespaceMode::SharedEngine,
686 StaticWorkflowNamespaces::default(),
687 schedule_ownership,
688 );
689 NamespaceGuard::new(resolver)
690 }
691
692 fn caller() -> CallerIdentity {
693 CallerIdentity::new("alice", [String::from("tenant-a")])
694 }
695
696 fn workflow_ids() -> (WorkflowId, RunId) {
697 (
698 WorkflowId::new(uuid::Uuid::from_u128(1)),
699 RunId::new(uuid::Uuid::from_u128(2)),
700 )
701 }
702
703 #[tokio::test]
704 async fn denied_targeted_operations_do_not_call_engine()
705 -> Result<(), Box<dyn std::error::Error>> {
706 let (workflow_id, run_id) = workflow_ids();
707 let ownership = StaticWorkflowNamespaces::default();
708 ownership.record(workflow_id.clone(), "tenant-b")?;
709 let guard = guard_with_ownership(ownership);
710 let fake = RecordingFakeEngine::new();
711 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
712
713 let signal = ProtoSignalRequest {
714 namespace: String::from("tenant-a"),
715 workflow_id: None,
716 run_id: None,
717 signal_name: String::from("ship"),
718 payload: None,
719 };
720 let query = ProtoQueryRequest {
721 namespace: String::from("tenant-a"),
722 workflow_id: None,
723 run_id: None,
724 query_name: String::from("state"),
725 };
726 let cancel = ProtoCancelRequest {
727 namespace: String::from("tenant-a"),
728 workflow_id: None,
729 run_id: None,
730 reason: String::from("operator"),
731 };
732 let reopen = ProtoReopenRequest {
733 namespace: String::from("tenant-a"),
734 workflow_id: None,
735 run_id: None,
736 };
737 let describe = ProtoDescribeWorkflowRequest {
738 namespace: String::from("tenant-a"),
739 workflow_id: None,
740 run_id: None,
741 include_history: false,
742 };
743
744 let operations = [
745 NamespaceOperation::signal(&signal, target),
746 NamespaceOperation::query(&query, target),
747 NamespaceOperation::cancel(&cancel, target),
748 NamespaceOperation::reopen(&reopen, target),
749 NamespaceOperation::describe(&describe, target),
750 ];
751
752 for operation in operations {
753 let result = guard.scope(&caller(), &operation).await;
754 assert_eq!(
758 result.err().map(|error| error.to_wire_error().code),
759 Some(aion_proto::WireErrorCode::NotFound)
760 );
761 }
762 assert!(fake.calls()?.is_empty());
763 Ok(())
764 }
765
766 #[tokio::test]
767 async fn denied_list_and_worker_scope_do_not_call_engine()
768 -> Result<(), Box<dyn std::error::Error>> {
769 let (workflow_id, _run_id) = workflow_ids();
770 let ownership = StaticWorkflowNamespaces::default();
771 ownership.record(workflow_id, "tenant-b")?;
772 let guard = guard_with_ownership(ownership);
773 let fake = RecordingFakeEngine::new();
774 let filter = WorkflowFilter::default();
775
776 let list = ProtoListWorkflowsRequest {
777 namespace: String::from("tenant-b"),
778 filter: None,
779 };
780 let worker = ProtoRegisterWorker {
781 namespaces: vec![String::from("tenant-b")],
782 activity_types: vec![String::from("ship")],
783 task_queue: String::new(),
784 node: String::new(),
785 activities: Vec::new(),
786 identity: String::new(),
787 };
788
789 assert!(
790 guard
791 .scope(&caller(), &NamespaceOperation::list(&list, &filter))
792 .await
793 .is_err()
794 );
795 assert!(
796 guard
797 .scope(&caller(), &NamespaceOperation::register_worker(&worker),)
798 .await
799 .is_err()
800 );
801 assert!(fake.calls()?.is_empty());
802 Ok(())
803 }
804
805 #[tokio::test]
806 async fn denied_subscriptions_do_not_call_engine() -> Result<(), Box<dyn std::error::Error>> {
807 let (workflow_id, run_id) = workflow_ids();
808 let ownership = StaticWorkflowNamespaces::default();
809 ownership.record(workflow_id.clone(), "tenant-b")?;
810 let guard = guard_with_ownership(ownership);
811 let fake = RecordingFakeEngine::new();
812 let event_filter = aion::EventFilter::default();
813
814 let filtered = FilteredSubscription {
815 namespace: String::from("tenant-a"),
816 workflow_type: None,
817 status: None,
818 namespace_selector: Some(String::from("tenant-b")),
819 };
820 let filtered_by_workflow = FilteredSubscription {
821 namespace: String::from("tenant-a"),
822 workflow_type: None,
823 status: None,
824 namespace_selector: None,
825 };
826 let per_workflow = PerWorkflowSubscription {
827 namespace: String::from("tenant-a"),
828 workflow_id: None,
829 resume_from_seq: None,
830 };
831 let cross_namespace_filter = aion::EventFilter {
832 workflow_id: Some(workflow_id.clone()),
833 run: None,
834 family: None,
835 };
836 let firehose = FirehoseSubscription {
837 namespace: String::from("tenant-b"),
838 };
839
840 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
841 let denied_subscriptions = [
845 (
846 NamespaceOperation::subscribe(
847 SubscriptionScope::Filtered(&filtered),
848 &event_filter,
849 ),
850 aion_proto::WireErrorCode::NamespaceDenied,
851 ),
852 (
853 NamespaceOperation::subscribe(
854 SubscriptionScope::Filtered(&filtered_by_workflow),
855 &cross_namespace_filter,
856 ),
857 aion_proto::WireErrorCode::NotFound,
858 ),
859 (
860 NamespaceOperation::subscribe(
861 SubscriptionScope::PerWorkflow(&per_workflow, target),
862 &cross_namespace_filter,
863 ),
864 aion_proto::WireErrorCode::NotFound,
865 ),
866 (
867 NamespaceOperation::subscribe(
868 SubscriptionScope::Firehose(&firehose),
869 &event_filter,
870 ),
871 aion_proto::WireErrorCode::NamespaceDenied,
872 ),
873 ];
874
875 for (operation, expected_code) in &denied_subscriptions {
876 assert_eq!(
877 guard
878 .scope(&caller(), operation)
879 .await
880 .err()
881 .map(|error| error.to_wire_error().code)
882 .as_ref(),
883 Some(expected_code)
884 );
885 }
886 assert!(fake.calls()?.is_empty());
887 Ok(())
888 }
889
890 fn schedule_id() -> ScheduleId {
891 ScheduleId::new(uuid::Uuid::from_u128(9))
892 }
893
894 fn schedule_id_request(namespace: &str) -> ProtoScheduleIdRequest {
895 ProtoScheduleIdRequest {
896 namespace: namespace.to_owned(),
897 schedule_id: None,
898 }
899 }
900
901 #[tokio::test]
902 async fn schedule_ownership_misses_are_not_found_and_do_not_call_engine()
903 -> Result<(), Box<dyn std::error::Error>> {
904 let schedule_id = schedule_id();
905 let schedule_ownership = StaticScheduleNamespaces::default();
906 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
907 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
908 let guard = guard_with_schedule_ownership(counting.clone());
912 let target = ScheduleTarget::schedule(&schedule_id);
913
914 let update = ProtoUpdateScheduleRequest {
915 namespace: String::from("tenant-a"),
916 schedule_id: None,
917 config: None,
918 };
919 let id_request = schedule_id_request("tenant-a");
920
921 let operations = [
922 NamespaceOperation::update_schedule(&update, target),
923 NamespaceOperation::pause_schedule(&id_request, target),
924 NamespaceOperation::resume_schedule(&id_request, target),
925 NamespaceOperation::delete_schedule(&id_request, target),
926 NamespaceOperation::describe_schedule(&id_request, target),
927 ];
928 let operation_count = operations.len();
929
930 for operation in operations {
931 let result = guard.scope(&caller(), &operation).await;
932 let error = result
936 .err()
937 .map(|error| error.to_wire_error())
938 .ok_or("expected foreign-owned schedule to be rejected")?;
939 assert_eq!(error.code, aion_proto::WireErrorCode::NotFound);
940 assert_eq!(error.message, "schedule not found in namespace tenant-a");
941 }
942 assert_eq!(counting.calls(), operation_count);
945 Ok(())
946 }
947
948 #[tokio::test]
949 async fn ungranted_schedule_operations_are_namespace_denied()
950 -> Result<(), Box<dyn std::error::Error>> {
951 let schedule_id = schedule_id();
952 let schedule_ownership = StaticScheduleNamespaces::default();
953 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
954 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
955 let guard = guard_with_schedule_ownership(counting.clone());
956 let target = ScheduleTarget::schedule(&schedule_id);
957
958 let create = ProtoCreateScheduleRequest {
959 namespace: String::from("tenant-b"),
960 config: None,
961 };
962 let update = ProtoUpdateScheduleRequest {
963 namespace: String::from("tenant-b"),
964 schedule_id: None,
965 config: None,
966 };
967 let id_request = schedule_id_request("tenant-b");
968 let list = ProtoListSchedulesRequest {
969 namespace: String::from("tenant-b"),
970 };
971
972 let operations = [
973 NamespaceOperation::create_schedule(&create),
974 NamespaceOperation::update_schedule(&update, target),
975 NamespaceOperation::pause_schedule(&id_request, target),
976 NamespaceOperation::resume_schedule(&id_request, target),
977 NamespaceOperation::delete_schedule(&id_request, target),
978 NamespaceOperation::describe_schedule(&id_request, target),
979 NamespaceOperation::list_schedules(&list),
980 ];
981
982 for operation in operations {
986 let result = guard.scope(&caller(), &operation).await;
987 assert_eq!(
988 result.err().map(|error| error.to_wire_error().code),
989 Some(aion_proto::WireErrorCode::NamespaceDenied)
990 );
991 }
992 assert_eq!(counting.calls(), 0);
995 Ok(())
996 }
997
998 #[tokio::test]
999 async fn granted_schedule_create_and_list_return_scoped_engine()
1000 -> Result<(), Box<dyn std::error::Error>> {
1001 let guard = guard_with_schedule_ownership(StaticScheduleNamespaces::default());
1002 let create = ProtoCreateScheduleRequest {
1003 namespace: String::from("tenant-a"),
1004 config: None,
1005 };
1006 let list = ProtoListSchedulesRequest {
1007 namespace: String::from("tenant-a"),
1008 };
1009
1010 let scoped_create = guard
1011 .scope(&caller(), &NamespaceOperation::create_schedule(&create))
1012 .await?;
1013 let scoped_list = guard
1014 .scope(&caller(), &NamespaceOperation::list_schedules(&list))
1015 .await?;
1016
1017 assert_eq!(scoped_create.namespace(), "tenant-a");
1018 assert_eq!(scoped_list.namespace(), "tenant-a");
1019 Ok(())
1020 }
1021
1022 #[tokio::test]
1023 async fn authorized_start_returns_scoped_engine() -> Result<(), Box<dyn std::error::Error>> {
1024 let guard = guard_with_ownership(StaticWorkflowNamespaces::default());
1025 let request = ProtoStartWorkflowRequest {
1026 namespace: String::from("tenant-a"),
1027 workflow_type: String::from("checkout"),
1028 input: None,
1029 routing_key: None,
1030 task_queue: None,
1031 };
1032
1033 let scoped = guard
1034 .scope(&caller(), &NamespaceOperation::start(&request))
1035 .await?;
1036
1037 assert_eq!(scoped.namespace(), "tenant-a");
1038 Ok(())
1039 }
1040
1041 #[tokio::test]
1042 async fn single_tenant_mode_authorizes_configured_namespace()
1043 -> Result<(), Box<dyn std::error::Error>> {
1044 let resolver = NamespaceResolver::authorization_only(
1045 NamespaceMode::SingleTenant {
1046 namespace: String::from("tenant-a"),
1047 },
1048 StaticWorkflowNamespaces::default(),
1049 StaticScheduleNamespaces::default(),
1050 );
1051 let guard = NamespaceGuard::new(resolver);
1052 let request = ProtoRegisterWorker {
1053 namespaces: vec![String::from("tenant-a")],
1054 activity_types: Vec::new(),
1055 task_queue: String::new(),
1056 node: String::new(),
1057 activities: Vec::new(),
1058 identity: String::new(),
1059 };
1060
1061 let scoped = guard
1062 .scope(
1063 &CallerIdentity::new("single-tenant", Vec::<String>::new()),
1064 &NamespaceOperation::register_worker(&request),
1065 )
1066 .await?;
1067
1068 assert_eq!(scoped.namespace(), "tenant-a");
1069 Ok(())
1070 }
1071}