1use std::any::Any;
31use std::collections::HashMap;
32use std::sync::Arc;
33
34use rainier_support::{Error, Result};
35
36pub trait Actor: Send + Sync + 'static {}
59
60impl<T: Send + Sync + 'static> Actor for T {}
61
62type Ability<A> = Arc<dyn Fn(&A, Option<&dyn Any>) -> bool + Send + Sync>;
64
65type BeforeCheck<A> = Arc<dyn Fn(&A, &str) -> Option<bool> + Send + Sync>;
67
68pub struct Gate<A: Actor> {
75 abilities: HashMap<String, Ability<A>>,
76 before: Vec<BeforeCheck<A>>,
78}
79
80impl<A: Actor> Default for Gate<A> {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl<A: Actor> Gate<A> {
87 pub fn new() -> Self {
89 Self { abilities: HashMap::new(), before: Vec::new() }
90 }
91
92 pub fn define<S, F>(mut self, ability: impl Into<String>, check: F) -> Self
94 where
95 S: 'static,
96 F: Fn(&A, Option<&S>) -> bool + Send + Sync + 'static,
97 {
98 let erased: Ability<A> = Arc::new(move |user, subject| {
99 match subject {
100 Some(subject) => match subject.downcast_ref::<S>() {
103 Some(typed) => check(user, Some(typed)),
104 None => false,
105 },
106 None => check(user, None),
107 }
108 });
109 self.abilities.insert(ability.into(), erased);
110 self
111 }
112
113 pub fn define_simple<F>(self, ability: impl Into<String>, check: F) -> Self
115 where
116 F: Fn(&A) -> bool + Send + Sync + 'static,
117 {
118 self.define::<(), _>(ability, move |user, _| check(user))
119 }
120
121 pub fn before<F>(mut self, check: F) -> Self
127 where
128 F: Fn(&A, &str) -> Option<bool> + Send + Sync + 'static,
129 {
130 self.before.push(Arc::new(check));
131 self
132 }
133
134 pub fn allows<S: 'static>(&self, ability: &str, actor: &A, subject: Option<&S>) -> bool {
139 for before in &self.before {
140 if let Some(decision) = before(actor, ability) {
141 return decision;
142 }
143 }
144
145 match self.abilities.get(ability) {
146 Some(check) => check(actor, subject.map(|s| s as &dyn Any)),
147 None => false,
148 }
149 }
150
151 pub fn denies<S: 'static>(&self, ability: &str, actor: &A, subject: Option<&S>) -> bool {
153 !self.allows(ability, actor, subject)
154 }
155
156 pub fn allows_any(&self, ability: &str, actor: &A) -> bool {
158 self.allows::<()>(ability, actor, None)
159 }
160
161 pub fn authorize<S: 'static>(
163 &self,
164 ability: &str,
165 actor: &A,
166 subject: Option<&S>,
167 ) -> Result<()> {
168 if self.allows(ability, actor, subject) {
169 return Ok(());
170 }
171 Err(Error::unauthorized("This action is unauthorized."))
172 }
173
174 pub fn has(&self, ability: &str) -> bool {
176 self.abilities.contains_key(ability)
177 }
178
179 pub fn abilities(&self) -> Vec<&str> {
181 let mut names: Vec<&str> = self.abilities.keys().map(String::as_str).collect();
182 names.sort();
183 names
184 }
185}
186
187impl<A: Actor> std::fmt::Debug for Gate<A> {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("Gate")
190 .field("abilities", &self.abilities())
191 .field("before", &self.before.len())
192 .finish()
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[derive(Debug, Clone)]
201 struct User {
202 id: u64,
203 admin: bool,
204 banned: bool,
205 }
206
207 #[derive(Debug)]
208 struct Post {
209 author_id: u64,
210 }
211
212 #[derive(Debug)]
213 struct Comment;
214
215 fn user(id: u64) -> User {
216 User { id, admin: false, banned: false }
217 }
218
219 fn admin() -> User {
220 User { id: 99, admin: true, banned: false }
221 }
222
223 fn gate() -> Gate<User> {
224 Gate::new()
225 .define("posts.update", |user: &User, post: Option<&Post>| {
226 post.is_some_and(|post| post.author_id == user.id)
227 })
228 .define_simple("posts.create", |user: &User| !user.banned)
229 }
230
231 #[test]
232 fn an_ability_decides_from_the_user_and_subject() {
233 let gate = gate();
234 let post = Post { author_id: 1 };
235
236 assert!(gate.allows("posts.update", &user(1), Some(&post)));
237 assert!(gate.denies("posts.update", &user(2), Some(&post)));
238 }
239
240 #[test]
241 fn an_undefined_ability_is_denied() {
242 let gate = gate();
244 assert!(gate.denies::<Post>("posts.publish", &admin(), None));
245 assert!(!gate.has("posts.publish"));
246 }
247
248 #[test]
249 fn an_ability_with_no_subject() {
250 let gate = gate();
251 assert!(gate.allows_any("posts.create", &user(1)));
252
253 let banned = User { banned: true, ..user(1) };
254 assert!(!gate.allows_any("posts.create", &banned));
255 }
256
257 #[test]
258 fn a_subject_of_the_wrong_type_is_denied_rather_than_ignored() {
259 let gate = gate();
260 assert!(gate.denies("posts.update", &user(1), Some(&Comment)));
263 }
264
265 #[test]
266 fn a_before_check_short_circuits_everything() {
267 let gate = gate().before(|user: &User, _ability: &str| user.admin.then_some(true));
268
269 let someone_elses = Post { author_id: 1 };
270 assert!(gate.allows("posts.update", &admin(), Some(&someone_elses)));
271 assert!(gate.allows_any("posts.anything.undefined", &admin()));
272 }
273
274 #[test]
275 fn a_before_check_can_also_deny_outright() {
276 let gate = gate().before(|user: &User, _: &str| user.banned.then_some(false));
277
278 let banned = User { banned: true, ..user(1) };
279 let own_post = Post { author_id: 1 };
280 assert!(gate.denies("posts.update", &banned, Some(&own_post)));
281 }
282
283 #[test]
284 fn a_before_check_returning_none_defers_to_the_ability() {
285 let gate = gate().before(|_: &User, _: &str| None);
286 let post = Post { author_id: 1 };
287
288 assert!(gate.allows("posts.update", &user(1), Some(&post)));
289 assert!(gate.denies("posts.update", &user(2), Some(&post)));
290 }
291
292 #[test]
293 fn authorize_turns_a_denial_into_a_403() {
294 let gate = gate();
295 let post = Post { author_id: 1 };
296
297 assert!(gate.authorize("posts.update", &user(1), Some(&post)).is_ok());
298
299 let err = gate.authorize("posts.update", &user(2), Some(&post)).unwrap_err();
300 assert_eq!(err.status(), 403);
301 assert_eq!(err.message(), "This action is unauthorized.");
302 }
303
304 #[test]
305 fn defined_abilities_can_be_listed() {
306 assert_eq!(gate().abilities(), vec!["posts.create", "posts.update"]);
307 assert!(gate().has("posts.update"));
308 }
309
310 #[derive(Debug)]
312 struct ApiClient {
313 id: String,
314 scopes: Vec<&'static str>,
315 }
316
317 #[derive(Debug)]
319 struct StsPrincipal {
320 arn: String,
321 }
322
323 #[test]
324 fn a_gate_authorizes_an_api_client() {
325 let gate = Gate::<ApiClient>::new().define_simple("posts.read", |client: &ApiClient| {
329 client.scopes.contains(&"posts:read")
330 });
331
332 let reader = ApiClient { id: "svc-reports".into(), scopes: vec!["posts:read"] };
333 let writer = ApiClient { id: "svc-import".into(), scopes: vec!["posts:write"] };
334
335 assert!(gate.allows_any("posts.read", &reader));
336 assert!(gate.denies::<()>("posts.read", &writer, None));
337 assert_eq!(reader.id, "svc-reports");
338 }
339
340 #[test]
341 fn a_gate_authorizes_a_cloud_principal() {
342 let gate = Gate::<StsPrincipal>::new().define_simple("admin.enter", |principal| {
343 principal.arn.starts_with("arn:aws:sts::123456789012:assumed-role/Deployer/")
344 });
345
346 let deployer =
347 StsPrincipal { arn: "arn:aws:sts::123456789012:assumed-role/Deployer/ci".into() };
348 let stranger =
349 StsPrincipal { arn: "arn:aws:sts::999999999999:assumed-role/Other/x".into() };
350
351 assert!(gate.allows_any("admin.enter", &deployer));
352 assert!(!gate.allows_any("admin.enter", &stranger));
353 }
354
355 #[test]
356 fn an_undefined_ability_is_denied_for_a_machine_too() {
357 let gate = Gate::<ApiClient>::new();
360 let client = ApiClient { id: "svc".into(), scopes: vec!["everything"] };
361
362 assert!(!gate.allows_any("posts.read", &client));
363 }
364}