1use crate::store::Permissions;
19use rustlavel_core::{Config, Result};
20use rustlavel_db::Database;
21use rustlavel_http::plugin::{Plugin, Setup};
22use std::time::Duration;
23
24pub struct Rbac {
31 store: Permissions,
32}
33
34impl Rbac {
35 pub fn new(db: Database) -> Self {
37 Rbac { store: Permissions::new(db) }
38 }
39
40 pub fn from_config(db: Database, config: &Config) -> Result<Self> {
45 Ok(Rbac { store: Permissions::from_config(db, config)? })
46 }
47
48 pub fn with_store(store: Permissions) -> Self {
50 Rbac { store }
51 }
52
53 pub fn super_role(mut self, name: impl Into<String>) -> Self {
54 self.store = self.store.super_role(name);
55 self
56 }
57
58 pub fn cache_ttl(mut self, ttl: Duration) -> Self {
59 self.store = self.store.cache_ttl(ttl);
60 self
61 }
62
63 pub fn store(&self) -> &Permissions {
65 &self.store
66 }
67}
68
69impl Plugin for Rbac {
70 fn name(&self) -> &'static str {
71 "rbac"
72 }
73
74 fn register(self: Box<Self>, setup: &mut Setup<'_>) {
75 setup.state(self.store.clone());
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::grants::Grants;
85 use crate::guard::RbacExt;
86 use crate::store::DEFAULT_SUPER_ROLE;
87 use rustlavel_core::Context;
88 use rustlavel_db::DatabaseConfig;
89 use rustlavel_http::router::Router;
90 use rustlavel_http::{Request, TestClient};
91
92 fn offline() -> Database {
93 Database::lazy(
94 DatabaseConfig::from_url("postgres://nobody:nothing@127.0.0.1:1/none").unwrap(),
95 )
96 .unwrap()
97 }
98
99 fn registered(plugin: Rbac) -> TestClient {
101 let mut router = Router::new();
102 let config = Config::with_defaults();
103 let mut context = Some(Context::builder());
104
105 router.get("/who", |request: Request| async move {
106 let store = request.permissions()?;
107 let list = store.permissions_for(41).await?;
108 Ok::<_, rustlavel_core::Error>(list.join(","))
109 });
110
111 let mut setup = Setup { router: &mut router, config: &config, context: &mut context };
112 Box::new(plugin).register(&mut setup);
113
114 TestClient::new(router).with_context(context.expect("the builder survives setup").build())
115 }
116
117 #[tokio::test]
118 async fn the_plugin_puts_a_store_where_handlers_can_find_it() {
119 let plugin = Rbac::new(offline());
120 plugin.store().prime(
123 41,
124 Grants {
125 granted: ["posts.publish".to_string()].into_iter().collect(),
126 ..Grants::default()
127 },
128 );
129
130 registered(plugin).get("/who").await.assert_ok().assert_see("posts.publish");
131 }
132
133 #[tokio::test]
134 async fn without_the_plugin_a_handler_gets_the_configuration_error() {
135 let mut router = Router::new();
136 router.get("/who", |request: Request| async move {
137 let store = request.permissions()?;
138 Ok::<_, rustlavel_core::Error>(store.permissions_for(41).await?.join(","))
139 });
140
141 TestClient::new(router).get("/who").await.assert_status(500);
142 }
143
144 #[test]
145 fn the_plugin_is_named_for_route_list() {
146 assert_eq!(Rbac::new(offline()).name(), "rbac");
147 }
148
149 #[test]
150 fn configuration_reaches_the_store_through_the_plugin() {
151 let config = Config::new();
152 config.set("rbac.super_role", "owner");
153 config.set("rbac.cache_ttl_ms", 1_500);
154
155 let plugin = Rbac::from_config(offline(), &config).unwrap();
156
157 assert!(plugin.store().super_role_names().contains("owner"));
158 assert!(!plugin.store().super_role_names().contains(DEFAULT_SUPER_ROLE));
159 }
160
161 #[test]
162 fn the_builders_override_the_defaults() {
163 let plugin = Rbac::new(offline()).super_role("root").cache_ttl(Duration::from_secs(1));
164
165 assert_eq!(plugin.store().super_role_names().iter().collect::<Vec<_>>(), ["root"]);
166 }
167}