1use std::any::{Any, TypeId};
55use std::collections::HashMap;
56use std::pin::Pin;
57use std::sync::{Arc, RwLock};
58
59use http::StatusCode;
60
61use crate::session::Session;
62
63pub trait ProvideAuthorizationState: Send + Sync {
68 fn policy_registry(&self) -> &PolicyRegistry;
69 fn auth_session_key(&self) -> &str;
70 fn forbidden_response(&self) -> &ForbiddenResponse;
71
72 #[cfg(feature = "db")]
73 fn pool(
74 &self,
75 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>;
76}
77
78pub type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
82
83#[derive(Clone)]
94pub struct PolicyContext {
95 pub session: Session,
99
100 pub user_id: Option<String>,
103
104 pub roles: Vec<String>,
107
108 pub scopes: Vec<String>,
114
115 #[cfg(feature = "db")]
119 pub pool: Option<diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>,
120
121 pub policy_registry: PolicyRegistry,
127}
128
129impl PolicyContext {
130 pub async fn from_session(session: &Session, auth_session_key: &str) -> Self {
138 let user_id = session.get(auth_session_key).await;
139
140 if crate::current::Current::actor().is_none()
170 && let Some(user_id) = &user_id
171 {
172 crate::current::Current::set_actor(
173 crate::auth::impersonation::audit_actor_id(session, user_id).await,
174 );
175 }
176
177 let role = session.get("role").await;
178 let roles = role.into_iter().collect();
179 Self {
180 session: session.clone(),
181 user_id,
182 roles,
183 scopes: Vec::new(),
184 #[cfg(feature = "db")]
185 pool: None,
186 policy_registry: PolicyRegistry::default(),
187 }
188 }
189
190 pub async fn from_request(state: &impl ProvideAuthorizationState, session: &Session) -> Self {
194 let mut ctx = Self::from_session(session, state.auth_session_key()).await;
195 ctx.policy_registry = state.policy_registry().clone();
196 #[cfg(feature = "db")]
197 {
198 if let Some(pool) = state.pool() {
199 ctx.pool = Some(pool.clone());
200 }
201 }
202 ctx
203 }
204
205 #[must_use]
207 pub const fn is_authenticated(&self) -> bool {
208 self.user_id.is_some()
209 }
210
211 #[must_use]
215 pub fn user_id_i64(&self) -> Option<i64> {
216 self.user_id.as_deref().and_then(|s| s.parse().ok())
217 }
218
219 #[must_use]
221 pub fn has_role(&self, role: &str) -> bool {
222 self.roles.iter().any(|r| r == role)
223 }
224
225 #[must_use]
228 pub fn has_any_role<I, S>(&self, candidates: I) -> bool
229 where
230 I: IntoIterator<Item = S>,
231 S: AsRef<str>,
232 {
233 candidates.into_iter().any(|c| self.has_role(c.as_ref()))
234 }
235
236 #[must_use]
242 pub fn has_scope(&self, scope: &str) -> bool {
243 self.scopes.iter().any(|s| s == scope)
244 }
245
246 #[must_use]
249 pub fn has_any_scope<I, S>(&self, candidates: I) -> bool
250 where
251 I: IntoIterator<Item = S>,
252 S: AsRef<str>,
253 {
254 candidates.into_iter().any(|c| self.has_scope(c.as_ref()))
255 }
256
257 #[must_use]
260 pub fn has_all_scopes<I, S>(&self, candidates: I) -> bool
261 where
262 I: IntoIterator<Item = S>,
263 S: AsRef<str>,
264 {
265 candidates.into_iter().all(|c| self.has_scope(c.as_ref()))
266 }
267
268 #[must_use]
272 pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
273 self.scopes = scopes;
274 self
275 }
276
277 #[cfg(feature = "tls")]
293 #[must_use]
294 pub fn client_identity(
295 &self,
296 ) -> Option<std::sync::Arc<crate::tls::client_auth::ClientIdentity>> {
297 crate::tls::client_auth::current_client_identity()
298 }
299
300 #[cfg(feature = "tls")]
303 #[must_use]
304 pub fn has_client_identity(&self) -> bool {
305 self.client_identity().is_some()
306 }
307
308 #[cfg(feature = "tls")]
312 #[must_use]
313 pub fn client_has_san(&self, san: &str) -> bool {
314 self.client_identity().is_some_and(|id| id.has_san(san))
315 }
316
317 pub async fn from_request_parts(
324 state: &crate::AppState,
325 session: &Session,
326 scopes: Option<&crate::auth::ApiTokenScopes>,
327 ) -> Self {
328 let ctx = Self::from_request(state, session).await;
329 match scopes {
330 Some(s) => ctx.with_scopes(s.0.clone()),
331 None => ctx,
332 }
333 }
334
335 #[cfg(feature = "db")]
339 #[must_use]
340 pub fn with_pool(
341 mut self,
342 pool: diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>,
343 ) -> Self {
344 self.pool = Some(pool);
345 self
346 }
347}
348
349pub trait Policy<R: Send + Sync + 'static>: Send + Sync + 'static {
372 fn can_show<'a>(&'a self, _ctx: &'a PolicyContext, _resource: &'a R) -> BoxFuture<'a, bool> {
374 Box::pin(async { false })
375 }
376
377 fn can_create<'a>(&'a self, _ctx: &'a PolicyContext) -> BoxFuture<'a, bool> {
384 Box::pin(async { false })
385 }
386
387 fn can_create_payload<'a>(
391 &'a self,
392 ctx: &'a PolicyContext,
393 _payload: &'a serde_json::Value,
394 ) -> BoxFuture<'a, bool> {
395 self.can_create(ctx)
396 }
397
398 fn can_update<'a>(&'a self, _ctx: &'a PolicyContext, _resource: &'a R) -> BoxFuture<'a, bool> {
400 Box::pin(async { false })
401 }
402
403 fn can_delete<'a>(&'a self, _ctx: &'a PolicyContext, _resource: &'a R) -> BoxFuture<'a, bool> {
405 Box::pin(async { false })
406 }
407
408 fn can<'a>(
413 &'a self,
414 action: &'a str,
415 ctx: &'a PolicyContext,
416 resource: &'a R,
417 ) -> BoxFuture<'a, bool> {
418 Box::pin(async move {
419 match action {
420 "show" | "read" => self.can_show(ctx, resource).await,
421 "create" => self.can_create(ctx).await,
422 "update" | "edit" => self.can_update(ctx, resource).await,
423 "delete" | "destroy" => self.can_delete(ctx, resource).await,
424 _ => false,
425 }
426 })
427 }
428}
429
430#[cfg(feature = "db")]
445pub trait Scope<R: Send + Sync + 'static>: Send + Sync + 'static {
446 fn list<'a>(
453 &'a self,
454 _ctx: &'a PolicyContext,
455 _conn: &'a mut crate::db::RuntimeConnection,
456 ) -> BoxFuture<'a, crate::AutumnResult<Vec<R>>> {
457 Box::pin(async { Ok(Vec::new()) })
458 }
459}
460
461#[cfg(not(feature = "db"))]
466pub trait Scope<R: Send + Sync + 'static>: Send + Sync + 'static {
467 fn list<'a>(&'a self, _ctx: &'a PolicyContext) -> BoxFuture<'a, crate::AutumnResult<Vec<R>>> {
468 Box::pin(async { Ok(Vec::new()) })
469 }
470}
471
472pub struct ScopeQuery<'a, R: Send + Sync + 'static> {
482 ctx: &'a PolicyContext,
483 _marker: std::marker::PhantomData<fn() -> R>,
484}
485
486#[cfg(feature = "db")]
487impl<R: Send + Sync + 'static> ScopeQuery<'_, R> {
488 pub async fn load(
499 self,
500 conn: &mut crate::db::RuntimeConnection,
501 ) -> crate::AutumnResult<Vec<R>> {
502 let scope = self.ctx.policy_registry.scope::<R>().ok_or_else(|| {
503 crate::AutumnError::from(std::io::Error::other(format!(
504 "no scope registered for resource type {}",
505 std::any::type_name::<R>()
506 )))
507 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
508 })?;
509 scope.list(self.ctx, conn).await
510 }
511}
512
513#[cfg(not(feature = "db"))]
514impl<R: Send + Sync + 'static> ScopeQuery<'_, R> {
515 pub async fn load(self) -> crate::AutumnResult<Vec<R>> {
516 let scope = self.ctx.policy_registry.scope::<R>().ok_or_else(|| {
517 crate::AutumnError::from(std::io::Error::other(format!(
518 "no scope registered for resource type {}",
519 std::any::type_name::<R>()
520 )))
521 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
522 })?;
523 scope.list(self.ctx).await
524 }
525}
526
527pub trait Scoped: Send + Sync + Sized + 'static {
541 #[must_use]
544 fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self> {
545 ScopeQuery {
546 ctx,
547 _marker: std::marker::PhantomData,
548 }
549 }
550}
551
552impl<T: Send + Sync + 'static> Scoped for T {}
553
554#[derive(Clone, Default)]
563pub struct PolicyRegistry {
564 inner: Arc<RwLock<RegistryInner>>,
565}
566
567#[derive(Default)]
568struct RegistryInner {
569 policies: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
570 scopes: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
571}
572
573impl PolicyRegistry {
574 pub fn register_policy<R, P>(&self, policy: P)
583 where
584 R: Send + Sync + 'static,
585 P: Policy<R>,
586 {
587 let mut inner = self.inner.write().expect("policy registry lock poisoned");
588 let key = TypeId::of::<R>();
589 assert!(
590 !inner.policies.contains_key(&key),
591 "Policy for {} already registered. Multiple policies per resource are not supported.",
592 std::any::type_name::<R>()
593 );
594 let boxed: Arc<dyn Policy<R>> = Arc::new(policy);
595 inner.policies.insert(key, Arc::new(boxed));
596 }
597
598 pub fn register_scope<R, S>(&self, scope: S)
604 where
605 R: Send + Sync + 'static,
606 S: Scope<R>,
607 {
608 let mut inner = self.inner.write().expect("policy registry lock poisoned");
609 let key = TypeId::of::<R>();
610 assert!(
611 !inner.scopes.contains_key(&key),
612 "Scope for {} already registered. Multiple scopes per resource are not supported.",
613 std::any::type_name::<R>()
614 );
615 let boxed: Arc<dyn Scope<R>> = Arc::new(scope);
616 inner.scopes.insert(key, Arc::new(boxed));
617 }
618
619 #[must_use]
626 pub fn policy<R: Send + Sync + 'static>(&self) -> Option<Arc<dyn Policy<R>>> {
627 let inner = self.inner.read().expect("policy registry lock poisoned");
628 inner
629 .policies
630 .get(&TypeId::of::<R>())
631 .and_then(|a| a.downcast_ref::<Arc<dyn Policy<R>>>().cloned())
632 }
633
634 #[must_use]
640 pub fn scope<R: Send + Sync + 'static>(&self) -> Option<Arc<dyn Scope<R>>> {
641 let inner = self.inner.read().expect("policy registry lock poisoned");
642 inner
643 .scopes
644 .get(&TypeId::of::<R>())
645 .and_then(|a| a.downcast_ref::<Arc<dyn Scope<R>>>().cloned())
646 }
647
648 #[must_use]
654 pub fn has_policy<R: Send + Sync + 'static>(&self) -> bool {
655 self.inner
656 .read()
657 .expect("policy registry lock poisoned")
658 .policies
659 .contains_key(&TypeId::of::<R>())
660 }
661}
662
663impl std::fmt::Debug for PolicyRegistry {
664 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
665 let inner = self.inner.read().expect("policy registry lock poisoned");
666 f.debug_struct("PolicyRegistry")
667 .field("policies", &inner.policies.len())
668 .field("scopes", &inner.scopes.len())
669 .finish()
670 }
671}
672
673#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
685pub enum ForbiddenResponse {
686 Forbidden403,
688 #[default]
690 NotFound404,
691}
692
693impl ForbiddenResponse {
694 #[must_use]
696 pub const fn status(self) -> StatusCode {
697 match self {
698 Self::Forbidden403 => StatusCode::FORBIDDEN,
699 Self::NotFound404 => StatusCode::NOT_FOUND,
700 }
701 }
702
703 #[must_use]
707 pub const fn message(self) -> &'static str {
708 match self {
709 Self::Forbidden403 => "forbidden",
710 Self::NotFound404 => "not found",
711 }
712 }
713
714 #[must_use]
718 pub fn into_error(self) -> crate::AutumnError {
719 crate::AutumnError::from(std::io::Error::other(self.message())).with_status(self.status())
720 }
721}
722
723impl std::str::FromStr for ForbiddenResponse {
724 type Err = String;
725
726 fn from_str(s: &str) -> Result<Self, Self::Err> {
727 match s.trim() {
728 "403" | "forbidden" | "Forbidden" => Ok(Self::Forbidden403),
729 "404" | "not_found" | "NotFound" | "" => Ok(Self::NotFound404),
730 other => Err(format!(
731 "invalid forbidden_response: {other:?} (expected \"403\" or \"404\")"
732 )),
733 }
734 }
735}
736
737impl<'de> serde::Deserialize<'de> for ForbiddenResponse {
738 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
739 where
740 D: serde::Deserializer<'de>,
741 {
742 let raw = String::deserialize(deserializer)?;
743 raw.parse().map_err(serde::de::Error::custom)
744 }
745}
746
747pub async fn authorize<R>(
784 state: &impl ProvideAuthorizationState,
785 session: &Session,
786 action: &str,
787 resource: &R,
788) -> crate::AutumnResult<()>
789where
790 R: Send + Sync + 'static,
791{
792 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
793 crate::AutumnError::from(std::io::Error::other(format!(
794 "no policy registered for resource type {}",
795 std::any::type_name::<R>()
796 )))
797 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
798 })?;
799
800 let ctx = PolicyContext::from_request(state, session).await;
801
802 if policy.can(action, &ctx, resource).await {
803 Ok(())
804 } else {
805 Err(state.forbidden_response().into_error())
806 }
807}
808
809pub async fn authorize_with_scopes<R>(
822 state: &crate::AppState,
823 session: &Session,
824 scopes: Option<&crate::auth::ApiTokenScopes>,
825 action: &str,
826 resource: &R,
827) -> crate::AutumnResult<()>
828where
829 R: Send + Sync + 'static,
830{
831 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
832 crate::AutumnError::from(std::io::Error::other(format!(
833 "no policy registered for resource type {}",
834 std::any::type_name::<R>()
835 )))
836 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
837 })?;
838
839 let ctx = PolicyContext::from_request_parts(state, session, scopes).await;
840
841 if policy.can(action, &ctx, resource).await {
842 Ok(())
843 } else {
844 Err(state.forbidden_response().into_error())
845 }
846}
847
848#[doc(hidden)]
852pub async fn __check_policy<R>(
853 state: &impl ProvideAuthorizationState,
854 session: &Session,
855 action: &str,
856 resource: &R,
857) -> crate::AutumnResult<()>
858where
859 R: Send + Sync + 'static,
860{
861 authorize(state, session, action, resource).await
862}
863
864#[doc(hidden)]
871pub async fn __check_policy_scoped<R>(
872 state: &crate::AppState,
873 session: &Session,
874 scopes: Option<&crate::auth::ApiTokenScopes>,
875 action: &str,
876 resource: &R,
877) -> crate::AutumnResult<()>
878where
879 R: Send + Sync + 'static,
880{
881 authorize_with_scopes(state, session, scopes, action, resource).await
882}
883
884#[doc(hidden)]
894pub async fn __check_policy_create<R>(
895 state: &impl ProvideAuthorizationState,
896 session: &Session,
897) -> crate::AutumnResult<()>
898where
899 R: Send + Sync + 'static,
900{
901 authorize_create::<R>(state, session).await
902}
903
904#[doc(hidden)]
913pub async fn __check_policy_create_payload<R>(
914 state: &impl ProvideAuthorizationState,
915 session: &Session,
916 payload: &serde_json::Value,
917) -> crate::AutumnResult<()>
918where
919 R: Send + Sync + 'static,
920{
921 authorize_create_payload::<R>(state, session, payload).await
922}
923
924#[doc(hidden)]
931pub async fn __check_policy_create_payload_scoped<R>(
932 state: &crate::AppState,
933 session: &Session,
934 scopes: Option<&crate::auth::ApiTokenScopes>,
935 payload: &serde_json::Value,
936) -> crate::AutumnResult<()>
937where
938 R: Send + Sync + 'static,
939{
940 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
941 crate::AutumnError::from(std::io::Error::other(format!(
942 "no policy registered for resource type {}",
943 std::any::type_name::<R>()
944 )))
945 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
946 })?;
947 let ctx = PolicyContext::from_request_parts(state, session, scopes).await;
948 if policy.can_create_payload(&ctx, payload).await {
949 Ok(())
950 } else {
951 Err(state.forbidden_response().into_error())
952 }
953}
954
955pub async fn authorize_create<R>(
966 state: &impl ProvideAuthorizationState,
967 session: &Session,
968) -> crate::AutumnResult<()>
969where
970 R: Send + Sync + 'static,
971{
972 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
973 crate::AutumnError::from(std::io::Error::other(format!(
974 "no policy registered for resource type {}",
975 std::any::type_name::<R>()
976 )))
977 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
978 })?;
979
980 let ctx = PolicyContext::from_request(state, session).await;
981
982 if policy.can_create(&ctx).await {
983 Ok(())
984 } else {
985 Err(state.forbidden_response().into_error())
986 }
987}
988
989pub async fn authorize_create_payload<R>(
1002 state: &impl ProvideAuthorizationState,
1003 session: &Session,
1004 payload: &serde_json::Value,
1005) -> crate::AutumnResult<()>
1006where
1007 R: Send + Sync + 'static,
1008{
1009 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
1010 crate::AutumnError::from(std::io::Error::other(format!(
1011 "no policy registered for resource type {}",
1012 std::any::type_name::<R>()
1013 )))
1014 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
1015 })?;
1016
1017 let ctx = PolicyContext::from_request(state, session).await;
1018
1019 if policy.can_create_payload(&ctx, payload).await {
1020 Ok(())
1021 } else {
1022 Err(state.forbidden_response().into_error())
1023 }
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1029 use std::collections::HashMap;
1030
1031 #[derive(Debug, Clone, PartialEq)]
1032 struct Note {
1033 author_id: i64,
1034 }
1035
1036 #[derive(Default)]
1037 struct AdminOrOwnerPolicy;
1038
1039 impl Policy<Note> for AdminOrOwnerPolicy {
1040 fn can_show<'a>(&'a self, _ctx: &'a PolicyContext, _note: &'a Note) -> BoxFuture<'a, bool> {
1041 Box::pin(async { true })
1042 }
1043 fn can_update<'a>(&'a self, ctx: &'a PolicyContext, note: &'a Note) -> BoxFuture<'a, bool> {
1044 Box::pin(
1045 async move { ctx.has_role("admin") || ctx.user_id_i64() == Some(note.author_id) },
1046 )
1047 }
1048 fn can_delete<'a>(
1049 &'a self,
1050 ctx: &'a PolicyContext,
1051 _note: &'a Note,
1052 ) -> BoxFuture<'a, bool> {
1053 Box::pin(async move { ctx.has_role("admin") })
1054 }
1055 }
1056
1057 fn ctx(user_id: Option<&str>, role: Option<&str>) -> PolicyContext {
1058 let session = Session::new_for_test(String::new(), HashMap::new());
1059 PolicyContext {
1060 session,
1061 user_id: user_id.map(str::to_owned),
1062 roles: role.into_iter().map(str::to_owned).collect(),
1063 scopes: Vec::new(),
1064 #[cfg(feature = "db")]
1065 pool: None,
1066 policy_registry: PolicyRegistry::default(),
1067 }
1068 }
1069
1070 #[cfg(feature = "tls")]
1073 #[tokio::test]
1074 async fn client_identity_is_absent_outside_an_mtls_request() {
1075 let c = ctx(Some("1"), None);
1076 assert!(c.client_identity().is_none());
1077 assert!(!c.has_client_identity());
1078 assert!(!c.client_has_san("URI:spiffe://acme/svc/orders"));
1079 }
1080
1081 #[cfg(feature = "tls")]
1082 #[tokio::test]
1083 async fn a_policy_can_decide_on_the_verified_machine_identity() {
1084 use std::sync::Arc;
1085
1086 use crate::tls::client_auth::{ClientIdentity, with_client_identity};
1087
1088 let identity = Arc::new(ClientIdentity::new_for_test(
1089 "svc-orders",
1090 vec!["URI:spiffe://acme/svc/orders".to_owned()],
1091 ));
1092
1093 with_client_identity(Some(identity), async {
1094 let c = ctx(None, None);
1095 assert!(c.has_client_identity());
1096 assert!(c.client_has_san("URI:spiffe://acme/svc/orders"));
1097 assert!(!c.client_has_san("URI:spiffe://acme/svc/billing"));
1098 assert_eq!(
1099 c.client_identity()
1100 .expect("identity in scope")
1101 .common_name(),
1102 Some("svc-orders")
1103 );
1104 })
1105 .await;
1106
1107 assert!(!ctx(None, None).has_client_identity());
1109 }
1110
1111 #[tokio::test]
1112 async fn default_impls_deny() {
1113 struct EmptyPolicy;
1114 impl Policy<Note> for EmptyPolicy {}
1115 let policy = EmptyPolicy;
1116 let c = ctx(Some("1"), None);
1117 let n = Note { author_id: 1 };
1118 assert!(!policy.can_show(&c, &n).await);
1119 assert!(!policy.can_create(&c).await);
1120 assert!(!policy.can_update(&c, &n).await);
1121 assert!(!policy.can_delete(&c, &n).await);
1122 assert!(!policy.can("publish", &c, &n).await);
1123 }
1124
1125 #[tokio::test]
1126 async fn owner_can_update() {
1127 let policy = AdminOrOwnerPolicy;
1128 let c = ctx(Some("42"), None);
1129 let n = Note { author_id: 42 };
1130 assert!(policy.can_update(&c, &n).await);
1131 assert!(!policy.can_delete(&c, &n).await);
1132 }
1133
1134 #[tokio::test]
1135 async fn non_owner_cannot_update() {
1136 let policy = AdminOrOwnerPolicy;
1137 let c = ctx(Some("99"), None);
1138 let n = Note { author_id: 42 };
1139 assert!(!policy.can_update(&c, &n).await);
1140 }
1141
1142 #[tokio::test]
1143 async fn admin_can_delete() {
1144 let policy = AdminOrOwnerPolicy;
1145 let c = ctx(Some("99"), Some("admin"));
1146 let n = Note { author_id: 42 };
1147 assert!(policy.can_delete(&c, &n).await);
1148 }
1149
1150 #[tokio::test]
1151 async fn can_dispatches_named_actions() {
1152 let policy = AdminOrOwnerPolicy;
1153 let c = ctx(Some("42"), None);
1154 let n = Note { author_id: 42 };
1155 assert!(policy.can("show", &c, &n).await);
1156 assert!(policy.can("update", &c, &n).await);
1157 assert!(policy.can("edit", &c, &n).await);
1158 assert!(!policy.can("publish", &c, &n).await);
1159 }
1160
1161 #[test]
1162 fn policy_registry_stores_and_resolves() {
1163 let registry = PolicyRegistry::default();
1164 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1165 assert!(registry.has_policy::<Note>());
1166 assert!(registry.policy::<Note>().is_some());
1167 assert!(registry.scope::<Note>().is_none());
1168 }
1169
1170 #[test]
1171 #[should_panic(expected = "already registered")]
1172 fn double_policy_registration_panics() {
1173 let registry = PolicyRegistry::default();
1174 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1175 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1176 }
1177
1178 #[test]
1179 fn forbidden_response_default_is_404() {
1180 let resp = ForbiddenResponse::default();
1181 assert_eq!(resp, ForbiddenResponse::NotFound404);
1182 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1183 }
1184
1185 #[test]
1186 fn forbidden_response_parses_strings() {
1187 assert_eq!(
1188 "403".parse::<ForbiddenResponse>().unwrap(),
1189 ForbiddenResponse::Forbidden403
1190 );
1191 assert_eq!(
1192 "404".parse::<ForbiddenResponse>().unwrap(),
1193 ForbiddenResponse::NotFound404
1194 );
1195 assert_eq!(
1196 "forbidden".parse::<ForbiddenResponse>().unwrap(),
1197 ForbiddenResponse::Forbidden403
1198 );
1199 assert!("418".parse::<ForbiddenResponse>().is_err());
1200 }
1201
1202 #[test]
1203 fn policy_context_helpers() {
1204 let c = ctx(Some("42"), Some("editor"));
1205 assert!(c.is_authenticated());
1206 assert_eq!(c.user_id_i64(), Some(42));
1207 assert!(c.has_role("editor"));
1208 assert!(!c.has_role("admin"));
1209 assert!(c.has_any_role(["admin", "editor"]));
1210 assert!(!c.has_any_role(["viewer", "guest"]));
1211 }
1212
1213 #[test]
1214 fn anonymous_context_is_not_authenticated() {
1215 let c = ctx(None, None);
1216 assert!(!c.is_authenticated());
1217 assert!(c.user_id_i64().is_none());
1218 assert!(!c.has_role("admin"));
1219 assert!(!c.has_any_role(["admin", "editor"]));
1220 }
1221
1222 #[test]
1223 fn user_id_i64_handles_non_numeric_session_value() {
1224 let c = ctx(Some("not-a-number"), None);
1225 assert!(c.user_id_i64().is_none());
1226 }
1227
1228 #[test]
1229 fn scope_accessors_mirror_roles() {
1230 let c = ctx(Some("42"), Some("editor"))
1231 .with_scopes(vec!["posts:read".to_owned(), "posts:write".to_owned()]);
1232 assert!(c.has_scope("posts:read"));
1233 assert!(!c.has_scope("posts:delete"));
1234 assert!(c.has_any_scope(["posts:delete", "posts:write"]));
1235 assert!(!c.has_any_scope(["a", "b"]));
1236 assert!(c.has_all_scopes(["posts:read", "posts:write"]));
1237 assert!(!c.has_all_scopes(["posts:read", "posts:delete"]));
1238 assert!(c.has_all_scopes(std::iter::empty::<&str>()));
1240 }
1241
1242 #[test]
1243 fn non_user_principal_authorizes_purely_on_scopes() {
1244 let c = ctx(None, None).with_scopes(vec!["posts:write".to_owned()]);
1246 assert!(!c.is_authenticated());
1247 assert!(c.user_id_i64().is_none());
1248 assert!(!c.has_role("admin"));
1249 assert!(c.has_scope("posts:write"));
1250 }
1251
1252 #[tokio::test]
1253 async fn from_session_leaves_scopes_empty() {
1254 let session = session_with(Some("42"), Some("editor"));
1255 let c = PolicyContext::from_session(&session, "user_id").await;
1256 assert!(c.scopes.is_empty());
1257 assert!(!c.has_scope("posts:read"));
1258 }
1259
1260 #[tokio::test]
1261 async fn from_session_seeds_current_actor_for_authenticated_user() {
1262 crate::current::scope_request(async {
1269 assert_eq!(crate::current::Current::actor(), None);
1270 let session = session_with(Some("42"), Some("editor"));
1271 let c = PolicyContext::from_session(&session, "user_id").await;
1272 assert_eq!(c.user_id.as_deref(), Some("42"));
1273 assert_eq!(crate::current::Current::actor(), Some("42".to_owned()));
1274 })
1275 .await;
1276 }
1277
1278 #[tokio::test]
1279 async fn from_session_leaves_current_actor_none_for_anonymous() {
1280 crate::current::scope_request(async {
1283 let session = session_with(None, None);
1284 let c = PolicyContext::from_session(&session, "user_id").await;
1285 assert!(c.user_id.is_none());
1286 assert_eq!(crate::current::Current::actor(), None);
1287 })
1288 .await;
1289 }
1290
1291 #[tokio::test]
1292 async fn from_session_does_not_override_existing_actor() {
1293 crate::current::scope_request(async {
1298 crate::current::Current::set_actor("token-principal".to_owned());
1299 let session = session_with(Some("42"), Some("editor"));
1300 let c = PolicyContext::from_session(&session, "user_id").await;
1301 assert_eq!(c.user_id.as_deref(), Some("42"));
1303 assert_eq!(
1305 crate::current::Current::actor(),
1306 Some("token-principal".to_owned())
1307 );
1308 })
1309 .await;
1310 }
1311
1312 #[test]
1313 fn forbidden_response_status_and_message_round_trip() {
1314 assert_eq!(
1315 ForbiddenResponse::Forbidden403.status(),
1316 StatusCode::FORBIDDEN
1317 );
1318 assert_eq!(
1319 ForbiddenResponse::NotFound404.status(),
1320 StatusCode::NOT_FOUND
1321 );
1322 assert_eq!(ForbiddenResponse::Forbidden403.message(), "forbidden");
1323 assert_eq!(ForbiddenResponse::NotFound404.message(), "not found");
1324 }
1325
1326 #[test]
1327 fn forbidden_response_into_error_carries_status_and_message() {
1328 let err = ForbiddenResponse::NotFound404.into_error();
1329 assert_eq!(err.status(), StatusCode::NOT_FOUND);
1330 assert_eq!(err.to_string(), "not found");
1331
1332 let err = ForbiddenResponse::Forbidden403.into_error();
1333 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1334 assert_eq!(err.to_string(), "forbidden");
1335 }
1336
1337 #[test]
1338 fn forbidden_response_parses_empty_string_as_default_404() {
1339 assert_eq!(
1340 "".parse::<ForbiddenResponse>().unwrap(),
1341 ForbiddenResponse::NotFound404
1342 );
1343 assert_eq!(
1344 "not_found".parse::<ForbiddenResponse>().unwrap(),
1345 ForbiddenResponse::NotFound404
1346 );
1347 assert_eq!(
1348 "NotFound".parse::<ForbiddenResponse>().unwrap(),
1349 ForbiddenResponse::NotFound404
1350 );
1351 assert_eq!(
1352 "Forbidden".parse::<ForbiddenResponse>().unwrap(),
1353 ForbiddenResponse::Forbidden403
1354 );
1355 }
1356
1357 #[test]
1358 fn forbidden_response_parse_error_carries_input_value() {
1359 let err = "418".parse::<ForbiddenResponse>().unwrap_err();
1360 assert!(err.contains("418"));
1361 assert!(err.contains("403"));
1362 assert!(err.contains("404"));
1363 }
1364
1365 #[test]
1366 fn forbidden_response_deserializes_from_toml() {
1367 #[derive(Debug, serde::Deserialize)]
1368 struct Holder {
1369 value: ForbiddenResponse,
1370 }
1371 let h: Holder = toml::from_str(r#"value = "403""#).unwrap();
1372 assert_eq!(h.value, ForbiddenResponse::Forbidden403);
1373 let h: Holder = toml::from_str(r#"value = "404""#).unwrap();
1374 assert_eq!(h.value, ForbiddenResponse::NotFound404);
1375 let err = toml::from_str::<Holder>(r#"value = "418""#).unwrap_err();
1376 assert!(err.to_string().contains("418"));
1377 }
1378
1379 #[test]
1380 fn registry_scope_double_registration_panics_with_clear_message() {
1381 let registry = PolicyRegistry::default();
1382 registry.register_scope::<Note, _>(EmptyScope);
1383 let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1384 registry.register_scope::<Note, _>(EmptyScope);
1385 }))
1386 .unwrap_err();
1387 let msg = panicked
1388 .downcast_ref::<String>()
1389 .map(String::as_str)
1390 .or_else(|| panicked.downcast_ref::<&'static str>().copied())
1391 .unwrap_or("");
1392 assert!(
1393 msg.contains("already registered"),
1394 "expected double-registration panic, got {msg:?}"
1395 );
1396 }
1397
1398 struct OtherResource;
1399 struct OtherPolicy;
1400 impl Policy<OtherResource> for OtherPolicy {}
1401 struct ThirdResource;
1402 struct EmptyScope;
1403 impl Scope<Note> for EmptyScope {}
1404
1405 #[test]
1406 fn registry_resolves_distinct_resource_types_independently() {
1407 let registry = PolicyRegistry::default();
1408 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1409 registry.register_policy::<OtherResource, _>(OtherPolicy);
1410
1411 assert!(registry.has_policy::<Note>());
1412 assert!(registry.has_policy::<OtherResource>());
1413 assert!(!registry.has_policy::<ThirdResource>());
1415 assert!(registry.scope::<Note>().is_none());
1416 }
1417
1418 #[test]
1419 fn registry_debug_shows_counts() {
1420 let registry = PolicyRegistry::default();
1421 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1422 registry.register_scope::<Note, _>(EmptyScope);
1423 let dbg = format!("{registry:?}");
1424 assert!(dbg.contains("PolicyRegistry"));
1425 assert!(dbg.contains("policies"));
1426 assert!(dbg.contains("scopes"));
1427 }
1428
1429 #[derive(Clone)]
1430 struct MockState {
1431 policy_registry: PolicyRegistry,
1432 forbidden_response: ForbiddenResponse,
1433 auth_session_key: String,
1434 }
1435
1436 impl ProvideAuthorizationState for MockState {
1437 fn policy_registry(&self) -> &PolicyRegistry {
1438 &self.policy_registry
1439 }
1440
1441 fn auth_session_key(&self) -> &str {
1442 &self.auth_session_key
1443 }
1444
1445 fn forbidden_response(&self) -> &ForbiddenResponse {
1446 &self.forbidden_response
1447 }
1448
1449 #[cfg(feature = "db")]
1450 fn pool(
1451 &self,
1452 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
1453 {
1454 None
1455 }
1456 }
1457
1458 impl MockState {
1459 fn with_forbidden_response(mut self, forbidden: ForbiddenResponse) -> Self {
1460 self.forbidden_response = forbidden;
1461 self
1462 }
1463 }
1464
1465 fn detached_state() -> MockState {
1466 MockState {
1467 policy_registry: PolicyRegistry::default(),
1468 forbidden_response: ForbiddenResponse::default(),
1469 auth_session_key: "user_id".to_owned(),
1470 }
1471 }
1472
1473 fn detached_state_with(registry: PolicyRegistry, forbidden: ForbiddenResponse) -> MockState {
1474 let mut state = detached_state().with_forbidden_response(forbidden);
1475 state.policy_registry = registry;
1476 state
1477 }
1478
1479 fn session_with(user_id: Option<&str>, role: Option<&str>) -> Session {
1480 let mut data = HashMap::new();
1481 if let Some(u) = user_id {
1482 data.insert("user_id".to_owned(), u.to_owned());
1483 }
1484 if let Some(r) = role {
1485 data.insert("role".to_owned(), r.to_owned());
1486 }
1487 Session::new_for_test(String::new(), data)
1488 }
1489
1490 #[tokio::test]
1491 async fn authorize_returns_500_when_no_policy_registered() {
1492 let state = detached_state_with(PolicyRegistry::default(), ForbiddenResponse::default());
1493 let session = session_with(Some("42"), None);
1494 let n = Note { author_id: 42 };
1495 let err = authorize::<Note>(&state, &session, "update", &n)
1496 .await
1497 .unwrap_err();
1498 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1499 }
1500
1501 #[tokio::test]
1502 async fn authorize_returns_configured_deny_when_policy_denies() {
1503 let registry = PolicyRegistry::default();
1504 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1505 let state = detached_state_with(registry.clone(), ForbiddenResponse::Forbidden403);
1506 let session = session_with(Some("99"), None); let n = Note { author_id: 42 };
1510 let err = authorize::<Note>(&state, &session, "update", &n)
1511 .await
1512 .unwrap_err();
1513 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1514 }
1515
1516 #[tokio::test]
1517 async fn authorize_returns_ok_when_policy_allows() {
1518 let state = detached_state();
1519 state
1520 .policy_registry()
1521 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1522 let session = session_with(Some("42"), None); let n = Note { author_id: 42 };
1524 authorize::<Note>(&state, &session, "update", &n)
1525 .await
1526 .expect("owner is allowed to update");
1527 }
1528
1529 #[tokio::test]
1530 async fn authorize_create_returns_500_when_no_policy_registered() {
1531 let state = detached_state();
1532 let session = session_with(Some("42"), None);
1533 let err = authorize_create::<Note>(&state, &session)
1534 .await
1535 .unwrap_err();
1536 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1537 }
1538
1539 #[tokio::test]
1540 async fn authorize_create_dispatches_can_create() {
1541 struct AuthOnlyCreatePolicy;
1542 impl Policy<Note> for AuthOnlyCreatePolicy {
1543 fn can_create<'a>(&'a self, ctx: &'a PolicyContext) -> BoxFuture<'a, bool> {
1544 Box::pin(async move { ctx.is_authenticated() })
1545 }
1546 }
1547
1548 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1549 state
1550 .policy_registry()
1551 .register_policy::<Note, _>(AuthOnlyCreatePolicy);
1552
1553 let anon = session_with(None, None);
1554 let err = authorize_create::<Note>(&state, &anon).await.unwrap_err();
1555 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1556
1557 let user = session_with(Some("1"), None);
1558 authorize_create::<Note>(&state, &user)
1559 .await
1560 .expect("authenticated user passes can_create");
1561 }
1562
1563 #[tokio::test]
1564 async fn authorize_create_payload_dispatches_can_create_payload() {
1565 struct OwnerPayloadPolicy;
1566 impl Policy<Note> for OwnerPayloadPolicy {
1567 fn can_create_payload<'a>(
1568 &'a self,
1569 ctx: &'a PolicyContext,
1570 payload: &'a serde_json::Value,
1571 ) -> BoxFuture<'a, bool> {
1572 Box::pin(async move {
1573 payload.get("author_id").and_then(serde_json::Value::as_i64)
1574 == ctx.user_id_i64()
1575 })
1576 }
1577 }
1578
1579 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1580 state
1581 .policy_registry()
1582 .register_policy::<Note, _>(OwnerPayloadPolicy);
1583
1584 let user = session_with(Some("1"), None);
1585 let own_payload = serde_json::json!({"author_id": 1});
1586 authorize_create_payload::<Note>(&state, &user, &own_payload)
1587 .await
1588 .expect("owner payload passes can_create_payload");
1589
1590 let other_payload = serde_json::json!({"author_id": 2});
1591 let err = authorize_create_payload::<Note>(&state, &user, &other_payload)
1592 .await
1593 .unwrap_err();
1594 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1595 }
1596
1597 #[tokio::test]
1598 async fn check_policy_create_alias_preserves_two_arg_shape() {
1599 struct AuthOnlyCreatePolicy;
1600 impl Policy<Note> for AuthOnlyCreatePolicy {
1601 fn can_create<'a>(&'a self, ctx: &'a PolicyContext) -> BoxFuture<'a, bool> {
1602 Box::pin(async move { ctx.is_authenticated() })
1603 }
1604 }
1605
1606 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1607 state
1608 .policy_registry()
1609 .register_policy::<Note, _>(AuthOnlyCreatePolicy);
1610
1611 let anon = session_with(None, None);
1612 let err = __check_policy_create::<Note>(&state, &anon)
1613 .await
1614 .unwrap_err();
1615 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1616
1617 let user = session_with(Some("1"), None);
1618 __check_policy_create::<Note>(&state, &user)
1619 .await
1620 .expect("old generated create policy alias remains compatible");
1621 }
1622
1623 #[tokio::test]
1624 async fn check_policy_create_payload_alias_dispatches_payload() {
1625 struct OwnerPayloadPolicy;
1626 impl Policy<Note> for OwnerPayloadPolicy {
1627 fn can_create_payload<'a>(
1628 &'a self,
1629 ctx: &'a PolicyContext,
1630 payload: &'a serde_json::Value,
1631 ) -> BoxFuture<'a, bool> {
1632 Box::pin(async move {
1633 payload.get("author_id").and_then(serde_json::Value::as_i64)
1634 == ctx.user_id_i64()
1635 })
1636 }
1637 }
1638
1639 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1640 state
1641 .policy_registry()
1642 .register_policy::<Note, _>(OwnerPayloadPolicy);
1643
1644 let user = session_with(Some("1"), None);
1645 let payload = serde_json::json!({"author_id": 1});
1646 __check_policy_create_payload::<Note>(&state, &user, &payload)
1647 .await
1648 .expect("new generated create policy alias passes payload");
1649 }
1650
1651 #[tokio::test]
1652 async fn check_policy_alias_round_trips() {
1653 let state = detached_state();
1654 state
1655 .policy_registry()
1656 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1657 let session = session_with(Some("42"), None);
1658 let n = Note { author_id: 42 };
1659 __check_policy::<Note>(&state, &session, "update", &n)
1662 .await
1663 .unwrap();
1664 }
1665
1666 #[tokio::test]
1667 async fn from_request_clones_pool_and_registry_from_state() {
1668 let state = detached_state();
1669 state
1670 .policy_registry()
1671 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1672 let session = session_with(Some("7"), Some("admin"));
1673 let ctx = PolicyContext::from_request(&state, &session).await;
1674 assert_eq!(ctx.user_id.as_deref(), Some("7"));
1675 assert!(ctx.has_role("admin"));
1676 assert!(ctx.policy_registry.has_policy::<Note>());
1678 }
1679
1680 #[tokio::test]
1681 async fn scoped_blanket_trait_constructible_without_registered_scope() {
1682 let state = detached_state();
1683 let session = session_with(Some("1"), None);
1684 let ctx = PolicyContext::from_request(&state, &session).await;
1685 let _query = Note::scope(&ctx);
1687 assert!(ctx.policy_registry.scope::<Note>().is_none());
1691 }
1692
1693 #[tokio::test]
1696 async fn authorize_with_scopes_returns_500_when_no_policy_registered() {
1697 let state = crate::AppState::detached();
1698 let session = session_with(None, None);
1699 let err =
1700 authorize_with_scopes::<Note>(&state, &session, None, "update", &Note { author_id: 1 })
1701 .await
1702 .unwrap_err();
1703 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1704 }
1705
1706 #[tokio::test]
1707 async fn authorize_with_scopes_returns_deny_when_policy_denies() {
1708 let state =
1709 crate::AppState::detached().with_forbidden_response(ForbiddenResponse::Forbidden403);
1710 state
1711 .policy_registry()
1712 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1713 let session = session_with(Some("99"), None); let n = Note { author_id: 42 };
1715 let err = authorize_with_scopes::<Note>(&state, &session, None, "update", &n)
1716 .await
1717 .unwrap_err();
1718 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1719 }
1720
1721 #[tokio::test]
1722 async fn authorize_with_scopes_threads_scopes_into_policy_context() {
1723 struct ScopeGatedPolicy;
1724 impl Policy<Note> for ScopeGatedPolicy {
1725 fn can_update<'a>(
1726 &'a self,
1727 ctx: &'a PolicyContext,
1728 _doc: &'a Note,
1729 ) -> BoxFuture<'a, bool> {
1730 Box::pin(async move { ctx.has_scope("posts:write") })
1731 }
1732 }
1733
1734 let state = crate::AppState::detached();
1735 state
1736 .policy_registry()
1737 .register_policy::<Note, _>(ScopeGatedPolicy);
1738 let session = session_with(None, None);
1739 let n = Note { author_id: 1 };
1740 let scopes = crate::auth::ApiTokenScopes(vec!["posts:write".to_owned()]);
1741
1742 authorize_with_scopes::<Note>(&state, &session, Some(&scopes), "update", &n)
1744 .await
1745 .expect("scope allows update");
1746
1747 authorize_with_scopes::<Note>(&state, &session, None, "update", &n)
1749 .await
1750 .unwrap_err();
1751 }
1752
1753 #[tokio::test]
1754 async fn from_request_parts_propagates_scopes() {
1755 let state = crate::AppState::detached();
1756 let session = session_with(Some("7"), Some("admin"));
1757 let scopes = crate::auth::ApiTokenScopes(vec!["posts:write".to_owned()]);
1758 let ctx = PolicyContext::from_request_parts(&state, &session, Some(&scopes)).await;
1759 assert_eq!(ctx.user_id.as_deref(), Some("7"));
1760 assert!(ctx.has_role("admin"));
1761 assert!(ctx.has_scope("posts:write"));
1762 assert!(!ctx.has_scope("posts:read"));
1763 }
1764
1765 #[tokio::test]
1766 async fn from_request_parts_with_no_scopes_leaves_scopes_empty() {
1767 let state = crate::AppState::detached();
1768 let session = session_with(Some("7"), None);
1769 let ctx = PolicyContext::from_request_parts(&state, &session, None).await;
1770 assert!(ctx.scopes.is_empty());
1771 }
1772
1773 #[tokio::test]
1774 async fn check_policy_scoped_round_trips_through_authorize_with_scopes() {
1775 let state = crate::AppState::detached();
1776 state
1777 .policy_registry()
1778 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1779 let session = session_with(Some("42"), None); let n = Note { author_id: 42 };
1781 __check_policy_scoped::<Note>(&state, &session, None, "update", &n)
1782 .await
1783 .unwrap();
1784 }
1785
1786 #[tokio::test]
1787 async fn check_policy_create_payload_scoped_threads_scopes() {
1788 struct ScopedCreatePolicy;
1789 impl Policy<Note> for ScopedCreatePolicy {
1790 fn can_create_payload<'a>(
1791 &'a self,
1792 ctx: &'a PolicyContext,
1793 _payload: &'a serde_json::Value,
1794 ) -> BoxFuture<'a, bool> {
1795 Box::pin(async move { ctx.has_scope("posts:write") })
1796 }
1797 }
1798
1799 let state =
1800 crate::AppState::detached().with_forbidden_response(ForbiddenResponse::Forbidden403);
1801 state
1802 .policy_registry()
1803 .register_policy::<Note, _>(ScopedCreatePolicy);
1804 let session = session_with(None, None);
1805 let payload = serde_json::json!({"title": "Hello"});
1806 let scopes = crate::auth::ApiTokenScopes(vec!["posts:write".to_owned()]);
1807
1808 __check_policy_create_payload_scoped::<Note>(&state, &session, Some(&scopes), &payload)
1809 .await
1810 .expect("scope grants create");
1811
1812 let err = __check_policy_create_payload_scoped::<Note>(&state, &session, None, &payload)
1813 .await
1814 .unwrap_err();
1815 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1816 }
1817
1818 #[tokio::test]
1819 async fn check_policy_create_payload_scoped_returns_500_when_no_policy() {
1820 let state = crate::AppState::detached();
1821 let session = session_with(None, None);
1822 let err = __check_policy_create_payload_scoped::<Note>(
1823 &state,
1824 &session,
1825 None,
1826 &serde_json::json!({}),
1827 )
1828 .await
1829 .unwrap_err();
1830 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1831 }
1832}