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 };
828
829 assert!(
830 guard
831 .scope(&caller(), &NamespaceOperation::list(&list))
832 .await
833 .is_err()
834 );
835 assert!(
836 guard
837 .scope(&caller(), &NamespaceOperation::register_worker(&worker),)
838 .await
839 .is_err()
840 );
841 assert!(fake.calls()?.is_empty());
842 Ok(())
843 }
844
845 #[tokio::test]
846 async fn denied_subscriptions_do_not_call_engine() -> Result<(), Box<dyn std::error::Error>> {
847 let (workflow_id, run_id) = workflow_ids();
848 let ownership = StaticWorkflowNamespaces::default();
849 ownership.record(workflow_id.clone(), "tenant-b")?;
850 let guard = guard_with_ownership(ownership);
851 let fake = RecordingFakeEngine::new();
852 let event_filter = aion::EventFilter::default();
853
854 let filtered = FilteredSubscription {
855 namespace: String::from("tenant-a"),
856 workflow_type: None,
857 status: None,
858 namespace_selector: Some(String::from("tenant-b")),
859 };
860 let filtered_by_workflow = FilteredSubscription {
861 namespace: String::from("tenant-a"),
862 workflow_type: None,
863 status: None,
864 namespace_selector: None,
865 };
866 let per_workflow = PerWorkflowSubscription {
867 namespace: String::from("tenant-a"),
868 workflow_id: None,
869 resume_from_seq: None,
870 };
871 let cross_namespace_filter = aion::EventFilter {
872 workflow_id: Some(workflow_id.clone()),
873 run: None,
874 family: None,
875 };
876 let firehose = FirehoseSubscription {
877 namespace: String::from("tenant-b"),
878 };
879
880 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
881 let denied_subscriptions = [
885 (
886 NamespaceOperation::subscribe(
887 SubscriptionScope::Filtered(&filtered),
888 &event_filter,
889 ),
890 aion_proto::WireErrorCode::NamespaceDenied,
891 ),
892 (
893 NamespaceOperation::subscribe(
894 SubscriptionScope::Filtered(&filtered_by_workflow),
895 &cross_namespace_filter,
896 ),
897 aion_proto::WireErrorCode::NotFound,
898 ),
899 (
900 NamespaceOperation::subscribe(
901 SubscriptionScope::PerWorkflow(&per_workflow, target),
902 &cross_namespace_filter,
903 ),
904 aion_proto::WireErrorCode::NotFound,
905 ),
906 (
907 NamespaceOperation::subscribe(
908 SubscriptionScope::Firehose(&firehose),
909 &event_filter,
910 ),
911 aion_proto::WireErrorCode::NamespaceDenied,
912 ),
913 ];
914
915 for (operation, expected_code) in &denied_subscriptions {
916 assert_eq!(
917 guard
918 .scope(&caller(), operation)
919 .await
920 .err()
921 .map(|error| error.to_wire_error().code)
922 .as_ref(),
923 Some(expected_code)
924 );
925 }
926 assert!(fake.calls()?.is_empty());
927 Ok(())
928 }
929
930 fn schedule_id() -> ScheduleId {
931 ScheduleId::new(uuid::Uuid::from_u128(9))
932 }
933
934 fn schedule_id_request(namespace: &str) -> ProtoScheduleIdRequest {
935 ProtoScheduleIdRequest {
936 namespace: namespace.to_owned(),
937 schedule_id: None,
938 }
939 }
940
941 #[tokio::test]
942 async fn schedule_ownership_misses_are_not_found_and_do_not_call_engine()
943 -> Result<(), Box<dyn std::error::Error>> {
944 let schedule_id = schedule_id();
945 let schedule_ownership = StaticScheduleNamespaces::default();
946 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
947 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
948 let guard = guard_with_schedule_ownership(counting.clone());
952 let target = ScheduleTarget::schedule(&schedule_id);
953
954 let update = ProtoUpdateScheduleRequest {
955 namespace: String::from("tenant-a"),
956 schedule_id: None,
957 config: None,
958 };
959 let id_request = schedule_id_request("tenant-a");
960
961 let operations = [
962 NamespaceOperation::update_schedule(&update, target),
963 NamespaceOperation::pause_schedule(&id_request, target),
964 NamespaceOperation::resume_schedule(&id_request, target),
965 NamespaceOperation::delete_schedule(&id_request, target),
966 NamespaceOperation::describe_schedule(&id_request, target),
967 ];
968 let operation_count = operations.len();
969
970 for operation in operations {
971 let result = guard.scope(&caller(), &operation).await;
972 let error = result
976 .err()
977 .map(|error| error.to_wire_error())
978 .ok_or("expected foreign-owned schedule to be rejected")?;
979 assert_eq!(error.code, aion_proto::WireErrorCode::NotFound);
980 assert_eq!(error.message, "schedule not found in namespace tenant-a");
981 }
982 assert_eq!(counting.calls(), operation_count);
985 Ok(())
986 }
987
988 #[tokio::test]
989 async fn ungranted_schedule_operations_are_namespace_denied()
990 -> Result<(), Box<dyn std::error::Error>> {
991 let schedule_id = schedule_id();
992 let schedule_ownership = StaticScheduleNamespaces::default();
993 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
994 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
995 let guard = guard_with_schedule_ownership(counting.clone());
996 let target = ScheduleTarget::schedule(&schedule_id);
997
998 let create = ProtoCreateScheduleRequest {
999 namespace: String::from("tenant-b"),
1000 config: None,
1001 };
1002 let update = ProtoUpdateScheduleRequest {
1003 namespace: String::from("tenant-b"),
1004 schedule_id: None,
1005 config: None,
1006 };
1007 let id_request = schedule_id_request("tenant-b");
1008 let list = ProtoListSchedulesRequest {
1009 namespace: String::from("tenant-b"),
1010 };
1011
1012 let operations = [
1013 NamespaceOperation::create_schedule(&create),
1014 NamespaceOperation::update_schedule(&update, target),
1015 NamespaceOperation::pause_schedule(&id_request, target),
1016 NamespaceOperation::resume_schedule(&id_request, target),
1017 NamespaceOperation::delete_schedule(&id_request, target),
1018 NamespaceOperation::describe_schedule(&id_request, target),
1019 NamespaceOperation::list_schedules(&list),
1020 ];
1021
1022 for operation in operations {
1026 let result = guard.scope(&caller(), &operation).await;
1027 assert_eq!(
1028 result.err().map(|error| error.to_wire_error().code),
1029 Some(aion_proto::WireErrorCode::NamespaceDenied)
1030 );
1031 }
1032 assert_eq!(counting.calls(), 0);
1035 Ok(())
1036 }
1037
1038 #[tokio::test]
1039 async fn granted_schedule_create_and_list_return_scoped_engine()
1040 -> Result<(), Box<dyn std::error::Error>> {
1041 let guard = guard_with_schedule_ownership(StaticScheduleNamespaces::default());
1042 let create = ProtoCreateScheduleRequest {
1043 namespace: String::from("tenant-a"),
1044 config: None,
1045 };
1046 let list = ProtoListSchedulesRequest {
1047 namespace: String::from("tenant-a"),
1048 };
1049
1050 let scoped_create = guard
1051 .scope(&caller(), &NamespaceOperation::create_schedule(&create))
1052 .await?;
1053 let scoped_list = guard
1054 .scope(&caller(), &NamespaceOperation::list_schedules(&list))
1055 .await?;
1056
1057 assert_eq!(scoped_create.namespace(), "tenant-a");
1058 assert_eq!(scoped_list.namespace(), "tenant-a");
1059 Ok(())
1060 }
1061
1062 #[tokio::test]
1063 async fn authorized_start_returns_scoped_engine() -> Result<(), Box<dyn std::error::Error>> {
1064 let guard = guard_with_ownership(StaticWorkflowNamespaces::default());
1065 let request = ProtoStartWorkflowRequest {
1066 namespace: String::from("tenant-a"),
1067 workflow_type: String::from("checkout"),
1068 input: None,
1069 routing_key: None,
1070 task_queue: None,
1071 display_name: None,
1072 };
1073
1074 let scoped = guard
1075 .scope(&caller(), &NamespaceOperation::start(&request))
1076 .await?;
1077
1078 assert_eq!(scoped.namespace(), "tenant-a");
1079 Ok(())
1080 }
1081
1082 #[tokio::test]
1083 async fn single_tenant_mode_authorizes_configured_namespace()
1084 -> Result<(), Box<dyn std::error::Error>> {
1085 let resolver = NamespaceResolver::authorization_only(
1086 NamespaceMode::SingleTenant {
1087 namespace: String::from("tenant-a"),
1088 },
1089 StaticWorkflowNamespaces::default(),
1090 StaticScheduleNamespaces::default(),
1091 );
1092 let guard = NamespaceGuard::new(resolver);
1093 let request = ProtoRegisterWorker {
1094 namespaces: vec![String::from("tenant-a")],
1095 activity_types: Vec::new(),
1096 task_queue: String::new(),
1097 node: String::new(),
1098 activities: Vec::new(),
1099 identity: String::new(),
1100 instance: None,
1101 };
1102
1103 let scoped = guard
1104 .scope(
1105 &CallerIdentity::new("single-tenant", Vec::<String>::new()),
1106 &NamespaceOperation::register_worker(&request),
1107 )
1108 .await?;
1109
1110 assert_eq!(scoped.namespace(), "tenant-a");
1111 Ok(())
1112 }
1113}