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, ProtoQueryRequest, ProtoRegisterWorker,
9 ProtoScheduleIdRequest, ProtoSignalRequest, ProtoStartWorkflowRequest,
10 ProtoUpdateScheduleRequest, SubscriptionRequest, subscription_request,
11};
12
13use crate::error::ServerError;
14
15use super::resolver::{CallerIdentity, NamespaceResolver, ScopedEngine};
16
17#[derive(Clone)]
19pub struct NamespaceGuard {
20 resolver: NamespaceResolver,
21}
22
23impl NamespaceGuard {
24 #[must_use]
26 pub const fn new(resolver: NamespaceResolver) -> Self {
27 Self { resolver }
28 }
29
30 #[must_use]
32 pub const fn resolver(&self) -> &NamespaceResolver {
33 &self.resolver
34 }
35
36 pub async fn scope(
51 &self,
52 caller: &CallerIdentity,
53 operation: &NamespaceOperation<'_>,
54 ) -> Result<ScopedEngine, ServerError> {
55 let requested_namespace = operation.requested_namespace();
56 let scoped = self.resolver.resolve(caller, requested_namespace)?;
57 operation.verify(&self.resolver, scoped.namespace()).await?;
58 Ok(scoped)
59 }
60
61 pub fn scope_worker_namespaces(
75 &self,
76 caller: &CallerIdentity,
77 namespaces: &[String],
78 ) -> Result<Vec<String>, ServerError> {
79 if namespaces.is_empty() {
80 return Err(ServerError::namespace_denied(
81 "worker registration must name at least one namespace",
82 ));
83 }
84 let mut authorized: Vec<String> = Vec::with_capacity(namespaces.len());
85 for namespace in namespaces {
86 let scoped = self.resolver.resolve(caller, namespace)?;
87 let resolved = scoped.namespace().to_owned();
88 if !authorized.contains(&resolved) {
89 authorized.push(resolved);
90 }
91 }
92 Ok(authorized)
93 }
94}
95
96pub enum NamespaceOperation<'a> {
98 StartWorkflow(&'a ProtoStartWorkflowRequest),
100 Signal(&'a ProtoSignalRequest, WorkflowTarget<'a>),
102 Query(&'a ProtoQueryRequest, WorkflowTarget<'a>),
104 Cancel(&'a ProtoCancelRequest, WorkflowTarget<'a>),
106 ListWorkflows(&'a ProtoListWorkflowsRequest, &'a WorkflowFilter),
108 CountWorkflows(&'a ProtoCountWorkflowsRequest),
110 Describe(&'a ProtoDescribeWorkflowRequest, WorkflowTarget<'a>),
112 CreateSchedule(&'a ProtoCreateScheduleRequest),
114 UpdateSchedule(&'a ProtoUpdateScheduleRequest, ScheduleTarget<'a>),
116 PauseSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
118 ResumeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
120 DeleteSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
122 ListSchedules(&'a ProtoListSchedulesRequest),
124 DescribeSchedule(&'a ProtoScheduleIdRequest, ScheduleTarget<'a>),
126 Subscribe(SubscriptionScope<'a>, &'a EventFilter),
128 RegisterWorker(&'a ProtoRegisterWorker),
130}
131
132impl<'a> NamespaceOperation<'a> {
133 #[must_use]
135 pub const fn start(request: &'a ProtoStartWorkflowRequest) -> Self {
136 Self::StartWorkflow(request)
137 }
138
139 #[must_use]
141 pub const fn signal(request: &'a ProtoSignalRequest, target: WorkflowTarget<'a>) -> Self {
142 Self::Signal(request, target)
143 }
144
145 #[must_use]
147 pub const fn query(request: &'a ProtoQueryRequest, target: WorkflowTarget<'a>) -> Self {
148 Self::Query(request, target)
149 }
150
151 #[must_use]
153 pub const fn cancel(request: &'a ProtoCancelRequest, target: WorkflowTarget<'a>) -> Self {
154 Self::Cancel(request, target)
155 }
156
157 #[must_use]
159 pub const fn list(request: &'a ProtoListWorkflowsRequest, filter: &'a WorkflowFilter) -> Self {
160 Self::ListWorkflows(request, filter)
161 }
162
163 #[must_use]
165 pub const fn count(request: &'a ProtoCountWorkflowsRequest) -> Self {
166 Self::CountWorkflows(request)
167 }
168
169 #[must_use]
171 pub const fn describe(
172 request: &'a ProtoDescribeWorkflowRequest,
173 target: WorkflowTarget<'a>,
174 ) -> Self {
175 Self::Describe(request, target)
176 }
177
178 #[must_use]
180 pub const fn create_schedule(request: &'a ProtoCreateScheduleRequest) -> Self {
181 Self::CreateSchedule(request)
182 }
183
184 #[must_use]
186 pub const fn update_schedule(
187 request: &'a ProtoUpdateScheduleRequest,
188 target: ScheduleTarget<'a>,
189 ) -> Self {
190 Self::UpdateSchedule(request, target)
191 }
192
193 #[must_use]
195 pub const fn pause_schedule(
196 request: &'a ProtoScheduleIdRequest,
197 target: ScheduleTarget<'a>,
198 ) -> Self {
199 Self::PauseSchedule(request, target)
200 }
201
202 #[must_use]
204 pub const fn resume_schedule(
205 request: &'a ProtoScheduleIdRequest,
206 target: ScheduleTarget<'a>,
207 ) -> Self {
208 Self::ResumeSchedule(request, target)
209 }
210
211 #[must_use]
213 pub const fn delete_schedule(
214 request: &'a ProtoScheduleIdRequest,
215 target: ScheduleTarget<'a>,
216 ) -> Self {
217 Self::DeleteSchedule(request, target)
218 }
219
220 #[must_use]
222 pub const fn list_schedules(request: &'a ProtoListSchedulesRequest) -> Self {
223 Self::ListSchedules(request)
224 }
225
226 #[must_use]
228 pub const fn describe_schedule(
229 request: &'a ProtoScheduleIdRequest,
230 target: ScheduleTarget<'a>,
231 ) -> Self {
232 Self::DescribeSchedule(request, target)
233 }
234
235 #[must_use]
237 pub const fn subscribe(scope: SubscriptionScope<'a>, filter: &'a EventFilter) -> Self {
238 Self::Subscribe(scope, filter)
239 }
240
241 #[must_use]
243 pub const fn register_worker(request: &'a ProtoRegisterWorker) -> Self {
244 Self::RegisterWorker(request)
245 }
246
247 fn requested_namespace(&self) -> &str {
248 match self {
249 Self::StartWorkflow(request) => request.namespace.as_str(),
250 Self::Signal(request, _target) => request.namespace.as_str(),
251 Self::Query(request, _target) => request.namespace.as_str(),
252 Self::Cancel(request, _target) => request.namespace.as_str(),
253 Self::ListWorkflows(request, _filter) => request.namespace.as_str(),
254 Self::CountWorkflows(request) => request.namespace.as_str(),
255 Self::Describe(request, _target) => request.namespace.as_str(),
256 Self::CreateSchedule(request) => request.namespace.as_str(),
257 Self::UpdateSchedule(request, _target) => request.namespace.as_str(),
258 Self::PauseSchedule(request, _target)
259 | Self::ResumeSchedule(request, _target)
260 | Self::DeleteSchedule(request, _target)
261 | Self::DescribeSchedule(request, _target) => request.namespace.as_str(),
262 Self::ListSchedules(request) => request.namespace.as_str(),
263 Self::Subscribe(scope, _filter) => scope.namespace(),
264 Self::RegisterWorker(request) => request.namespaces.first().map_or("", String::as_str),
271 }
272 }
273
274 async fn verify(
275 &self,
276 resolver: &NamespaceResolver,
277 authorized_namespace: &str,
278 ) -> Result<(), ServerError> {
279 match self {
280 Self::Signal(_, target)
281 | Self::Query(_, target)
282 | Self::Cancel(_, target)
283 | Self::Describe(_, target) => target.verify(resolver, authorized_namespace).await,
284 Self::UpdateSchedule(_, target)
285 | Self::PauseSchedule(_, target)
286 | Self::ResumeSchedule(_, target)
287 | Self::DeleteSchedule(_, target)
288 | Self::DescribeSchedule(_, target) => {
289 target.verify(resolver, authorized_namespace).await
290 }
291 Self::Subscribe(scope, filter) => {
292 scope.verify(resolver, authorized_namespace, filter).await
293 }
294 Self::StartWorkflow(_)
301 | Self::ListWorkflows(_, _)
302 | Self::CountWorkflows(_)
303 | Self::CreateSchedule(_)
304 | Self::ListSchedules(_)
305 | Self::RegisterWorker(_) => Ok(()),
306 }
307 }
308}
309
310#[derive(Clone, Copy)]
312pub struct WorkflowTarget<'a> {
313 workflow_id: &'a WorkflowId,
314 run_id: Option<&'a RunId>,
315}
316
317impl<'a> WorkflowTarget<'a> {
318 #[must_use]
320 pub const fn with_run(workflow_id: &'a WorkflowId, run_id: &'a RunId) -> Self {
321 Self {
322 workflow_id,
323 run_id: Some(run_id),
324 }
325 }
326
327 #[must_use]
329 pub const fn workflow(workflow_id: &'a WorkflowId) -> Self {
330 Self {
331 workflow_id,
332 run_id: None,
333 }
334 }
335
336 #[must_use]
338 pub const fn workflow_id(&self) -> &WorkflowId {
339 self.workflow_id
340 }
341
342 #[must_use]
344 pub const fn run_id(&self) -> Option<&RunId> {
345 self.run_id
346 }
347
348 async fn verify(
349 &self,
350 resolver: &NamespaceResolver,
351 namespace: &str,
352 ) -> Result<(), ServerError> {
353 resolver
354 .verify_workflow_ownership(namespace, self.workflow_id)
355 .await
356 }
357}
358
359#[derive(Clone, Copy)]
361pub struct ScheduleTarget<'a> {
362 schedule_id: &'a ScheduleId,
363}
364
365impl<'a> ScheduleTarget<'a> {
366 #[must_use]
368 pub const fn schedule(schedule_id: &'a ScheduleId) -> Self {
369 Self { schedule_id }
370 }
371
372 #[must_use]
374 pub const fn schedule_id(&self) -> &ScheduleId {
375 self.schedule_id
376 }
377
378 async fn verify(
379 &self,
380 resolver: &NamespaceResolver,
381 namespace: &str,
382 ) -> Result<(), ServerError> {
383 resolver
384 .verify_schedule_ownership(namespace, self.schedule_id)
385 .await
386 }
387}
388
389pub enum SubscriptionScope<'a> {
391 PerWorkflow(&'a PerWorkflowSubscription, WorkflowTarget<'a>),
393 Filtered(&'a FilteredSubscription),
395 Firehose(&'a FirehoseSubscription),
397}
398
399impl<'a> SubscriptionScope<'a> {
400 pub fn from_request(
408 request: &'a SubscriptionRequest,
409 workflow_target: Option<WorkflowTarget<'a>>,
410 ) -> Result<Self, ServerError> {
411 match &request.subscription {
412 Some(subscription_request::Subscription::PerWorkflow(subscription)) => {
413 let target = workflow_target.ok_or_else(|| {
414 ServerError::namespace_denied(
415 "per-workflow subscription target must be decoded before guard scope",
416 )
417 })?;
418 Ok(Self::PerWorkflow(subscription, target))
419 }
420 Some(subscription_request::Subscription::Filtered(subscription)) => {
421 Ok(Self::Filtered(subscription))
422 }
423 Some(subscription_request::Subscription::Firehose(subscription)) => {
424 Ok(Self::Firehose(subscription))
425 }
426 None => Err(ServerError::namespace_denied(
427 "subscription request must name a namespace",
428 )),
429 }
430 }
431
432 fn namespace(&self) -> &str {
433 match self {
434 Self::PerWorkflow(subscription, _target) => subscription.namespace.as_str(),
435 Self::Filtered(subscription) => subscription.namespace.as_str(),
436 Self::Firehose(subscription) => subscription.namespace.as_str(),
437 }
438 }
439
440 async fn verify(
441 &self,
442 resolver: &NamespaceResolver,
443 namespace: &str,
444 filter: &EventFilter,
445 ) -> Result<(), ServerError> {
446 match self {
447 Self::PerWorkflow(_subscription, target) => {
448 verify_subscription_filter_target(filter, Some(*target), resolver, namespace).await
449 }
450 Self::Filtered(subscription) => {
451 verify_namespace_selector(subscription.namespace_selector.as_deref(), namespace)?;
452 verify_subscription_filter_target(filter, None, resolver, namespace).await
453 }
454 Self::Firehose(_) => {
455 verify_subscription_filter_target(filter, None, resolver, namespace).await
456 }
457 }
458 }
459}
460
461fn verify_namespace_selector(selector: Option<&str>, namespace: &str) -> Result<(), ServerError> {
462 match selector {
463 Some(selector) if selector != namespace => Err(ServerError::namespace_denied(
464 "subscription namespace selector is not authorized",
465 )),
466 Some(_) | None => Ok(()),
467 }
468}
469
470async fn verify_subscription_filter_target(
471 filter: &EventFilter,
472 explicit_target: Option<WorkflowTarget<'_>>,
473 resolver: &NamespaceResolver,
474 namespace: &str,
475) -> Result<(), ServerError> {
476 if let Some(target) = explicit_target {
477 if filter
478 .workflow_id
479 .as_ref()
480 .is_some_and(|workflow_id| workflow_id != target.workflow_id())
481 {
482 return Err(ServerError::namespace_denied(
483 "subscription filter workflow does not match decoded target",
484 ));
485 }
486 target.verify(resolver, namespace).await
487 } else if let Some(workflow_id) = &filter.workflow_id {
488 resolver
489 .verify_workflow_ownership(namespace, workflow_id)
490 .await
491 } else {
492 Ok(())
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use std::sync::atomic::{AtomicUsize, Ordering};
499 use std::sync::{Arc, Mutex};
500
501 use aion_core::{RunId, ScheduleId, WorkflowFilter, WorkflowId};
502 use aion_proto::{
503 FilteredSubscription, FirehoseSubscription, PerWorkflowSubscription, ProtoCancelRequest,
504 ProtoCreateScheduleRequest, ProtoDescribeWorkflowRequest, ProtoListSchedulesRequest,
505 ProtoListWorkflowsRequest, ProtoQueryRequest, ProtoRegisterWorker, ProtoScheduleIdRequest,
506 ProtoSignalRequest, ProtoStartWorkflowRequest, ProtoUpdateScheduleRequest,
507 };
508 use async_trait::async_trait;
509
510 use super::{
511 NamespaceGuard, NamespaceOperation, ScheduleTarget, SubscriptionScope, WorkflowTarget,
512 };
513 use crate::config::NamespaceMode;
514 use crate::error::ServerError;
515 use crate::namespace::{
516 CallerIdentity, NamespaceResolver, ScheduleNamespaceSource, StaticScheduleNamespaces,
517 StaticWorkflowNamespaces,
518 };
519
520 struct RecordingFakeEngine {
521 calls: Mutex<Vec<&'static str>>,
522 }
523
524 impl RecordingFakeEngine {
525 fn new() -> Self {
526 Self {
527 calls: Mutex::new(Vec::new()),
528 }
529 }
530
531 fn calls(&self) -> Result<Vec<&'static str>, Box<dyn std::error::Error>> {
532 let calls = self
533 .calls
534 .lock()
535 .map_err(|_| "fake engine calls lock poisoned")?;
536 Ok(calls.clone())
537 }
538 }
539
540 #[derive(Clone)]
545 struct CountingScheduleNamespaces {
546 inner: StaticScheduleNamespaces,
547 calls: Arc<AtomicUsize>,
548 }
549
550 impl CountingScheduleNamespaces {
551 fn wrapping(inner: StaticScheduleNamespaces) -> Self {
552 Self {
553 inner,
554 calls: Arc::new(AtomicUsize::new(0)),
555 }
556 }
557
558 fn calls(&self) -> usize {
559 self.calls.load(Ordering::SeqCst)
560 }
561 }
562
563 #[async_trait]
564 impl ScheduleNamespaceSource for CountingScheduleNamespaces {
565 async fn schedule_namespace(
566 &self,
567 schedule_id: &ScheduleId,
568 ) -> Result<Option<String>, ServerError> {
569 self.calls.fetch_add(1, Ordering::SeqCst);
570 self.inner.schedule_namespace(schedule_id).await
571 }
572 }
573
574 fn guard_with_ownership(ownership: StaticWorkflowNamespaces) -> NamespaceGuard {
575 let resolver = NamespaceResolver::authorization_only(
576 NamespaceMode::SharedEngine,
577 ownership,
578 StaticScheduleNamespaces::default(),
579 );
580 NamespaceGuard::new(resolver)
581 }
582
583 fn guard_with_schedule_ownership(
584 schedule_ownership: impl ScheduleNamespaceSource + 'static,
585 ) -> NamespaceGuard {
586 let resolver = NamespaceResolver::authorization_only(
587 NamespaceMode::SharedEngine,
588 StaticWorkflowNamespaces::default(),
589 schedule_ownership,
590 );
591 NamespaceGuard::new(resolver)
592 }
593
594 fn caller() -> CallerIdentity {
595 CallerIdentity::new("alice", [String::from("tenant-a")])
596 }
597
598 fn workflow_ids() -> (WorkflowId, RunId) {
599 (
600 WorkflowId::new(uuid::Uuid::from_u128(1)),
601 RunId::new(uuid::Uuid::from_u128(2)),
602 )
603 }
604
605 #[tokio::test]
606 async fn denied_targeted_operations_do_not_call_engine()
607 -> Result<(), Box<dyn std::error::Error>> {
608 let (workflow_id, run_id) = workflow_ids();
609 let ownership = StaticWorkflowNamespaces::default();
610 ownership.record(workflow_id.clone(), "tenant-b")?;
611 let guard = guard_with_ownership(ownership);
612 let fake = RecordingFakeEngine::new();
613 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
614
615 let signal = ProtoSignalRequest {
616 namespace: String::from("tenant-a"),
617 workflow_id: None,
618 run_id: None,
619 signal_name: String::from("ship"),
620 payload: None,
621 };
622 let query = ProtoQueryRequest {
623 namespace: String::from("tenant-a"),
624 workflow_id: None,
625 run_id: None,
626 query_name: String::from("state"),
627 };
628 let cancel = ProtoCancelRequest {
629 namespace: String::from("tenant-a"),
630 workflow_id: None,
631 run_id: None,
632 reason: String::from("operator"),
633 };
634 let describe = ProtoDescribeWorkflowRequest {
635 namespace: String::from("tenant-a"),
636 workflow_id: None,
637 run_id: None,
638 include_history: false,
639 };
640
641 let operations = [
642 NamespaceOperation::signal(&signal, target),
643 NamespaceOperation::query(&query, target),
644 NamespaceOperation::cancel(&cancel, target),
645 NamespaceOperation::describe(&describe, target),
646 ];
647
648 for operation in operations {
649 let result = guard.scope(&caller(), &operation).await;
650 assert_eq!(
654 result.err().map(|error| error.to_wire_error().code),
655 Some(aion_proto::WireErrorCode::NotFound)
656 );
657 }
658 assert!(fake.calls()?.is_empty());
659 Ok(())
660 }
661
662 #[tokio::test]
663 async fn denied_list_and_worker_scope_do_not_call_engine()
664 -> Result<(), Box<dyn std::error::Error>> {
665 let (workflow_id, _run_id) = workflow_ids();
666 let ownership = StaticWorkflowNamespaces::default();
667 ownership.record(workflow_id, "tenant-b")?;
668 let guard = guard_with_ownership(ownership);
669 let fake = RecordingFakeEngine::new();
670 let filter = WorkflowFilter::default();
671
672 let list = ProtoListWorkflowsRequest {
673 namespace: String::from("tenant-b"),
674 filter: None,
675 };
676 let worker = ProtoRegisterWorker {
677 namespaces: vec![String::from("tenant-b")],
678 activity_types: vec![String::from("ship")],
679 task_queue: String::new(),
680 node: String::new(),
681 };
682
683 assert!(
684 guard
685 .scope(&caller(), &NamespaceOperation::list(&list, &filter))
686 .await
687 .is_err()
688 );
689 assert!(
690 guard
691 .scope(&caller(), &NamespaceOperation::register_worker(&worker),)
692 .await
693 .is_err()
694 );
695 assert!(fake.calls()?.is_empty());
696 Ok(())
697 }
698
699 #[tokio::test]
700 async fn denied_subscriptions_do_not_call_engine() -> Result<(), Box<dyn std::error::Error>> {
701 let (workflow_id, run_id) = workflow_ids();
702 let ownership = StaticWorkflowNamespaces::default();
703 ownership.record(workflow_id.clone(), "tenant-b")?;
704 let guard = guard_with_ownership(ownership);
705 let fake = RecordingFakeEngine::new();
706 let event_filter = aion::EventFilter::default();
707
708 let filtered = FilteredSubscription {
709 namespace: String::from("tenant-a"),
710 workflow_type: None,
711 status: None,
712 namespace_selector: Some(String::from("tenant-b")),
713 };
714 let filtered_by_workflow = FilteredSubscription {
715 namespace: String::from("tenant-a"),
716 workflow_type: None,
717 status: None,
718 namespace_selector: None,
719 };
720 let per_workflow = PerWorkflowSubscription {
721 namespace: String::from("tenant-a"),
722 workflow_id: None,
723 resume_from_seq: None,
724 };
725 let cross_namespace_filter = aion::EventFilter {
726 workflow_id: Some(workflow_id.clone()),
727 run: None,
728 family: None,
729 };
730 let firehose = FirehoseSubscription {
731 namespace: String::from("tenant-b"),
732 };
733
734 let target = WorkflowTarget::with_run(&workflow_id, &run_id);
735 let denied_subscriptions = [
739 (
740 NamespaceOperation::subscribe(
741 SubscriptionScope::Filtered(&filtered),
742 &event_filter,
743 ),
744 aion_proto::WireErrorCode::NamespaceDenied,
745 ),
746 (
747 NamespaceOperation::subscribe(
748 SubscriptionScope::Filtered(&filtered_by_workflow),
749 &cross_namespace_filter,
750 ),
751 aion_proto::WireErrorCode::NotFound,
752 ),
753 (
754 NamespaceOperation::subscribe(
755 SubscriptionScope::PerWorkflow(&per_workflow, target),
756 &cross_namespace_filter,
757 ),
758 aion_proto::WireErrorCode::NotFound,
759 ),
760 (
761 NamespaceOperation::subscribe(
762 SubscriptionScope::Firehose(&firehose),
763 &event_filter,
764 ),
765 aion_proto::WireErrorCode::NamespaceDenied,
766 ),
767 ];
768
769 for (operation, expected_code) in &denied_subscriptions {
770 assert_eq!(
771 guard
772 .scope(&caller(), operation)
773 .await
774 .err()
775 .map(|error| error.to_wire_error().code)
776 .as_ref(),
777 Some(expected_code)
778 );
779 }
780 assert!(fake.calls()?.is_empty());
781 Ok(())
782 }
783
784 fn schedule_id() -> ScheduleId {
785 ScheduleId::new(uuid::Uuid::from_u128(9))
786 }
787
788 fn schedule_id_request(namespace: &str) -> ProtoScheduleIdRequest {
789 ProtoScheduleIdRequest {
790 namespace: namespace.to_owned(),
791 schedule_id: None,
792 }
793 }
794
795 #[tokio::test]
796 async fn schedule_ownership_misses_are_not_found_and_do_not_call_engine()
797 -> Result<(), Box<dyn std::error::Error>> {
798 let schedule_id = schedule_id();
799 let schedule_ownership = StaticScheduleNamespaces::default();
800 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
801 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
802 let guard = guard_with_schedule_ownership(counting.clone());
806 let target = ScheduleTarget::schedule(&schedule_id);
807
808 let update = ProtoUpdateScheduleRequest {
809 namespace: String::from("tenant-a"),
810 schedule_id: None,
811 config: None,
812 };
813 let id_request = schedule_id_request("tenant-a");
814
815 let operations = [
816 NamespaceOperation::update_schedule(&update, target),
817 NamespaceOperation::pause_schedule(&id_request, target),
818 NamespaceOperation::resume_schedule(&id_request, target),
819 NamespaceOperation::delete_schedule(&id_request, target),
820 NamespaceOperation::describe_schedule(&id_request, target),
821 ];
822 let operation_count = operations.len();
823
824 for operation in operations {
825 let result = guard.scope(&caller(), &operation).await;
826 let error = result
830 .err()
831 .map(|error| error.to_wire_error())
832 .ok_or("expected foreign-owned schedule to be rejected")?;
833 assert_eq!(error.code, aion_proto::WireErrorCode::NotFound);
834 assert_eq!(error.message, "schedule not found in namespace tenant-a");
835 }
836 assert_eq!(counting.calls(), operation_count);
839 Ok(())
840 }
841
842 #[tokio::test]
843 async fn ungranted_schedule_operations_are_namespace_denied()
844 -> Result<(), Box<dyn std::error::Error>> {
845 let schedule_id = schedule_id();
846 let schedule_ownership = StaticScheduleNamespaces::default();
847 schedule_ownership.record(schedule_id.clone(), "tenant-b")?;
848 let counting = CountingScheduleNamespaces::wrapping(schedule_ownership);
849 let guard = guard_with_schedule_ownership(counting.clone());
850 let target = ScheduleTarget::schedule(&schedule_id);
851
852 let create = ProtoCreateScheduleRequest {
853 namespace: String::from("tenant-b"),
854 config: None,
855 };
856 let update = ProtoUpdateScheduleRequest {
857 namespace: String::from("tenant-b"),
858 schedule_id: None,
859 config: None,
860 };
861 let id_request = schedule_id_request("tenant-b");
862 let list = ProtoListSchedulesRequest {
863 namespace: String::from("tenant-b"),
864 };
865
866 let operations = [
867 NamespaceOperation::create_schedule(&create),
868 NamespaceOperation::update_schedule(&update, target),
869 NamespaceOperation::pause_schedule(&id_request, target),
870 NamespaceOperation::resume_schedule(&id_request, target),
871 NamespaceOperation::delete_schedule(&id_request, target),
872 NamespaceOperation::describe_schedule(&id_request, target),
873 NamespaceOperation::list_schedules(&list),
874 ];
875
876 for operation in operations {
880 let result = guard.scope(&caller(), &operation).await;
881 assert_eq!(
882 result.err().map(|error| error.to_wire_error().code),
883 Some(aion_proto::WireErrorCode::NamespaceDenied)
884 );
885 }
886 assert_eq!(counting.calls(), 0);
889 Ok(())
890 }
891
892 #[tokio::test]
893 async fn granted_schedule_create_and_list_return_scoped_engine()
894 -> Result<(), Box<dyn std::error::Error>> {
895 let guard = guard_with_schedule_ownership(StaticScheduleNamespaces::default());
896 let create = ProtoCreateScheduleRequest {
897 namespace: String::from("tenant-a"),
898 config: None,
899 };
900 let list = ProtoListSchedulesRequest {
901 namespace: String::from("tenant-a"),
902 };
903
904 let scoped_create = guard
905 .scope(&caller(), &NamespaceOperation::create_schedule(&create))
906 .await?;
907 let scoped_list = guard
908 .scope(&caller(), &NamespaceOperation::list_schedules(&list))
909 .await?;
910
911 assert_eq!(scoped_create.namespace(), "tenant-a");
912 assert_eq!(scoped_list.namespace(), "tenant-a");
913 Ok(())
914 }
915
916 #[tokio::test]
917 async fn authorized_start_returns_scoped_engine() -> Result<(), Box<dyn std::error::Error>> {
918 let guard = guard_with_ownership(StaticWorkflowNamespaces::default());
919 let request = ProtoStartWorkflowRequest {
920 namespace: String::from("tenant-a"),
921 workflow_type: String::from("checkout"),
922 input: None,
923 routing_key: None,
924 };
925
926 let scoped = guard
927 .scope(&caller(), &NamespaceOperation::start(&request))
928 .await?;
929
930 assert_eq!(scoped.namespace(), "tenant-a");
931 Ok(())
932 }
933
934 #[tokio::test]
935 async fn single_tenant_mode_authorizes_configured_namespace()
936 -> Result<(), Box<dyn std::error::Error>> {
937 let resolver = NamespaceResolver::authorization_only(
938 NamespaceMode::SingleTenant {
939 namespace: String::from("tenant-a"),
940 },
941 StaticWorkflowNamespaces::default(),
942 StaticScheduleNamespaces::default(),
943 );
944 let guard = NamespaceGuard::new(resolver);
945 let request = ProtoRegisterWorker {
946 namespaces: vec![String::from("tenant-a")],
947 activity_types: Vec::new(),
948 task_queue: String::new(),
949 node: String::new(),
950 };
951
952 let scoped = guard
953 .scope(
954 &CallerIdentity::new("single-tenant", Vec::<String>::new()),
955 &NamespaceOperation::register_worker(&request),
956 )
957 .await?;
958
959 assert_eq!(scoped.namespace(), "tenant-a");
960 Ok(())
961 }
962}