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 arguments: None,
726 };
727 let cancel = ProtoCancelRequest {
728 namespace: String::from("tenant-a"),
729 workflow_id: None,
730 run_id: None,
731 reason: String::from("operator"),
732 };
733 let reopen = ProtoReopenRequest {
734 namespace: String::from("tenant-a"),
735 workflow_id: None,
736 run_id: None,
737 };
738 let describe = ProtoDescribeWorkflowRequest {
739 namespace: String::from("tenant-a"),
740 workflow_id: None,
741 run_id: None,
742 include_history: false,
743 };
744
745 let operations = [
746 NamespaceOperation::signal(&signal, target),
747 NamespaceOperation::query(&query, target),
748 NamespaceOperation::cancel(&cancel, target),
749 NamespaceOperation::reopen(&reopen, target),
750 NamespaceOperation::describe(&describe, target),
751 ];
752
753 for operation in operations {
754 let result = guard.scope(&caller(), &operation).await;
755 assert_eq!(
759 result.err().map(|error| error.to_wire_error().code),
760 Some(aion_proto::WireErrorCode::NotFound)
761 );
762 }
763 assert!(fake.calls()?.is_empty());
764 Ok(())
765 }
766
767 #[tokio::test]
768 async fn denied_list_and_worker_scope_do_not_call_engine()
769 -> Result<(), Box<dyn std::error::Error>> {
770 let (workflow_id, _run_id) = workflow_ids();
771 let ownership = StaticWorkflowNamespaces::default();
772 ownership.record(workflow_id, "tenant-b")?;
773 let guard = guard_with_ownership(ownership);
774 let fake = RecordingFakeEngine::new();
775 let filter = WorkflowFilter::default();
776
777 let list = ProtoListWorkflowsRequest {
778 namespace: String::from("tenant-b"),
779 filter: None,
780 };
781 let worker = ProtoRegisterWorker {
782 namespaces: vec![String::from("tenant-b")],
783 activity_types: vec![String::from("ship")],
784 task_queue: String::new(),
785 node: String::new(),
786 activities: Vec::new(),
787 identity: String::new(),
788 instance: None,
789 };
790
791 assert!(
792 guard
793 .scope(&caller(), &NamespaceOperation::list(&list, &filter))
794 .await
795 .is_err()
796 );
797 assert!(
798 guard
799 .scope(&caller(), &NamespaceOperation::register_worker(&worker),)
800 .await
801 .is_err()
802 );
803 assert!(fake.calls()?.is_empty());
804 Ok(())
805 }
806
807 #[tokio::test]
808 async fn denied_subscriptions_do_not_call_engine() -> Result<(), Box<dyn std::error::Error>> {
809 let (workflow_id, run_id) = workflow_ids();
810 let ownership = StaticWorkflowNamespaces::default();
811 ownership.record(workflow_id.clone(), "tenant-b")?;
812 let guard = guard_with_ownership(ownership);
813 let fake = RecordingFakeEngine::new();
814 let event_filter = aion::EventFilter::default();
815
816 let filtered = FilteredSubscription {
817 namespace: String::from("tenant-a"),
818 workflow_type: None,
819 status: None,
820 namespace_selector: Some(String::from("tenant-b")),
821 };
822 let filtered_by_workflow = FilteredSubscription {
823 namespace: String::from("tenant-a"),
824 workflow_type: None,
825 status: None,
826 namespace_selector: None,
827 };
828 let per_workflow = PerWorkflowSubscription {
829 namespace: String::from("tenant-a"),
830 workflow_id: None,
831 resume_from_seq: None,
832 };
833 let cross_namespace_filter = aion::EventFilter {
834 workflow_id: Some(workflow_id.clone()),
835 run: None,
836 family: None,
837 };
838 let firehose = FirehoseSubscription {
839 namespace: String::from("tenant-b"),
840 };
841
842 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
843 let denied_subscriptions = [
847 (
848 NamespaceOperation::subscribe(
849 SubscriptionScope::Filtered(&filtered),
850 &event_filter,
851 ),
852 aion_proto::WireErrorCode::NamespaceDenied,
853 ),
854 (
855 NamespaceOperation::subscribe(
856 SubscriptionScope::Filtered(&filtered_by_workflow),
857 &cross_namespace_filter,
858 ),
859 aion_proto::WireErrorCode::NotFound,
860 ),
861 (
862 NamespaceOperation::subscribe(
863 SubscriptionScope::PerWorkflow(&per_workflow, target),
864 &cross_namespace_filter,
865 ),
866 aion_proto::WireErrorCode::NotFound,
867 ),
868 (
869 NamespaceOperation::subscribe(
870 SubscriptionScope::Firehose(&firehose),
871 &event_filter,
872 ),
873 aion_proto::WireErrorCode::NamespaceDenied,
874 ),
875 ];
876
877 for (operation, expected_code) in &denied_subscriptions {
878 assert_eq!(
879 guard
880 .scope(&caller(), operation)
881 .await
882 .err()
883 .map(|error| error.to_wire_error().code)
884 .as_ref(),
885 Some(expected_code)
886 );
887 }
888 assert!(fake.calls()?.is_empty());
889 Ok(())
890 }
891
892 fn schedule_id() -> ScheduleId {
893 ScheduleId::new(uuid::Uuid::from_u128(9))
894 }
895
896 fn schedule_id_request(namespace: &str) -> ProtoScheduleIdRequest {
897 ProtoScheduleIdRequest {
898 namespace: namespace.to_owned(),
899 schedule_id: None,
900 }
901 }
902
903 #[tokio::test]
904 async fn schedule_ownership_misses_are_not_found_and_do_not_call_engine()
905 -> Result<(), Box<dyn std::error::Error>> {
906 let schedule_id = schedule_id();
907 let schedule_ownership = StaticScheduleNamespaces::default();
908 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
909 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
910 let guard = guard_with_schedule_ownership(counting.clone());
914 let target = ScheduleTarget::schedule(&schedule_id);
915
916 let update = ProtoUpdateScheduleRequest {
917 namespace: String::from("tenant-a"),
918 schedule_id: None,
919 config: None,
920 };
921 let id_request = schedule_id_request("tenant-a");
922
923 let operations = [
924 NamespaceOperation::update_schedule(&update, target),
925 NamespaceOperation::pause_schedule(&id_request, target),
926 NamespaceOperation::resume_schedule(&id_request, target),
927 NamespaceOperation::delete_schedule(&id_request, target),
928 NamespaceOperation::describe_schedule(&id_request, target),
929 ];
930 let operation_count = operations.len();
931
932 for operation in operations {
933 let result = guard.scope(&caller(), &operation).await;
934 let error = result
938 .err()
939 .map(|error| error.to_wire_error())
940 .ok_or("expected foreign-owned schedule to be rejected")?;
941 assert_eq!(error.code, aion_proto::WireErrorCode::NotFound);
942 assert_eq!(error.message, "schedule not found in namespace tenant-a");
943 }
944 assert_eq!(counting.calls(), operation_count);
947 Ok(())
948 }
949
950 #[tokio::test]
951 async fn ungranted_schedule_operations_are_namespace_denied()
952 -> Result<(), Box<dyn std::error::Error>> {
953 let schedule_id = schedule_id();
954 let schedule_ownership = StaticScheduleNamespaces::default();
955 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
956 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
957 let guard = guard_with_schedule_ownership(counting.clone());
958 let target = ScheduleTarget::schedule(&schedule_id);
959
960 let create = ProtoCreateScheduleRequest {
961 namespace: String::from("tenant-b"),
962 config: None,
963 };
964 let update = ProtoUpdateScheduleRequest {
965 namespace: String::from("tenant-b"),
966 schedule_id: None,
967 config: None,
968 };
969 let id_request = schedule_id_request("tenant-b");
970 let list = ProtoListSchedulesRequest {
971 namespace: String::from("tenant-b"),
972 };
973
974 let operations = [
975 NamespaceOperation::create_schedule(&create),
976 NamespaceOperation::update_schedule(&update, target),
977 NamespaceOperation::pause_schedule(&id_request, target),
978 NamespaceOperation::resume_schedule(&id_request, target),
979 NamespaceOperation::delete_schedule(&id_request, target),
980 NamespaceOperation::describe_schedule(&id_request, target),
981 NamespaceOperation::list_schedules(&list),
982 ];
983
984 for operation in operations {
988 let result = guard.scope(&caller(), &operation).await;
989 assert_eq!(
990 result.err().map(|error| error.to_wire_error().code),
991 Some(aion_proto::WireErrorCode::NamespaceDenied)
992 );
993 }
994 assert_eq!(counting.calls(), 0);
997 Ok(())
998 }
999
1000 #[tokio::test]
1001 async fn granted_schedule_create_and_list_return_scoped_engine()
1002 -> Result<(), Box<dyn std::error::Error>> {
1003 let guard = guard_with_schedule_ownership(StaticScheduleNamespaces::default());
1004 let create = ProtoCreateScheduleRequest {
1005 namespace: String::from("tenant-a"),
1006 config: None,
1007 };
1008 let list = ProtoListSchedulesRequest {
1009 namespace: String::from("tenant-a"),
1010 };
1011
1012 let scoped_create = guard
1013 .scope(&caller(), &NamespaceOperation::create_schedule(&create))
1014 .await?;
1015 let scoped_list = guard
1016 .scope(&caller(), &NamespaceOperation::list_schedules(&list))
1017 .await?;
1018
1019 assert_eq!(scoped_create.namespace(), "tenant-a");
1020 assert_eq!(scoped_list.namespace(), "tenant-a");
1021 Ok(())
1022 }
1023
1024 #[tokio::test]
1025 async fn authorized_start_returns_scoped_engine() -> Result<(), Box<dyn std::error::Error>> {
1026 let guard = guard_with_ownership(StaticWorkflowNamespaces::default());
1027 let request = ProtoStartWorkflowRequest {
1028 namespace: String::from("tenant-a"),
1029 workflow_type: String::from("checkout"),
1030 input: None,
1031 routing_key: None,
1032 task_queue: None,
1033 };
1034
1035 let scoped = guard
1036 .scope(&caller(), &NamespaceOperation::start(&request))
1037 .await?;
1038
1039 assert_eq!(scoped.namespace(), "tenant-a");
1040 Ok(())
1041 }
1042
1043 #[tokio::test]
1044 async fn single_tenant_mode_authorizes_configured_namespace()
1045 -> Result<(), Box<dyn std::error::Error>> {
1046 let resolver = NamespaceResolver::authorization_only(
1047 NamespaceMode::SingleTenant {
1048 namespace: String::from("tenant-a"),
1049 },
1050 StaticWorkflowNamespaces::default(),
1051 StaticScheduleNamespaces::default(),
1052 );
1053 let guard = NamespaceGuard::new(resolver);
1054 let request = ProtoRegisterWorker {
1055 namespaces: vec![String::from("tenant-a")],
1056 activity_types: Vec::new(),
1057 task_queue: String::new(),
1058 node: String::new(),
1059 activities: Vec::new(),
1060 identity: String::new(),
1061 instance: None,
1062 };
1063
1064 let scoped = guard
1065 .scope(
1066 &CallerIdentity::new("single-tenant", Vec::<String>::new()),
1067 &NamespaceOperation::register_worker(&request),
1068 )
1069 .await?;
1070
1071 assert_eq!(scoped.namespace(), "tenant-a");
1072 Ok(())
1073 }
1074}