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()
166 && let Some(user_id) = &user_id
167 {
168 crate::current::Current::set_actor(user_id.clone());
169 }
170
171 let role = session.get("role").await;
172 let roles = role.into_iter().collect();
173 Self {
174 session: session.clone(),
175 user_id,
176 roles,
177 scopes: Vec::new(),
178 #[cfg(feature = "db")]
179 pool: None,
180 policy_registry: PolicyRegistry::default(),
181 }
182 }
183
184 pub async fn from_request(state: &impl ProvideAuthorizationState, session: &Session) -> Self {
188 let mut ctx = Self::from_session(session, state.auth_session_key()).await;
189 ctx.policy_registry = state.policy_registry().clone();
190 #[cfg(feature = "db")]
191 {
192 if let Some(pool) = state.pool() {
193 ctx.pool = Some(pool.clone());
194 }
195 }
196 ctx
197 }
198
199 #[must_use]
201 pub const fn is_authenticated(&self) -> bool {
202 self.user_id.is_some()
203 }
204
205 #[must_use]
209 pub fn user_id_i64(&self) -> Option<i64> {
210 self.user_id.as_deref().and_then(|s| s.parse().ok())
211 }
212
213 #[must_use]
215 pub fn has_role(&self, role: &str) -> bool {
216 self.roles.iter().any(|r| r == role)
217 }
218
219 #[must_use]
222 pub fn has_any_role<I, S>(&self, candidates: I) -> bool
223 where
224 I: IntoIterator<Item = S>,
225 S: AsRef<str>,
226 {
227 candidates.into_iter().any(|c| self.has_role(c.as_ref()))
228 }
229
230 #[must_use]
236 pub fn has_scope(&self, scope: &str) -> bool {
237 self.scopes.iter().any(|s| s == scope)
238 }
239
240 #[must_use]
243 pub fn has_any_scope<I, S>(&self, candidates: I) -> bool
244 where
245 I: IntoIterator<Item = S>,
246 S: AsRef<str>,
247 {
248 candidates.into_iter().any(|c| self.has_scope(c.as_ref()))
249 }
250
251 #[must_use]
254 pub fn has_all_scopes<I, S>(&self, candidates: I) -> bool
255 where
256 I: IntoIterator<Item = S>,
257 S: AsRef<str>,
258 {
259 candidates.into_iter().all(|c| self.has_scope(c.as_ref()))
260 }
261
262 #[must_use]
266 pub fn with_scopes(mut self, scopes: Vec<String>) -> Self {
267 self.scopes = scopes;
268 self
269 }
270
271 pub async fn from_request_parts(
278 state: &crate::AppState,
279 session: &Session,
280 scopes: Option<&crate::auth::ApiTokenScopes>,
281 ) -> Self {
282 let ctx = Self::from_request(state, session).await;
283 match scopes {
284 Some(s) => ctx.with_scopes(s.0.clone()),
285 None => ctx,
286 }
287 }
288
289 #[cfg(feature = "db")]
293 #[must_use]
294 pub fn with_pool(
295 mut self,
296 pool: diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>,
297 ) -> Self {
298 self.pool = Some(pool);
299 self
300 }
301}
302
303pub trait Policy<R: Send + Sync + 'static>: Send + Sync + 'static {
326 fn can_show<'a>(&'a self, _ctx: &'a PolicyContext, _resource: &'a R) -> BoxFuture<'a, bool> {
328 Box::pin(async { false })
329 }
330
331 fn can_create<'a>(&'a self, _ctx: &'a PolicyContext) -> BoxFuture<'a, bool> {
338 Box::pin(async { false })
339 }
340
341 fn can_create_payload<'a>(
345 &'a self,
346 ctx: &'a PolicyContext,
347 _payload: &'a serde_json::Value,
348 ) -> BoxFuture<'a, bool> {
349 self.can_create(ctx)
350 }
351
352 fn can_update<'a>(&'a self, _ctx: &'a PolicyContext, _resource: &'a R) -> BoxFuture<'a, bool> {
354 Box::pin(async { false })
355 }
356
357 fn can_delete<'a>(&'a self, _ctx: &'a PolicyContext, _resource: &'a R) -> BoxFuture<'a, bool> {
359 Box::pin(async { false })
360 }
361
362 fn can<'a>(
367 &'a self,
368 action: &'a str,
369 ctx: &'a PolicyContext,
370 resource: &'a R,
371 ) -> BoxFuture<'a, bool> {
372 Box::pin(async move {
373 match action {
374 "show" | "read" => self.can_show(ctx, resource).await,
375 "create" => self.can_create(ctx).await,
376 "update" | "edit" => self.can_update(ctx, resource).await,
377 "delete" | "destroy" => self.can_delete(ctx, resource).await,
378 _ => false,
379 }
380 })
381 }
382}
383
384#[cfg(feature = "db")]
399pub trait Scope<R: Send + Sync + 'static>: Send + Sync + 'static {
400 fn list<'a>(
407 &'a self,
408 _ctx: &'a PolicyContext,
409 _conn: &'a mut crate::db::RuntimeConnection,
410 ) -> BoxFuture<'a, crate::AutumnResult<Vec<R>>> {
411 Box::pin(async { Ok(Vec::new()) })
412 }
413}
414
415#[cfg(not(feature = "db"))]
420pub trait Scope<R: Send + Sync + 'static>: Send + Sync + 'static {
421 fn list<'a>(&'a self, _ctx: &'a PolicyContext) -> BoxFuture<'a, crate::AutumnResult<Vec<R>>> {
422 Box::pin(async { Ok(Vec::new()) })
423 }
424}
425
426pub struct ScopeQuery<'a, R: Send + Sync + 'static> {
436 ctx: &'a PolicyContext,
437 _marker: std::marker::PhantomData<fn() -> R>,
438}
439
440#[cfg(feature = "db")]
441impl<R: Send + Sync + 'static> ScopeQuery<'_, R> {
442 pub async fn load(
453 self,
454 conn: &mut crate::db::RuntimeConnection,
455 ) -> crate::AutumnResult<Vec<R>> {
456 let scope = self.ctx.policy_registry.scope::<R>().ok_or_else(|| {
457 crate::AutumnError::from(std::io::Error::other(format!(
458 "no scope registered for resource type {}",
459 std::any::type_name::<R>()
460 )))
461 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
462 })?;
463 scope.list(self.ctx, conn).await
464 }
465}
466
467#[cfg(not(feature = "db"))]
468impl<R: Send + Sync + 'static> ScopeQuery<'_, R> {
469 pub async fn load(self) -> crate::AutumnResult<Vec<R>> {
470 let scope = self.ctx.policy_registry.scope::<R>().ok_or_else(|| {
471 crate::AutumnError::from(std::io::Error::other(format!(
472 "no scope registered for resource type {}",
473 std::any::type_name::<R>()
474 )))
475 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
476 })?;
477 scope.list(self.ctx).await
478 }
479}
480
481pub trait Scoped: Send + Sync + Sized + 'static {
495 #[must_use]
498 fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self> {
499 ScopeQuery {
500 ctx,
501 _marker: std::marker::PhantomData,
502 }
503 }
504}
505
506impl<T: Send + Sync + 'static> Scoped for T {}
507
508#[derive(Clone, Default)]
517pub struct PolicyRegistry {
518 inner: Arc<RwLock<RegistryInner>>,
519}
520
521#[derive(Default)]
522struct RegistryInner {
523 policies: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
524 scopes: HashMap<TypeId, Arc<dyn Any + Send + Sync>>,
525}
526
527impl PolicyRegistry {
528 pub fn register_policy<R, P>(&self, policy: P)
537 where
538 R: Send + Sync + 'static,
539 P: Policy<R>,
540 {
541 let mut inner = self.inner.write().expect("policy registry lock poisoned");
542 let key = TypeId::of::<R>();
543 assert!(
544 !inner.policies.contains_key(&key),
545 "Policy for {} already registered. Multiple policies per resource are not supported.",
546 std::any::type_name::<R>()
547 );
548 let boxed: Arc<dyn Policy<R>> = Arc::new(policy);
549 inner.policies.insert(key, Arc::new(boxed));
550 }
551
552 pub fn register_scope<R, S>(&self, scope: S)
558 where
559 R: Send + Sync + 'static,
560 S: Scope<R>,
561 {
562 let mut inner = self.inner.write().expect("policy registry lock poisoned");
563 let key = TypeId::of::<R>();
564 assert!(
565 !inner.scopes.contains_key(&key),
566 "Scope for {} already registered. Multiple scopes per resource are not supported.",
567 std::any::type_name::<R>()
568 );
569 let boxed: Arc<dyn Scope<R>> = Arc::new(scope);
570 inner.scopes.insert(key, Arc::new(boxed));
571 }
572
573 #[must_use]
580 pub fn policy<R: Send + Sync + 'static>(&self) -> Option<Arc<dyn Policy<R>>> {
581 let inner = self.inner.read().expect("policy registry lock poisoned");
582 inner
583 .policies
584 .get(&TypeId::of::<R>())
585 .and_then(|a| a.downcast_ref::<Arc<dyn Policy<R>>>().cloned())
586 }
587
588 #[must_use]
594 pub fn scope<R: Send + Sync + 'static>(&self) -> Option<Arc<dyn Scope<R>>> {
595 let inner = self.inner.read().expect("policy registry lock poisoned");
596 inner
597 .scopes
598 .get(&TypeId::of::<R>())
599 .and_then(|a| a.downcast_ref::<Arc<dyn Scope<R>>>().cloned())
600 }
601
602 #[must_use]
608 pub fn has_policy<R: Send + Sync + 'static>(&self) -> bool {
609 self.inner
610 .read()
611 .expect("policy registry lock poisoned")
612 .policies
613 .contains_key(&TypeId::of::<R>())
614 }
615}
616
617impl std::fmt::Debug for PolicyRegistry {
618 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
619 let inner = self.inner.read().expect("policy registry lock poisoned");
620 f.debug_struct("PolicyRegistry")
621 .field("policies", &inner.policies.len())
622 .field("scopes", &inner.scopes.len())
623 .finish()
624 }
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
639pub enum ForbiddenResponse {
640 Forbidden403,
642 #[default]
644 NotFound404,
645}
646
647impl ForbiddenResponse {
648 #[must_use]
650 pub const fn status(self) -> StatusCode {
651 match self {
652 Self::Forbidden403 => StatusCode::FORBIDDEN,
653 Self::NotFound404 => StatusCode::NOT_FOUND,
654 }
655 }
656
657 #[must_use]
661 pub const fn message(self) -> &'static str {
662 match self {
663 Self::Forbidden403 => "forbidden",
664 Self::NotFound404 => "not found",
665 }
666 }
667
668 #[must_use]
672 pub fn into_error(self) -> crate::AutumnError {
673 crate::AutumnError::from(std::io::Error::other(self.message())).with_status(self.status())
674 }
675}
676
677impl std::str::FromStr for ForbiddenResponse {
678 type Err = String;
679
680 fn from_str(s: &str) -> Result<Self, Self::Err> {
681 match s.trim() {
682 "403" | "forbidden" | "Forbidden" => Ok(Self::Forbidden403),
683 "404" | "not_found" | "NotFound" | "" => Ok(Self::NotFound404),
684 other => Err(format!(
685 "invalid forbidden_response: {other:?} (expected \"403\" or \"404\")"
686 )),
687 }
688 }
689}
690
691impl<'de> serde::Deserialize<'de> for ForbiddenResponse {
692 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
693 where
694 D: serde::Deserializer<'de>,
695 {
696 let raw = String::deserialize(deserializer)?;
697 raw.parse().map_err(serde::de::Error::custom)
698 }
699}
700
701pub async fn authorize<R>(
738 state: &impl ProvideAuthorizationState,
739 session: &Session,
740 action: &str,
741 resource: &R,
742) -> crate::AutumnResult<()>
743where
744 R: Send + Sync + 'static,
745{
746 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
747 crate::AutumnError::from(std::io::Error::other(format!(
748 "no policy registered for resource type {}",
749 std::any::type_name::<R>()
750 )))
751 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
752 })?;
753
754 let ctx = PolicyContext::from_request(state, session).await;
755
756 if policy.can(action, &ctx, resource).await {
757 Ok(())
758 } else {
759 Err(state.forbidden_response().into_error())
760 }
761}
762
763pub async fn authorize_with_scopes<R>(
776 state: &crate::AppState,
777 session: &Session,
778 scopes: Option<&crate::auth::ApiTokenScopes>,
779 action: &str,
780 resource: &R,
781) -> crate::AutumnResult<()>
782where
783 R: Send + Sync + 'static,
784{
785 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
786 crate::AutumnError::from(std::io::Error::other(format!(
787 "no policy registered for resource type {}",
788 std::any::type_name::<R>()
789 )))
790 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
791 })?;
792
793 let ctx = PolicyContext::from_request_parts(state, session, scopes).await;
794
795 if policy.can(action, &ctx, resource).await {
796 Ok(())
797 } else {
798 Err(state.forbidden_response().into_error())
799 }
800}
801
802#[doc(hidden)]
806pub async fn __check_policy<R>(
807 state: &impl ProvideAuthorizationState,
808 session: &Session,
809 action: &str,
810 resource: &R,
811) -> crate::AutumnResult<()>
812where
813 R: Send + Sync + 'static,
814{
815 authorize(state, session, action, resource).await
816}
817
818#[doc(hidden)]
825pub async fn __check_policy_scoped<R>(
826 state: &crate::AppState,
827 session: &Session,
828 scopes: Option<&crate::auth::ApiTokenScopes>,
829 action: &str,
830 resource: &R,
831) -> crate::AutumnResult<()>
832where
833 R: Send + Sync + 'static,
834{
835 authorize_with_scopes(state, session, scopes, action, resource).await
836}
837
838#[doc(hidden)]
848pub async fn __check_policy_create<R>(
849 state: &impl ProvideAuthorizationState,
850 session: &Session,
851) -> crate::AutumnResult<()>
852where
853 R: Send + Sync + 'static,
854{
855 authorize_create::<R>(state, session).await
856}
857
858#[doc(hidden)]
867pub async fn __check_policy_create_payload<R>(
868 state: &impl ProvideAuthorizationState,
869 session: &Session,
870 payload: &serde_json::Value,
871) -> crate::AutumnResult<()>
872where
873 R: Send + Sync + 'static,
874{
875 authorize_create_payload::<R>(state, session, payload).await
876}
877
878#[doc(hidden)]
885pub async fn __check_policy_create_payload_scoped<R>(
886 state: &crate::AppState,
887 session: &Session,
888 scopes: Option<&crate::auth::ApiTokenScopes>,
889 payload: &serde_json::Value,
890) -> crate::AutumnResult<()>
891where
892 R: Send + Sync + 'static,
893{
894 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
895 crate::AutumnError::from(std::io::Error::other(format!(
896 "no policy registered for resource type {}",
897 std::any::type_name::<R>()
898 )))
899 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
900 })?;
901 let ctx = PolicyContext::from_request_parts(state, session, scopes).await;
902 if policy.can_create_payload(&ctx, payload).await {
903 Ok(())
904 } else {
905 Err(state.forbidden_response().into_error())
906 }
907}
908
909pub async fn authorize_create<R>(
920 state: &impl ProvideAuthorizationState,
921 session: &Session,
922) -> crate::AutumnResult<()>
923where
924 R: Send + Sync + 'static,
925{
926 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
927 crate::AutumnError::from(std::io::Error::other(format!(
928 "no policy registered for resource type {}",
929 std::any::type_name::<R>()
930 )))
931 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
932 })?;
933
934 let ctx = PolicyContext::from_request(state, session).await;
935
936 if policy.can_create(&ctx).await {
937 Ok(())
938 } else {
939 Err(state.forbidden_response().into_error())
940 }
941}
942
943pub async fn authorize_create_payload<R>(
956 state: &impl ProvideAuthorizationState,
957 session: &Session,
958 payload: &serde_json::Value,
959) -> crate::AutumnResult<()>
960where
961 R: Send + Sync + 'static,
962{
963 let policy = state.policy_registry().policy::<R>().ok_or_else(|| {
964 crate::AutumnError::from(std::io::Error::other(format!(
965 "no policy registered for resource type {}",
966 std::any::type_name::<R>()
967 )))
968 .with_status(StatusCode::INTERNAL_SERVER_ERROR)
969 })?;
970
971 let ctx = PolicyContext::from_request(state, session).await;
972
973 if policy.can_create_payload(&ctx, payload).await {
974 Ok(())
975 } else {
976 Err(state.forbidden_response().into_error())
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use std::collections::HashMap;
984
985 #[derive(Debug, Clone, PartialEq)]
986 struct Note {
987 author_id: i64,
988 }
989
990 #[derive(Default)]
991 struct AdminOrOwnerPolicy;
992
993 impl Policy<Note> for AdminOrOwnerPolicy {
994 fn can_show<'a>(&'a self, _ctx: &'a PolicyContext, _note: &'a Note) -> BoxFuture<'a, bool> {
995 Box::pin(async { true })
996 }
997 fn can_update<'a>(&'a self, ctx: &'a PolicyContext, note: &'a Note) -> BoxFuture<'a, bool> {
998 Box::pin(
999 async move { ctx.has_role("admin") || ctx.user_id_i64() == Some(note.author_id) },
1000 )
1001 }
1002 fn can_delete<'a>(
1003 &'a self,
1004 ctx: &'a PolicyContext,
1005 _note: &'a Note,
1006 ) -> BoxFuture<'a, bool> {
1007 Box::pin(async move { ctx.has_role("admin") })
1008 }
1009 }
1010
1011 fn ctx(user_id: Option<&str>, role: Option<&str>) -> PolicyContext {
1012 let session = Session::new_for_test(String::new(), HashMap::new());
1013 PolicyContext {
1014 session,
1015 user_id: user_id.map(str::to_owned),
1016 roles: role.into_iter().map(str::to_owned).collect(),
1017 scopes: Vec::new(),
1018 #[cfg(feature = "db")]
1019 pool: None,
1020 policy_registry: PolicyRegistry::default(),
1021 }
1022 }
1023
1024 #[tokio::test]
1025 async fn default_impls_deny() {
1026 struct EmptyPolicy;
1027 impl Policy<Note> for EmptyPolicy {}
1028 let policy = EmptyPolicy;
1029 let c = ctx(Some("1"), None);
1030 let n = Note { author_id: 1 };
1031 assert!(!policy.can_show(&c, &n).await);
1032 assert!(!policy.can_create(&c).await);
1033 assert!(!policy.can_update(&c, &n).await);
1034 assert!(!policy.can_delete(&c, &n).await);
1035 assert!(!policy.can("publish", &c, &n).await);
1036 }
1037
1038 #[tokio::test]
1039 async fn owner_can_update() {
1040 let policy = AdminOrOwnerPolicy;
1041 let c = ctx(Some("42"), None);
1042 let n = Note { author_id: 42 };
1043 assert!(policy.can_update(&c, &n).await);
1044 assert!(!policy.can_delete(&c, &n).await);
1045 }
1046
1047 #[tokio::test]
1048 async fn non_owner_cannot_update() {
1049 let policy = AdminOrOwnerPolicy;
1050 let c = ctx(Some("99"), None);
1051 let n = Note { author_id: 42 };
1052 assert!(!policy.can_update(&c, &n).await);
1053 }
1054
1055 #[tokio::test]
1056 async fn admin_can_delete() {
1057 let policy = AdminOrOwnerPolicy;
1058 let c = ctx(Some("99"), Some("admin"));
1059 let n = Note { author_id: 42 };
1060 assert!(policy.can_delete(&c, &n).await);
1061 }
1062
1063 #[tokio::test]
1064 async fn can_dispatches_named_actions() {
1065 let policy = AdminOrOwnerPolicy;
1066 let c = ctx(Some("42"), None);
1067 let n = Note { author_id: 42 };
1068 assert!(policy.can("show", &c, &n).await);
1069 assert!(policy.can("update", &c, &n).await);
1070 assert!(policy.can("edit", &c, &n).await);
1071 assert!(!policy.can("publish", &c, &n).await);
1072 }
1073
1074 #[test]
1075 fn policy_registry_stores_and_resolves() {
1076 let registry = PolicyRegistry::default();
1077 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1078 assert!(registry.has_policy::<Note>());
1079 assert!(registry.policy::<Note>().is_some());
1080 assert!(registry.scope::<Note>().is_none());
1081 }
1082
1083 #[test]
1084 #[should_panic(expected = "already registered")]
1085 fn double_policy_registration_panics() {
1086 let registry = PolicyRegistry::default();
1087 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1088 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1089 }
1090
1091 #[test]
1092 fn forbidden_response_default_is_404() {
1093 let resp = ForbiddenResponse::default();
1094 assert_eq!(resp, ForbiddenResponse::NotFound404);
1095 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1096 }
1097
1098 #[test]
1099 fn forbidden_response_parses_strings() {
1100 assert_eq!(
1101 "403".parse::<ForbiddenResponse>().unwrap(),
1102 ForbiddenResponse::Forbidden403
1103 );
1104 assert_eq!(
1105 "404".parse::<ForbiddenResponse>().unwrap(),
1106 ForbiddenResponse::NotFound404
1107 );
1108 assert_eq!(
1109 "forbidden".parse::<ForbiddenResponse>().unwrap(),
1110 ForbiddenResponse::Forbidden403
1111 );
1112 assert!("418".parse::<ForbiddenResponse>().is_err());
1113 }
1114
1115 #[test]
1116 fn policy_context_helpers() {
1117 let c = ctx(Some("42"), Some("editor"));
1118 assert!(c.is_authenticated());
1119 assert_eq!(c.user_id_i64(), Some(42));
1120 assert!(c.has_role("editor"));
1121 assert!(!c.has_role("admin"));
1122 assert!(c.has_any_role(["admin", "editor"]));
1123 assert!(!c.has_any_role(["viewer", "guest"]));
1124 }
1125
1126 #[test]
1127 fn anonymous_context_is_not_authenticated() {
1128 let c = ctx(None, None);
1129 assert!(!c.is_authenticated());
1130 assert!(c.user_id_i64().is_none());
1131 assert!(!c.has_role("admin"));
1132 assert!(!c.has_any_role(["admin", "editor"]));
1133 }
1134
1135 #[test]
1136 fn user_id_i64_handles_non_numeric_session_value() {
1137 let c = ctx(Some("not-a-number"), None);
1138 assert!(c.user_id_i64().is_none());
1139 }
1140
1141 #[test]
1142 fn scope_accessors_mirror_roles() {
1143 let c = ctx(Some("42"), Some("editor"))
1144 .with_scopes(vec!["posts:read".to_owned(), "posts:write".to_owned()]);
1145 assert!(c.has_scope("posts:read"));
1146 assert!(!c.has_scope("posts:delete"));
1147 assert!(c.has_any_scope(["posts:delete", "posts:write"]));
1148 assert!(!c.has_any_scope(["a", "b"]));
1149 assert!(c.has_all_scopes(["posts:read", "posts:write"]));
1150 assert!(!c.has_all_scopes(["posts:read", "posts:delete"]));
1151 assert!(c.has_all_scopes(std::iter::empty::<&str>()));
1153 }
1154
1155 #[test]
1156 fn non_user_principal_authorizes_purely_on_scopes() {
1157 let c = ctx(None, None).with_scopes(vec!["posts:write".to_owned()]);
1159 assert!(!c.is_authenticated());
1160 assert!(c.user_id_i64().is_none());
1161 assert!(!c.has_role("admin"));
1162 assert!(c.has_scope("posts:write"));
1163 }
1164
1165 #[tokio::test]
1166 async fn from_session_leaves_scopes_empty() {
1167 let session = session_with(Some("42"), Some("editor"));
1168 let c = PolicyContext::from_session(&session, "user_id").await;
1169 assert!(c.scopes.is_empty());
1170 assert!(!c.has_scope("posts:read"));
1171 }
1172
1173 #[tokio::test]
1174 async fn from_session_seeds_current_actor_for_authenticated_user() {
1175 crate::current::scope_request(async {
1182 assert_eq!(crate::current::Current::actor(), None);
1183 let session = session_with(Some("42"), Some("editor"));
1184 let c = PolicyContext::from_session(&session, "user_id").await;
1185 assert_eq!(c.user_id.as_deref(), Some("42"));
1186 assert_eq!(crate::current::Current::actor(), Some("42".to_owned()));
1187 })
1188 .await;
1189 }
1190
1191 #[tokio::test]
1192 async fn from_session_leaves_current_actor_none_for_anonymous() {
1193 crate::current::scope_request(async {
1196 let session = session_with(None, None);
1197 let c = PolicyContext::from_session(&session, "user_id").await;
1198 assert!(c.user_id.is_none());
1199 assert_eq!(crate::current::Current::actor(), None);
1200 })
1201 .await;
1202 }
1203
1204 #[tokio::test]
1205 async fn from_session_does_not_override_existing_actor() {
1206 crate::current::scope_request(async {
1211 crate::current::Current::set_actor("token-principal".to_owned());
1212 let session = session_with(Some("42"), Some("editor"));
1213 let c = PolicyContext::from_session(&session, "user_id").await;
1214 assert_eq!(c.user_id.as_deref(), Some("42"));
1216 assert_eq!(
1218 crate::current::Current::actor(),
1219 Some("token-principal".to_owned())
1220 );
1221 })
1222 .await;
1223 }
1224
1225 #[test]
1226 fn forbidden_response_status_and_message_round_trip() {
1227 assert_eq!(
1228 ForbiddenResponse::Forbidden403.status(),
1229 StatusCode::FORBIDDEN
1230 );
1231 assert_eq!(
1232 ForbiddenResponse::NotFound404.status(),
1233 StatusCode::NOT_FOUND
1234 );
1235 assert_eq!(ForbiddenResponse::Forbidden403.message(), "forbidden");
1236 assert_eq!(ForbiddenResponse::NotFound404.message(), "not found");
1237 }
1238
1239 #[test]
1240 fn forbidden_response_into_error_carries_status_and_message() {
1241 let err = ForbiddenResponse::NotFound404.into_error();
1242 assert_eq!(err.status(), StatusCode::NOT_FOUND);
1243 assert_eq!(err.to_string(), "not found");
1244
1245 let err = ForbiddenResponse::Forbidden403.into_error();
1246 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1247 assert_eq!(err.to_string(), "forbidden");
1248 }
1249
1250 #[test]
1251 fn forbidden_response_parses_empty_string_as_default_404() {
1252 assert_eq!(
1253 "".parse::<ForbiddenResponse>().unwrap(),
1254 ForbiddenResponse::NotFound404
1255 );
1256 assert_eq!(
1257 "not_found".parse::<ForbiddenResponse>().unwrap(),
1258 ForbiddenResponse::NotFound404
1259 );
1260 assert_eq!(
1261 "NotFound".parse::<ForbiddenResponse>().unwrap(),
1262 ForbiddenResponse::NotFound404
1263 );
1264 assert_eq!(
1265 "Forbidden".parse::<ForbiddenResponse>().unwrap(),
1266 ForbiddenResponse::Forbidden403
1267 );
1268 }
1269
1270 #[test]
1271 fn forbidden_response_parse_error_carries_input_value() {
1272 let err = "418".parse::<ForbiddenResponse>().unwrap_err();
1273 assert!(err.contains("418"));
1274 assert!(err.contains("403"));
1275 assert!(err.contains("404"));
1276 }
1277
1278 #[test]
1279 fn forbidden_response_deserializes_from_toml() {
1280 #[derive(Debug, serde::Deserialize)]
1281 struct Holder {
1282 value: ForbiddenResponse,
1283 }
1284 let h: Holder = toml::from_str(r#"value = "403""#).unwrap();
1285 assert_eq!(h.value, ForbiddenResponse::Forbidden403);
1286 let h: Holder = toml::from_str(r#"value = "404""#).unwrap();
1287 assert_eq!(h.value, ForbiddenResponse::NotFound404);
1288 let err = toml::from_str::<Holder>(r#"value = "418""#).unwrap_err();
1289 assert!(err.to_string().contains("418"));
1290 }
1291
1292 #[test]
1293 fn registry_scope_double_registration_panics_with_clear_message() {
1294 let registry = PolicyRegistry::default();
1295 registry.register_scope::<Note, _>(EmptyScope);
1296 let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1297 registry.register_scope::<Note, _>(EmptyScope);
1298 }))
1299 .unwrap_err();
1300 let msg = panicked
1301 .downcast_ref::<String>()
1302 .map(String::as_str)
1303 .or_else(|| panicked.downcast_ref::<&'static str>().copied())
1304 .unwrap_or("");
1305 assert!(
1306 msg.contains("already registered"),
1307 "expected double-registration panic, got {msg:?}"
1308 );
1309 }
1310
1311 struct OtherResource;
1312 struct OtherPolicy;
1313 impl Policy<OtherResource> for OtherPolicy {}
1314 struct ThirdResource;
1315 struct EmptyScope;
1316 impl Scope<Note> for EmptyScope {}
1317
1318 #[test]
1319 fn registry_resolves_distinct_resource_types_independently() {
1320 let registry = PolicyRegistry::default();
1321 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1322 registry.register_policy::<OtherResource, _>(OtherPolicy);
1323
1324 assert!(registry.has_policy::<Note>());
1325 assert!(registry.has_policy::<OtherResource>());
1326 assert!(!registry.has_policy::<ThirdResource>());
1328 assert!(registry.scope::<Note>().is_none());
1329 }
1330
1331 #[test]
1332 fn registry_debug_shows_counts() {
1333 let registry = PolicyRegistry::default();
1334 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1335 registry.register_scope::<Note, _>(EmptyScope);
1336 let dbg = format!("{registry:?}");
1337 assert!(dbg.contains("PolicyRegistry"));
1338 assert!(dbg.contains("policies"));
1339 assert!(dbg.contains("scopes"));
1340 }
1341
1342 #[derive(Clone)]
1343 struct MockState {
1344 policy_registry: PolicyRegistry,
1345 forbidden_response: ForbiddenResponse,
1346 auth_session_key: String,
1347 }
1348
1349 impl ProvideAuthorizationState for MockState {
1350 fn policy_registry(&self) -> &PolicyRegistry {
1351 &self.policy_registry
1352 }
1353
1354 fn auth_session_key(&self) -> &str {
1355 &self.auth_session_key
1356 }
1357
1358 fn forbidden_response(&self) -> &ForbiddenResponse {
1359 &self.forbidden_response
1360 }
1361
1362 #[cfg(feature = "db")]
1363 fn pool(
1364 &self,
1365 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
1366 {
1367 None
1368 }
1369 }
1370
1371 impl MockState {
1372 fn with_forbidden_response(mut self, forbidden: ForbiddenResponse) -> Self {
1373 self.forbidden_response = forbidden;
1374 self
1375 }
1376 }
1377
1378 fn detached_state() -> MockState {
1379 MockState {
1380 policy_registry: PolicyRegistry::default(),
1381 forbidden_response: ForbiddenResponse::default(),
1382 auth_session_key: "user_id".to_owned(),
1383 }
1384 }
1385
1386 fn detached_state_with(registry: PolicyRegistry, forbidden: ForbiddenResponse) -> MockState {
1387 let mut state = detached_state().with_forbidden_response(forbidden);
1388 state.policy_registry = registry;
1389 state
1390 }
1391
1392 fn session_with(user_id: Option<&str>, role: Option<&str>) -> Session {
1393 let mut data = HashMap::new();
1394 if let Some(u) = user_id {
1395 data.insert("user_id".to_owned(), u.to_owned());
1396 }
1397 if let Some(r) = role {
1398 data.insert("role".to_owned(), r.to_owned());
1399 }
1400 Session::new_for_test(String::new(), data)
1401 }
1402
1403 #[tokio::test]
1404 async fn authorize_returns_500_when_no_policy_registered() {
1405 let state = detached_state_with(PolicyRegistry::default(), ForbiddenResponse::default());
1406 let session = session_with(Some("42"), None);
1407 let n = Note { author_id: 42 };
1408 let err = authorize::<Note>(&state, &session, "update", &n)
1409 .await
1410 .unwrap_err();
1411 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1412 }
1413
1414 #[tokio::test]
1415 async fn authorize_returns_configured_deny_when_policy_denies() {
1416 let registry = PolicyRegistry::default();
1417 registry.register_policy::<Note, _>(AdminOrOwnerPolicy);
1418 let state = detached_state_with(registry.clone(), ForbiddenResponse::Forbidden403);
1419 let session = session_with(Some("99"), None); let n = Note { author_id: 42 };
1423 let err = authorize::<Note>(&state, &session, "update", &n)
1424 .await
1425 .unwrap_err();
1426 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1427 }
1428
1429 #[tokio::test]
1430 async fn authorize_returns_ok_when_policy_allows() {
1431 let state = detached_state();
1432 state
1433 .policy_registry()
1434 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1435 let session = session_with(Some("42"), None); let n = Note { author_id: 42 };
1437 authorize::<Note>(&state, &session, "update", &n)
1438 .await
1439 .expect("owner is allowed to update");
1440 }
1441
1442 #[tokio::test]
1443 async fn authorize_create_returns_500_when_no_policy_registered() {
1444 let state = detached_state();
1445 let session = session_with(Some("42"), None);
1446 let err = authorize_create::<Note>(&state, &session)
1447 .await
1448 .unwrap_err();
1449 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1450 }
1451
1452 #[tokio::test]
1453 async fn authorize_create_dispatches_can_create() {
1454 struct AuthOnlyCreatePolicy;
1455 impl Policy<Note> for AuthOnlyCreatePolicy {
1456 fn can_create<'a>(&'a self, ctx: &'a PolicyContext) -> BoxFuture<'a, bool> {
1457 Box::pin(async move { ctx.is_authenticated() })
1458 }
1459 }
1460
1461 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1462 state
1463 .policy_registry()
1464 .register_policy::<Note, _>(AuthOnlyCreatePolicy);
1465
1466 let anon = session_with(None, None);
1467 let err = authorize_create::<Note>(&state, &anon).await.unwrap_err();
1468 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1469
1470 let user = session_with(Some("1"), None);
1471 authorize_create::<Note>(&state, &user)
1472 .await
1473 .expect("authenticated user passes can_create");
1474 }
1475
1476 #[tokio::test]
1477 async fn authorize_create_payload_dispatches_can_create_payload() {
1478 struct OwnerPayloadPolicy;
1479 impl Policy<Note> for OwnerPayloadPolicy {
1480 fn can_create_payload<'a>(
1481 &'a self,
1482 ctx: &'a PolicyContext,
1483 payload: &'a serde_json::Value,
1484 ) -> BoxFuture<'a, bool> {
1485 Box::pin(async move {
1486 payload.get("author_id").and_then(serde_json::Value::as_i64)
1487 == ctx.user_id_i64()
1488 })
1489 }
1490 }
1491
1492 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1493 state
1494 .policy_registry()
1495 .register_policy::<Note, _>(OwnerPayloadPolicy);
1496
1497 let user = session_with(Some("1"), None);
1498 let own_payload = serde_json::json!({"author_id": 1});
1499 authorize_create_payload::<Note>(&state, &user, &own_payload)
1500 .await
1501 .expect("owner payload passes can_create_payload");
1502
1503 let other_payload = serde_json::json!({"author_id": 2});
1504 let err = authorize_create_payload::<Note>(&state, &user, &other_payload)
1505 .await
1506 .unwrap_err();
1507 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1508 }
1509
1510 #[tokio::test]
1511 async fn check_policy_create_alias_preserves_two_arg_shape() {
1512 struct AuthOnlyCreatePolicy;
1513 impl Policy<Note> for AuthOnlyCreatePolicy {
1514 fn can_create<'a>(&'a self, ctx: &'a PolicyContext) -> BoxFuture<'a, bool> {
1515 Box::pin(async move { ctx.is_authenticated() })
1516 }
1517 }
1518
1519 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1520 state
1521 .policy_registry()
1522 .register_policy::<Note, _>(AuthOnlyCreatePolicy);
1523
1524 let anon = session_with(None, None);
1525 let err = __check_policy_create::<Note>(&state, &anon)
1526 .await
1527 .unwrap_err();
1528 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1529
1530 let user = session_with(Some("1"), None);
1531 __check_policy_create::<Note>(&state, &user)
1532 .await
1533 .expect("old generated create policy alias remains compatible");
1534 }
1535
1536 #[tokio::test]
1537 async fn check_policy_create_payload_alias_dispatches_payload() {
1538 struct OwnerPayloadPolicy;
1539 impl Policy<Note> for OwnerPayloadPolicy {
1540 fn can_create_payload<'a>(
1541 &'a self,
1542 ctx: &'a PolicyContext,
1543 payload: &'a serde_json::Value,
1544 ) -> BoxFuture<'a, bool> {
1545 Box::pin(async move {
1546 payload.get("author_id").and_then(serde_json::Value::as_i64)
1547 == ctx.user_id_i64()
1548 })
1549 }
1550 }
1551
1552 let state = detached_state().with_forbidden_response(ForbiddenResponse::Forbidden403);
1553 state
1554 .policy_registry()
1555 .register_policy::<Note, _>(OwnerPayloadPolicy);
1556
1557 let user = session_with(Some("1"), None);
1558 let payload = serde_json::json!({"author_id": 1});
1559 __check_policy_create_payload::<Note>(&state, &user, &payload)
1560 .await
1561 .expect("new generated create policy alias passes payload");
1562 }
1563
1564 #[tokio::test]
1565 async fn check_policy_alias_round_trips() {
1566 let state = detached_state();
1567 state
1568 .policy_registry()
1569 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1570 let session = session_with(Some("42"), None);
1571 let n = Note { author_id: 42 };
1572 __check_policy::<Note>(&state, &session, "update", &n)
1575 .await
1576 .unwrap();
1577 }
1578
1579 #[tokio::test]
1580 async fn from_request_clones_pool_and_registry_from_state() {
1581 let state = detached_state();
1582 state
1583 .policy_registry()
1584 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1585 let session = session_with(Some("7"), Some("admin"));
1586 let ctx = PolicyContext::from_request(&state, &session).await;
1587 assert_eq!(ctx.user_id.as_deref(), Some("7"));
1588 assert!(ctx.has_role("admin"));
1589 assert!(ctx.policy_registry.has_policy::<Note>());
1591 }
1592
1593 #[tokio::test]
1594 async fn scoped_blanket_trait_constructible_without_registered_scope() {
1595 let state = detached_state();
1596 let session = session_with(Some("1"), None);
1597 let ctx = PolicyContext::from_request(&state, &session).await;
1598 let _query = Note::scope(&ctx);
1600 assert!(ctx.policy_registry.scope::<Note>().is_none());
1604 }
1605
1606 #[tokio::test]
1609 async fn authorize_with_scopes_returns_500_when_no_policy_registered() {
1610 let state = crate::AppState::detached();
1611 let session = session_with(None, None);
1612 let err =
1613 authorize_with_scopes::<Note>(&state, &session, None, "update", &Note { author_id: 1 })
1614 .await
1615 .unwrap_err();
1616 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1617 }
1618
1619 #[tokio::test]
1620 async fn authorize_with_scopes_returns_deny_when_policy_denies() {
1621 let state =
1622 crate::AppState::detached().with_forbidden_response(ForbiddenResponse::Forbidden403);
1623 state
1624 .policy_registry()
1625 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1626 let session = session_with(Some("99"), None); let n = Note { author_id: 42 };
1628 let err = authorize_with_scopes::<Note>(&state, &session, None, "update", &n)
1629 .await
1630 .unwrap_err();
1631 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1632 }
1633
1634 #[tokio::test]
1635 async fn authorize_with_scopes_threads_scopes_into_policy_context() {
1636 struct ScopeGatedPolicy;
1637 impl Policy<Note> for ScopeGatedPolicy {
1638 fn can_update<'a>(
1639 &'a self,
1640 ctx: &'a PolicyContext,
1641 _doc: &'a Note,
1642 ) -> BoxFuture<'a, bool> {
1643 Box::pin(async move { ctx.has_scope("posts:write") })
1644 }
1645 }
1646
1647 let state = crate::AppState::detached();
1648 state
1649 .policy_registry()
1650 .register_policy::<Note, _>(ScopeGatedPolicy);
1651 let session = session_with(None, None);
1652 let n = Note { author_id: 1 };
1653 let scopes = crate::auth::ApiTokenScopes(vec!["posts:write".to_owned()]);
1654
1655 authorize_with_scopes::<Note>(&state, &session, Some(&scopes), "update", &n)
1657 .await
1658 .expect("scope allows update");
1659
1660 authorize_with_scopes::<Note>(&state, &session, None, "update", &n)
1662 .await
1663 .unwrap_err();
1664 }
1665
1666 #[tokio::test]
1667 async fn from_request_parts_propagates_scopes() {
1668 let state = crate::AppState::detached();
1669 let session = session_with(Some("7"), Some("admin"));
1670 let scopes = crate::auth::ApiTokenScopes(vec!["posts:write".to_owned()]);
1671 let ctx = PolicyContext::from_request_parts(&state, &session, Some(&scopes)).await;
1672 assert_eq!(ctx.user_id.as_deref(), Some("7"));
1673 assert!(ctx.has_role("admin"));
1674 assert!(ctx.has_scope("posts:write"));
1675 assert!(!ctx.has_scope("posts:read"));
1676 }
1677
1678 #[tokio::test]
1679 async fn from_request_parts_with_no_scopes_leaves_scopes_empty() {
1680 let state = crate::AppState::detached();
1681 let session = session_with(Some("7"), None);
1682 let ctx = PolicyContext::from_request_parts(&state, &session, None).await;
1683 assert!(ctx.scopes.is_empty());
1684 }
1685
1686 #[tokio::test]
1687 async fn check_policy_scoped_round_trips_through_authorize_with_scopes() {
1688 let state = crate::AppState::detached();
1689 state
1690 .policy_registry()
1691 .register_policy::<Note, _>(AdminOrOwnerPolicy);
1692 let session = session_with(Some("42"), None); let n = Note { author_id: 42 };
1694 __check_policy_scoped::<Note>(&state, &session, None, "update", &n)
1695 .await
1696 .unwrap();
1697 }
1698
1699 #[tokio::test]
1700 async fn check_policy_create_payload_scoped_threads_scopes() {
1701 struct ScopedCreatePolicy;
1702 impl Policy<Note> for ScopedCreatePolicy {
1703 fn can_create_payload<'a>(
1704 &'a self,
1705 ctx: &'a PolicyContext,
1706 _payload: &'a serde_json::Value,
1707 ) -> BoxFuture<'a, bool> {
1708 Box::pin(async move { ctx.has_scope("posts:write") })
1709 }
1710 }
1711
1712 let state =
1713 crate::AppState::detached().with_forbidden_response(ForbiddenResponse::Forbidden403);
1714 state
1715 .policy_registry()
1716 .register_policy::<Note, _>(ScopedCreatePolicy);
1717 let session = session_with(None, None);
1718 let payload = serde_json::json!({"title": "Hello"});
1719 let scopes = crate::auth::ApiTokenScopes(vec!["posts:write".to_owned()]);
1720
1721 __check_policy_create_payload_scoped::<Note>(&state, &session, Some(&scopes), &payload)
1722 .await
1723 .expect("scope grants create");
1724
1725 let err = __check_policy_create_payload_scoped::<Note>(&state, &session, None, &payload)
1726 .await
1727 .unwrap_err();
1728 assert_eq!(err.status(), StatusCode::FORBIDDEN);
1729 }
1730
1731 #[tokio::test]
1732 async fn check_policy_create_payload_scoped_returns_500_when_no_policy() {
1733 let state = crate::AppState::detached();
1734 let session = session_with(None, None);
1735 let err = __check_policy_create_payload_scoped::<Note>(
1736 &state,
1737 &session,
1738 None,
1739 &serde_json::json!({}),
1740 )
1741 .await
1742 .unwrap_err();
1743 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
1744 }
1745}