1use aion::EventFilter;
4use aion_core::{RunId, ScheduleId, WorkflowId};
5use aion_proto::{
6 FilteredSubscription, FirehoseSubscription, PerWorkflowSubscription, ProtoCancelRequest,
7 ProtoCreateScheduleRequest, ProtoDescribeWorkflowRequest, ProtoListSchedulesRequest,
8 ProtoListWorkflowsRequest, ProtoPauseRequest, ProtoQueryRequest, ProtoRegisterWorker,
9 ProtoRenameRequest, 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]
38 pub fn shared_engine() -> Self {
39 Self::new(NamespaceResolver::authorization_only(
40 crate::config::NamespaceMode::SharedEngine,
41 super::StaticWorkflowNamespaces::default(),
42 super::StaticScheduleNamespaces::default(),
43 ))
44 }
45
46 #[must_use]
48 pub const fn resolver(&self) -> &NamespaceResolver {
49 &self.resolver
50 }
51
52 pub async fn scope(
67 &self,
68 caller: &CallerIdentity,
69 operation: &NamespaceOperation<'_>,
70 ) -> Result<ScopedEngine, ServerError> {
71 let requested_namespace = operation.requested_namespace();
72 let scoped = self.resolver.resolve(caller, requested_namespace)?;
73 operation.verify(&self.resolver, scoped.namespace()).await?;
74 Ok(scoped)
75 }
76
77 pub fn authorize_namespace(
93 &self,
94 caller: &CallerIdentity,
95 namespace: &str,
96 ) -> Result<String, ServerError> {
97 let scoped = self.resolver.resolve(caller, namespace)?;
98 Ok(scoped.namespace().to_owned())
99 }
100
101 pub fn scope_worker_namespaces(
115 &self,
116 caller: &CallerIdentity,
117 namespaces: &[String],
118 ) -> Result<Vec<String>, ServerError> {
119 if namespaces.is_empty() {
120 return Err(ServerError::namespace_denied(
121 "worker registration must name at least one namespace",
122 ));
123 }
124 let mut authorized: Vec<String> = Vec::with_capacity(namespaces.len());
125 for namespace in namespaces {
126 let scoped = self.resolver.resolve(caller, namespace)?;
127 let resolved = scoped.namespace().to_owned();
128 if !authorized.contains(&resolved) {
129 authorized.push(resolved);
130 }
131 }
132 Ok(authorized)
133 }
134}
135
136pub enum NamespaceOperation<'a> {
138 StartWorkflow(&'a ProtoStartWorkflowRequest),
140 Signal(&'a ProtoSignalRequest, WorkflowTarget<'a>),
142 Query(&'a ProtoQueryRequest, WorkflowTarget<'a>),
144 Cancel(&'a ProtoCancelRequest, WorkflowTarget<'a>),
146 Reopen(&'a ProtoReopenRequest, WorkflowTarget<'a>),
148 PauseWorkflow(&'a ProtoPauseRequest, WorkflowTarget<'a>),
150 ResumeWorkflow(&'a ProtoResumeRequest, WorkflowTarget<'a>),
152 RenameWorkflow(&'a ProtoRenameRequest, WorkflowTarget<'a>),
154 ListWorkflows(&'a ProtoListWorkflowsRequest),
156 Describe(&'a ProtoDescribeWorkflowRequest, WorkflowTarget<'a>),
159 CreateSchedule(&'a ProtoCreateScheduleRequest),
161 UpdateSchedule(&'a ProtoUpdateScheduleRequest, ScheduleTarget<'a>),
163 PauseSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
165 ResumeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
167 DeleteSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
169 ListSchedules(&'a ProtoListSchedulesRequest),
171 DescribeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
173 Subscribe(SubscriptionScope<'a>, &'a EventFilter),
175 RegisterWorker(&'a ProtoRegisterWorker),
177 Intervene {
181 namespace: &'a str,
183 target: WorkflowTarget<'a>,
185 },
186 ReadDocument {
191 namespace: &'a str,
193 target: WorkflowTarget<'a>,
195 },
196}
197
198impl<'a> NamespaceOperation<'a> {
199 #[must_use]
201 pub const fn start(request: &'a ProtoStartWorkflowRequest) -> Self {
202 Self::StartWorkflow(request)
203 }
204
205 #[must_use]
207 pub const fn signal(request: &'a ProtoSignalRequest, target: WorkflowTarget<'a>) -> Self {
208 Self::Signal(request, target)
209 }
210
211 #[must_use]
213 pub const fn query(request: &'a ProtoQueryRequest, target: WorkflowTarget<'a>) -> Self {
214 Self::Query(request, target)
215 }
216
217 #[must_use]
219 pub const fn cancel(request: &'a ProtoCancelRequest, target: WorkflowTarget<'a>) -> Self {
220 Self::Cancel(request, target)
221 }
222
223 #[must_use]
225 pub const fn reopen(request: &'a ProtoReopenRequest, target: WorkflowTarget<'a>) -> Self {
226 Self::Reopen(request, target)
227 }
228
229 #[must_use]
231 pub const fn pause_workflow(
232 request: &'a ProtoPauseRequest,
233 target: WorkflowTarget<'a>,
234 ) -> Self {
235 Self::PauseWorkflow(request, target)
236 }
237
238 #[must_use]
240 pub const fn resume_workflow(
241 request: &'a ProtoResumeRequest,
242 target: WorkflowTarget<'a>,
243 ) -> Self {
244 Self::ResumeWorkflow(request, target)
245 }
246
247 #[must_use]
249 pub const fn rename_workflow(
250 request: &'a ProtoRenameRequest,
251 target: WorkflowTarget<'a>,
252 ) -> Self {
253 Self::RenameWorkflow(request, target)
254 }
255
256 #[must_use]
258 pub const fn list(request: &'a ProtoListWorkflowsRequest) -> Self {
259 Self::ListWorkflows(request)
260 }
261
262 #[must_use]
264 pub const fn describe(
265 request: &'a ProtoDescribeWorkflowRequest,
266 target: WorkflowTarget<'a>,
267 ) -> Self {
268 Self::Describe(request, target)
269 }
270
271 #[must_use]
274 pub const fn read_document(namespace: &'a str, target: WorkflowTarget<'a>) -> Self {
275 Self::ReadDocument { namespace, target }
276 }
277
278 #[must_use]
280 pub const fn create_schedule(request: &'a ProtoCreateScheduleRequest) -> Self {
281 Self::CreateSchedule(request)
282 }
283
284 #[must_use]
286 pub const fn update_schedule(
287 request: &'a ProtoUpdateScheduleRequest,
288 target: ScheduleTarget<'a>,
289 ) -> Self {
290 Self::UpdateSchedule(request, target)
291 }
292
293 #[must_use]
295 pub const fn pause_schedule(
296 request: &'a ProtoScheduleIdRequest,
297 target: ScheduleTarget<'a>,
298 ) -> Self {
299 Self::PauseSchedule(request, target)
300 }
301
302 #[must_use]
304 pub const fn resume_schedule(
305 request: &'a ProtoScheduleIdRequest,
306 target: ScheduleTarget<'a>,
307 ) -> Self {
308 Self::ResumeSchedule(request, target)
309 }
310
311 #[must_use]
313 pub const fn delete_schedule(
314 request: &'a ProtoScheduleIdRequest,
315 target: ScheduleTarget<'a>,
316 ) -> Self {
317 Self::DeleteSchedule(request, target)
318 }
319
320 #[must_use]
322 pub const fn list_schedules(request: &'a ProtoListSchedulesRequest) -> Self {
323 Self::ListSchedules(request)
324 }
325
326 #[must_use]
328 pub const fn describe_schedule(
329 request: &'a ProtoScheduleIdRequest,
330 target: ScheduleTarget<'a>,
331 ) -> Self {
332 Self::DescribeSchedule(request, target)
333 }
334
335 #[must_use]
337 pub const fn subscribe(scope: SubscriptionScope<'a>, filter: &'a EventFilter) -> Self {
338 Self::Subscribe(scope, filter)
339 }
340
341 #[must_use]
343 pub const fn register_worker(request: &'a ProtoRegisterWorker) -> Self {
344 Self::RegisterWorker(request)
345 }
346
347 #[must_use]
349 pub const fn intervene(namespace: &'a str, target: WorkflowTarget<'a>) -> Self {
350 Self::Intervene { namespace, target }
351 }
352
353 fn requested_namespace(&self) -> &str {
354 match self {
355 Self::StartWorkflow(request) => request.namespace.as_str(),
356 Self::Signal(request, _target) => request.namespace.as_str(),
357 Self::Query(request, _target) => request.namespace.as_str(),
358 Self::Cancel(request, _target) => request.namespace.as_str(),
359 Self::Reopen(request, _target) => request.namespace.as_str(),
360 Self::PauseWorkflow(request, _target) => request.namespace.as_str(),
361 Self::ResumeWorkflow(request, _target) => request.namespace.as_str(),
362 Self::RenameWorkflow(request, _target) => request.namespace.as_str(),
363 Self::ListWorkflows(request) => request.namespace.as_str(),
364 Self::Describe(request, _target) => request.namespace.as_str(),
365 Self::CreateSchedule(request) => request.namespace.as_str(),
366 Self::UpdateSchedule(request, _target) => request.namespace.as_str(),
367 Self::PauseSchedule(request, _target)
368 | Self::ResumeSchedule(request, _target)
369 | Self::DeleteSchedule(request, _target)
370 | Self::DescribeSchedule(request, _target) => request.namespace.as_str(),
371 Self::ListSchedules(request) => request.namespace.as_str(),
372 Self::Subscribe(scope, _filter) => scope.namespace(),
373 Self::RegisterWorker(request) => request.namespaces.first().map_or("", String::as_str),
380 Self::Intervene { namespace, .. } | Self::ReadDocument { namespace, .. } => namespace,
381 }
382 }
383
384 async fn verify(
385 &self,
386 resolver: &NamespaceResolver,
387 authorized_namespace: &str,
388 ) -> Result<(), ServerError> {
389 match self {
390 Self::Signal(_, target)
393 | Self::Query(_, target)
394 | Self::Cancel(_, target)
395 | Self::Reopen(_, target)
396 | Self::PauseWorkflow(_, target)
397 | Self::ResumeWorkflow(_, target)
398 | Self::RenameWorkflow(_, target)
399 | Self::Describe(_, target)
400 | Self::Intervene { target, .. }
401 | Self::ReadDocument { target, .. } => {
402 target.verify(resolver, authorized_namespace).await
403 }
404 Self::UpdateSchedule(_, target)
405 | Self::PauseSchedule(_, target)
406 | Self::ResumeSchedule(_, target)
407 | Self::DeleteSchedule(_, target)
408 | Self::DescribeSchedule(_, target) => {
409 target.verify(resolver, authorized_namespace).await
410 }
411 Self::Subscribe(scope, filter) => {
412 scope.verify(resolver, authorized_namespace, filter).await
413 }
414 Self::StartWorkflow(_)
421 | Self::ListWorkflows(_)
422 | Self::CreateSchedule(_)
423 | Self::ListSchedules(_)
424 | Self::RegisterWorker(_) => Ok(()),
425 }
426 }
427}
428
429#[derive(Clone, Copy)]
431pub struct WorkflowTarget<'a> {
432 workflow_id: &'a WorkflowId,
433 run_id: Option<&'a RunId>,
434}
435
436impl<'a> WorkflowTarget<'a> {
437 #[must_use]
439 pub const fn with_run(workflow_id: &'a WorkflowId, run_id: &'a RunId) -> Self {
440 Self {
441 workflow_id,
442 run_id: Some(run_id),
443 }
444 }
445
446 #[must_use]
448 pub const fn workflow(workflow_id: &'a WorkflowId) -> Self {
449 Self {
450 workflow_id,
451 run_id: None,
452 }
453 }
454
455 #[must_use]
457 pub const fn workflow_id(&self) -> &WorkflowId {
458 self.workflow_id
459 }
460
461 #[must_use]
463 pub const fn run_id(&self) -> Option<&RunId> {
464 self.run_id
465 }
466
467 async fn verify(
468 &self,
469 resolver: &NamespaceResolver,
470 namespace: &str,
471 ) -> Result<(), ServerError> {
472 resolver
473 .verify_workflow_ownership(namespace, self.workflow_id)
474 .await
475 }
476}
477
478#[derive(Clone, Copy)]
480pub struct ScheduleTarget<'a> {
481 schedule_id: &'a ScheduleId,
482}
483
484impl<'a> ScheduleTarget<'a> {
485 #[must_use]
487 pub const fn schedule(schedule_id: &'a ScheduleId) -> Self {
488 Self { schedule_id }
489 }
490
491 #[must_use]
493 pub const fn schedule_id(&self) -> &ScheduleId {
494 self.schedule_id
495 }
496
497 async fn verify(
498 &self,
499 resolver: &NamespaceResolver,
500 namespace: &str,
501 ) -> Result<(), ServerError> {
502 resolver
503 .verify_schedule_ownership(namespace, self.schedule_id)
504 .await
505 }
506}
507
508pub enum SubscriptionScope<'a> {
510 PerWorkflow(&'a PerWorkflowSubscription, WorkflowTarget<'a>),
512 Filtered(&'a FilteredSubscription),
514 Firehose(&'a FirehoseSubscription),
516}
517
518impl<'a> SubscriptionScope<'a> {
519 pub fn from_request(
527 request: &'a SubscriptionRequest,
528 workflow_target: Option<WorkflowTarget<'a>>,
529 ) -> Result<Self, ServerError> {
530 match &request.subscription {
531 Some(subscription_request::Subscription::PerWorkflow(subscription)) => {
532 let target = workflow_target.ok_or_else(|| {
533 ServerError::namespace_denied(
534 "per-workflow subscription target must be decoded before guard scope",
535 )
536 })?;
537 Ok(Self::PerWorkflow(subscription, target))
538 }
539 Some(subscription_request::Subscription::Filtered(subscription)) => {
540 Ok(Self::Filtered(subscription))
541 }
542 Some(subscription_request::Subscription::Firehose(subscription)) => {
543 Ok(Self::Firehose(subscription))
544 }
545 Some(subscription_request::Subscription::Cluster(_)) => {
550 Err(ServerError::namespace_denied(
551 "cluster subscription is deployment-scoped and not served by the namespace guard",
552 ))
553 }
554 Some(subscription_request::Subscription::Transcript(_)) => {
558 Err(ServerError::namespace_denied(
559 "transcript subscription is served by the transcript channel, not this mapper",
560 ))
561 }
562 None => Err(ServerError::namespace_denied(
563 "subscription request must name a namespace",
564 )),
565 }
566 }
567
568 fn namespace(&self) -> &str {
569 match self {
570 Self::PerWorkflow(subscription, _target) => subscription.namespace.as_str(),
571 Self::Filtered(subscription) => subscription.namespace.as_str(),
572 Self::Firehose(subscription) => subscription.namespace.as_str(),
573 }
574 }
575
576 async fn verify(
577 &self,
578 resolver: &NamespaceResolver,
579 namespace: &str,
580 filter: &EventFilter,
581 ) -> Result<(), ServerError> {
582 match self {
583 Self::PerWorkflow(_subscription, target) => {
584 verify_subscription_filter_target(filter, Some(*target), resolver, namespace).await
585 }
586 Self::Filtered(subscription) => {
587 verify_namespace_selector(subscription.namespace_selector.as_deref(), namespace)?;
588 verify_subscription_filter_target(filter, None, resolver, namespace).await
589 }
590 Self::Firehose(_) => {
591 verify_subscription_filter_target(filter, None, resolver, namespace).await
592 }
593 }
594 }
595}
596
597fn verify_namespace_selector(selector: Option<&str>, namespace: &str) -> Result<(), ServerError> {
598 match selector {
599 Some(selector) if selector != namespace => Err(ServerError::namespace_denied(
600 "subscription namespace selector is not authorized",
601 )),
602 Some(_) | None => Ok(()),
603 }
604}
605
606async fn verify_subscription_filter_target(
607 filter: &EventFilter,
608 explicit_target: Option<WorkflowTarget<'_>>,
609 resolver: &NamespaceResolver,
610 namespace: &str,
611) -> Result<(), ServerError> {
612 if let Some(target) = explicit_target {
613 if filter
614 .workflow_id
615 .as_ref()
616 .is_some_and(|workflow_id| workflow_id != target.workflow_id())
617 {
618 return Err(ServerError::namespace_denied(
619 "subscription filter workflow does not match decoded target",
620 ));
621 }
622 target.verify(resolver, namespace).await
623 } else if let Some(workflow_id) = &filter.workflow_id {
624 resolver
625 .verify_workflow_ownership(namespace, workflow_id)
626 .await
627 } else {
628 Ok(())
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use std::sync::atomic::{AtomicUsize, Ordering};
635 use std::sync::{Arc, Mutex};
636
637 use aion_core::{RunId, ScheduleId, WorkflowId};
638 use aion_proto::{
639 FilteredSubscription, FirehoseSubscription, PerWorkflowSubscription, ProtoCancelRequest,
640 ProtoCreateScheduleRequest, ProtoDescribeWorkflowRequest, ProtoListSchedulesRequest,
641 ProtoListWorkflowsRequest, ProtoQueryRequest, ProtoRegisterWorker, ProtoReopenRequest,
642 ProtoScheduleIdRequest, ProtoSignalRequest, ProtoStartWorkflowRequest,
643 ProtoUpdateScheduleRequest,
644 };
645 use async_trait::async_trait;
646
647 use super::{
648 NamespaceGuard, NamespaceOperation, ScheduleTarget, SubscriptionScope, WorkflowTarget,
649 };
650 use crate::config::NamespaceMode;
651 use crate::error::ServerError;
652 use crate::namespace::{
653 CallerIdentity, NamespaceResolver, ScheduleNamespaceSource, StaticScheduleNamespaces,
654 StaticWorkflowNamespaces,
655 };
656
657 struct RecordingFakeEngine {
658 calls: Mutex<Vec<&'static str>>,
659 }
660
661 impl RecordingFakeEngine {
662 fn new() -> Self {
663 Self {
664 calls: Mutex::new(Vec::new()),
665 }
666 }
667
668 fn calls(&self) -> Result<Vec<&'static str>, Box<dyn std::error::Error>> {
669 let calls = self
670 .calls
671 .lock()
672 .map_err(|_| "fake engine calls lock poisoned")?;
673 Ok(calls.clone())
674 }
675 }
676
677 #[derive(Clone)]
682 struct CountingScheduleNamespaces {
683 inner: StaticScheduleNamespaces,
684 calls: Arc<AtomicUsize>,
685 }
686
687 impl CountingScheduleNamespaces {
688 fn wrapping(inner: StaticScheduleNamespaces) -> Self {
689 Self {
690 inner,
691 calls: Arc::new(AtomicUsize::new(0)),
692 }
693 }
694
695 fn calls(&self) -> usize {
696 self.calls.load(Ordering::SeqCst)
697 }
698 }
699
700 #[async_trait]
701 impl ScheduleNamespaceSource for CountingScheduleNamespaces {
702 async fn schedule_namespace(
703 &self,
704 schedule_id: &ScheduleId,
705 ) -> Result<Option<String>, ServerError> {
706 self.calls.fetch_add(1, Ordering::SeqCst);
707 self.inner.schedule_namespace(schedule_id).await
708 }
709 }
710
711 fn guard_with_ownership(ownership: StaticWorkflowNamespaces) -> NamespaceGuard {
712 let resolver = NamespaceResolver::authorization_only(
713 NamespaceMode::SharedEngine,
714 ownership,
715 StaticScheduleNamespaces::default(),
716 );
717 NamespaceGuard::new(resolver)
718 }
719
720 fn guard_with_schedule_ownership(
721 schedule_ownership: impl ScheduleNamespaceSource + 'static,
722 ) -> NamespaceGuard {
723 let resolver = NamespaceResolver::authorization_only(
724 NamespaceMode::SharedEngine,
725 StaticWorkflowNamespaces::default(),
726 schedule_ownership,
727 );
728 NamespaceGuard::new(resolver)
729 }
730
731 fn caller() -> CallerIdentity {
732 CallerIdentity::new("alice", [String::from("tenant-a")])
733 }
734
735 fn workflow_ids() -> (WorkflowId, RunId) {
736 (
737 WorkflowId::new(uuid::Uuid::from_u128(1)),
738 RunId::new(uuid::Uuid::from_u128(2)),
739 )
740 }
741
742 #[tokio::test]
743 async fn denied_targeted_operations_do_not_call_engine()
744 -> Result<(), Box<dyn std::error::Error>> {
745 let (workflow_id, run_id) = workflow_ids();
746 let ownership = StaticWorkflowNamespaces::default();
747 ownership.record(workflow_id.clone(), "tenant-b")?;
748 let guard = guard_with_ownership(ownership);
749 let fake = RecordingFakeEngine::new();
750 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
751
752 let signal = ProtoSignalRequest {
753 namespace: String::from("tenant-a"),
754 workflow_id: None,
755 run_id: None,
756 signal_name: String::from("ship"),
757 payload: None,
758 };
759 let query = ProtoQueryRequest {
760 namespace: String::from("tenant-a"),
761 workflow_id: None,
762 run_id: None,
763 query_name: String::from("state"),
764 arguments: None,
765 };
766 let cancel = ProtoCancelRequest {
767 namespace: String::from("tenant-a"),
768 workflow_id: None,
769 run_id: None,
770 reason: String::from("operator"),
771 };
772 let reopen = ProtoReopenRequest {
773 namespace: String::from("tenant-a"),
774 workflow_id: None,
775 run_id: None,
776 };
777 let describe = ProtoDescribeWorkflowRequest {
778 namespace: String::from("tenant-a"),
779 workflow_id: None,
780 run_id: None,
781 include_history: false,
782 };
783
784 let operations = [
785 NamespaceOperation::signal(&signal, target),
786 NamespaceOperation::query(&query, target),
787 NamespaceOperation::cancel(&cancel, target),
788 NamespaceOperation::reopen(&reopen, target),
789 NamespaceOperation::describe(&describe, target),
790 ];
791
792 for operation in operations {
793 let result = guard.scope(&caller(), &operation).await;
794 assert_eq!(
798 result.err().map(|error| error.to_wire_error().code),
799 Some(aion_proto::WireErrorCode::NotFound)
800 );
801 }
802 assert!(fake.calls()?.is_empty());
803 Ok(())
804 }
805
806 #[tokio::test]
807 async fn denied_list_and_worker_scope_do_not_call_engine()
808 -> Result<(), Box<dyn std::error::Error>> {
809 let (workflow_id, _run_id) = workflow_ids();
810 let ownership = StaticWorkflowNamespaces::default();
811 ownership.record(workflow_id, "tenant-b")?;
812 let guard = guard_with_ownership(ownership);
813 let fake = RecordingFakeEngine::new();
814
815 let list = ProtoListWorkflowsRequest {
816 namespace: String::from("tenant-b"),
817 request: None,
818 };
819 let worker = ProtoRegisterWorker {
820 namespaces: vec![String::from("tenant-b")],
821 activity_types: vec![String::from("ship")],
822 task_queue: String::new(),
823 node: String::new(),
824 activities: Vec::new(),
825 identity: String::new(),
826 instance: None,
827 max_concurrency: Some(4),
828 };
829
830 assert!(
831 guard
832 .scope(&caller(), &NamespaceOperation::list(&list))
833 .await
834 .is_err()
835 );
836 assert!(
837 guard
838 .scope(&caller(), &NamespaceOperation::register_worker(&worker),)
839 .await
840 .is_err()
841 );
842 assert!(fake.calls()?.is_empty());
843 Ok(())
844 }
845
846 #[tokio::test]
847 async fn denied_subscriptions_do_not_call_engine() -> Result<(), Box<dyn std::error::Error>> {
848 let (workflow_id, run_id) = workflow_ids();
849 let ownership = StaticWorkflowNamespaces::default();
850 ownership.record(workflow_id.clone(), "tenant-b")?;
851 let guard = guard_with_ownership(ownership);
852 let fake = RecordingFakeEngine::new();
853 let event_filter = aion::EventFilter::default();
854
855 let filtered = FilteredSubscription {
856 namespace: String::from("tenant-a"),
857 workflow_type: None,
858 status: None,
859 namespace_selector: Some(String::from("tenant-b")),
860 };
861 let filtered_by_workflow = FilteredSubscription {
862 namespace: String::from("tenant-a"),
863 workflow_type: None,
864 status: None,
865 namespace_selector: None,
866 };
867 let per_workflow = PerWorkflowSubscription {
868 namespace: String::from("tenant-a"),
869 workflow_id: None,
870 resume_from_seq: None,
871 };
872 let cross_namespace_filter = aion::EventFilter {
873 workflow_id: Some(workflow_id.clone()),
874 run: None,
875 family: None,
876 };
877 let firehose = FirehoseSubscription {
878 namespace: String::from("tenant-b"),
879 };
880
881 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
882 let denied_subscriptions = [
886 (
887 NamespaceOperation::subscribe(
888 SubscriptionScope::Filtered(&filtered),
889 &event_filter,
890 ),
891 aion_proto::WireErrorCode::NamespaceDenied,
892 ),
893 (
894 NamespaceOperation::subscribe(
895 SubscriptionScope::Filtered(&filtered_by_workflow),
896 &cross_namespace_filter,
897 ),
898 aion_proto::WireErrorCode::NotFound,
899 ),
900 (
901 NamespaceOperation::subscribe(
902 SubscriptionScope::PerWorkflow(&per_workflow, target),
903 &cross_namespace_filter,
904 ),
905 aion_proto::WireErrorCode::NotFound,
906 ),
907 (
908 NamespaceOperation::subscribe(
909 SubscriptionScope::Firehose(&firehose),
910 &event_filter,
911 ),
912 aion_proto::WireErrorCode::NamespaceDenied,
913 ),
914 ];
915
916 for (operation, expected_code) in &denied_subscriptions {
917 assert_eq!(
918 guard
919 .scope(&caller(), operation)
920 .await
921 .err()
922 .map(|error| error.to_wire_error().code)
923 .as_ref(),
924 Some(expected_code)
925 );
926 }
927 assert!(fake.calls()?.is_empty());
928 Ok(())
929 }
930
931 fn schedule_id() -> ScheduleId {
932 ScheduleId::new(uuid::Uuid::from_u128(9))
933 }
934
935 fn schedule_id_request(namespace: &str) -> ProtoScheduleIdRequest {
936 ProtoScheduleIdRequest {
937 namespace: namespace.to_owned(),
938 schedule_id: None,
939 }
940 }
941
942 #[tokio::test]
943 async fn schedule_ownership_misses_are_not_found_and_do_not_call_engine()
944 -> Result<(), Box<dyn std::error::Error>> {
945 let schedule_id = schedule_id();
946 let schedule_ownership = StaticScheduleNamespaces::default();
947 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
948 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
949 let guard = guard_with_schedule_ownership(counting.clone());
953 let target = ScheduleTarget::schedule(&schedule_id);
954
955 let update = ProtoUpdateScheduleRequest {
956 namespace: String::from("tenant-a"),
957 schedule_id: None,
958 config: None,
959 };
960 let id_request = schedule_id_request("tenant-a");
961
962 let operations = [
963 NamespaceOperation::update_schedule(&update, target),
964 NamespaceOperation::pause_schedule(&id_request, target),
965 NamespaceOperation::resume_schedule(&id_request, target),
966 NamespaceOperation::delete_schedule(&id_request, target),
967 NamespaceOperation::describe_schedule(&id_request, target),
968 ];
969 let operation_count = operations.len();
970
971 for operation in operations {
972 let result = guard.scope(&caller(), &operation).await;
973 let error = result
977 .err()
978 .map(|error| error.to_wire_error())
979 .ok_or("expected foreign-owned schedule to be rejected")?;
980 assert_eq!(error.code, aion_proto::WireErrorCode::NotFound);
981 assert_eq!(error.message, "schedule not found in namespace tenant-a");
982 }
983 assert_eq!(counting.calls(), operation_count);
986 Ok(())
987 }
988
989 #[tokio::test]
990 async fn ungranted_schedule_operations_are_namespace_denied()
991 -> Result<(), Box<dyn std::error::Error>> {
992 let schedule_id = schedule_id();
993 let schedule_ownership = StaticScheduleNamespaces::default();
994 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
995 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
996 let guard = guard_with_schedule_ownership(counting.clone());
997 let target = ScheduleTarget::schedule(&schedule_id);
998
999 let create = ProtoCreateScheduleRequest {
1000 namespace: String::from("tenant-b"),
1001 config: None,
1002 };
1003 let update = ProtoUpdateScheduleRequest {
1004 namespace: String::from("tenant-b"),
1005 schedule_id: None,
1006 config: None,
1007 };
1008 let id_request = schedule_id_request("tenant-b");
1009 let list = ProtoListSchedulesRequest {
1010 namespace: String::from("tenant-b"),
1011 };
1012
1013 let operations = [
1014 NamespaceOperation::create_schedule(&create),
1015 NamespaceOperation::update_schedule(&update, target),
1016 NamespaceOperation::pause_schedule(&id_request, target),
1017 NamespaceOperation::resume_schedule(&id_request, target),
1018 NamespaceOperation::delete_schedule(&id_request, target),
1019 NamespaceOperation::describe_schedule(&id_request, target),
1020 NamespaceOperation::list_schedules(&list),
1021 ];
1022
1023 for operation in operations {
1027 let result = guard.scope(&caller(), &operation).await;
1028 assert_eq!(
1029 result.err().map(|error| error.to_wire_error().code),
1030 Some(aion_proto::WireErrorCode::NamespaceDenied)
1031 );
1032 }
1033 assert_eq!(counting.calls(), 0);
1036 Ok(())
1037 }
1038
1039 #[tokio::test]
1040 async fn granted_schedule_create_and_list_return_scoped_engine()
1041 -> Result<(), Box<dyn std::error::Error>> {
1042 let guard = guard_with_schedule_ownership(StaticScheduleNamespaces::default());
1043 let create = ProtoCreateScheduleRequest {
1044 namespace: String::from("tenant-a"),
1045 config: None,
1046 };
1047 let list = ProtoListSchedulesRequest {
1048 namespace: String::from("tenant-a"),
1049 };
1050
1051 let scoped_create = guard
1052 .scope(&caller(), &NamespaceOperation::create_schedule(&create))
1053 .await?;
1054 let scoped_list = guard
1055 .scope(&caller(), &NamespaceOperation::list_schedules(&list))
1056 .await?;
1057
1058 assert_eq!(scoped_create.namespace(), "tenant-a");
1059 assert_eq!(scoped_list.namespace(), "tenant-a");
1060 Ok(())
1061 }
1062
1063 #[tokio::test]
1064 async fn authorized_start_returns_scoped_engine() -> Result<(), Box<dyn std::error::Error>> {
1065 let guard = guard_with_ownership(StaticWorkflowNamespaces::default());
1066 let request = ProtoStartWorkflowRequest {
1067 namespace: String::from("tenant-a"),
1068 workflow_type: String::from("checkout"),
1069 input: None,
1070 routing_key: None,
1071 task_queue: None,
1072 display_name: None,
1073 };
1074
1075 let scoped = guard
1076 .scope(&caller(), &NamespaceOperation::start(&request))
1077 .await?;
1078
1079 assert_eq!(scoped.namespace(), "tenant-a");
1080 Ok(())
1081 }
1082
1083 #[tokio::test]
1084 async fn single_tenant_mode_authorizes_configured_namespace()
1085 -> Result<(), Box<dyn std::error::Error>> {
1086 let resolver = NamespaceResolver::authorization_only(
1087 NamespaceMode::SingleTenant {
1088 namespace: String::from("tenant-a"),
1089 },
1090 StaticWorkflowNamespaces::default(),
1091 StaticScheduleNamespaces::default(),
1092 );
1093 let guard = NamespaceGuard::new(resolver);
1094 let request = ProtoRegisterWorker {
1095 namespaces: vec![String::from("tenant-a")],
1096 activity_types: Vec::new(),
1097 task_queue: String::new(),
1098 node: String::new(),
1099 activities: Vec::new(),
1100 identity: String::new(),
1101 instance: None,
1102 max_concurrency: Some(4),
1103 };
1104
1105 let scoped = guard
1106 .scope(
1107 &CallerIdentity::new("single-tenant", Vec::<String>::new()),
1108 &NamespaceOperation::register_worker(&request),
1109 )
1110 .await?;
1111
1112 assert_eq!(scoped.namespace(), "tenant-a");
1113 Ok(())
1114 }
1115}