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