use crate::{Permission, PermissionContext};
use async_trait::async_trait;
use std::collections::{HashMap, HashSet};
pub struct ObjectPermission {
permission: String,
object_id: Option<String>,
grants: HashMap<(String, String), HashSet<String>>,
}
impl ObjectPermission {
pub fn new(permission: impl Into<String>, object_id: Option<impl Into<String>>) -> Self {
Self {
permission: permission.into(),
object_id: object_id.map(|id| id.into()),
grants: HashMap::new(),
}
}
pub fn permission(&self) -> &str {
&self.permission
}
pub fn object_id(&self) -> Option<&str> {
self.object_id.as_deref()
}
pub fn grant(
&mut self,
username: impl Into<String>,
permission: impl Into<String>,
object_id: impl Into<String>,
) {
let key = (username.into(), object_id.into());
self.grants
.entry(key)
.or_default()
.insert(permission.into());
}
pub fn user_has_object_permission(
&self,
username: &str,
permission: &str,
object_id: &str,
) -> bool {
self.grants
.get(&(username.to_string(), object_id.to_string()))
.is_some_and(|perms| perms.contains(permission))
}
}
#[async_trait]
impl Permission for ObjectPermission {
async fn has_permission(&self, context: &PermissionContext<'_>) -> bool {
if !context.is_authenticated {
return false;
}
let user = match &context.user {
Some(u) => u,
None => return false,
};
let object_id = match &self.object_id {
Some(id) => id,
None => return true,
};
self.user_has_object_permission(user.username(), &self.permission, object_id)
}
}
pub struct RoleBasedPermission {
roles: HashMap<String, Vec<String>>,
user_roles: HashMap<String, String>,
required_permission: Option<String>,
}
impl RoleBasedPermission {
pub fn new() -> Self {
Self {
roles: HashMap::new(),
user_roles: HashMap::new(),
required_permission: None,
}
}
pub fn with_required_permission(permission: impl Into<String>) -> Self {
Self {
roles: HashMap::new(),
user_roles: HashMap::new(),
required_permission: Some(permission.into()),
}
}
pub fn add_role(&mut self, role: impl Into<String>, permissions: Vec<impl Into<String>>) {
let perms = permissions.into_iter().map(|p| p.into()).collect();
self.roles.insert(role.into(), perms);
}
pub fn assign_user_role(&mut self, username: impl Into<String>, role: impl Into<String>) {
self.user_roles.insert(username.into(), role.into());
}
pub fn user_has_permission(&self, username: &str, permission: &str) -> bool {
if let Some(role) = self.user_roles.get(username)
&& let Some(perms) = self.roles.get(role)
{
return perms.iter().any(|p| p == permission);
}
false
}
}
impl Default for RoleBasedPermission {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Permission for RoleBasedPermission {
async fn has_permission(&self, context: &PermissionContext<'_>) -> bool {
if !context.is_authenticated {
return false;
}
let user = match &context.user {
Some(u) => u,
None => return false,
};
match &self.required_permission {
Some(perm) => self.user_has_permission(user.username(), perm),
None => {
if let Some(role) = self.user_roles.get(user.username())
&& let Some(perms) = self.roles.get(role)
{
!perms.is_empty()
} else {
false
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SimpleUser;
use bytes::Bytes;
use hyper::Method;
use reinhardt_http::Request;
use rstest::rstest;
use uuid::Uuid;
fn make_user(username: &str) -> Box<dyn crate::User> {
Box::new(SimpleUser {
id: Uuid::now_v7(),
username: username.to_string(),
email: format!("{}@example.com", username),
is_active: true,
is_admin: false,
is_staff: false,
is_superuser: false,
})
}
#[rstest]
fn test_object_permission_creation() {
let perm = ObjectPermission::new("view", Some("article:123"));
assert_eq!(perm.permission(), "view");
assert_eq!(perm.object_id(), Some("article:123"));
}
#[rstest]
fn test_object_permission_without_object_id() {
let perm = ObjectPermission::new("create", None::<String>);
assert_eq!(perm.permission(), "create");
assert_eq!(perm.object_id(), None);
}
#[rstest]
#[tokio::test]
async fn test_object_permission_no_object_id_authenticated() {
let perm = ObjectPermission::new("view", None::<String>);
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_object_permission_denies_without_grant() {
let perm = ObjectPermission::new("edit", Some("post:42"));
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_object_permission_grants_with_matching_grant() {
let mut perm = ObjectPermission::new("edit", Some("post:42"));
perm.grant("alice", "edit", "post:42");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_object_permission_denies_wrong_user() {
let mut perm = ObjectPermission::new("edit", Some("post:42"));
perm.grant("alice", "edit", "post:42");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("bob")),
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_object_permission_unauthenticated() {
let perm = ObjectPermission::new("edit", Some("post:42"));
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: false,
is_admin: false,
is_active: false,
user: None,
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_object_permission_authenticated_no_user() {
let perm = ObjectPermission::new("edit", Some("post:42"));
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: None,
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
fn test_role_based_permission_creation() {
let perm = RoleBasedPermission::new();
assert!(!perm.user_has_permission("alice", "read"));
}
#[rstest]
fn test_role_based_permission_add_role() {
let mut perm = RoleBasedPermission::new();
perm.add_role("admin", vec!["read", "write", "delete"]);
perm.assign_user_role("alice", "admin");
assert!(perm.user_has_permission("alice", "read"));
assert!(perm.user_has_permission("alice", "write"));
assert!(perm.user_has_permission("alice", "delete"));
}
#[rstest]
fn test_role_based_permission_different_roles() {
let mut perm = RoleBasedPermission::new();
perm.add_role("admin", vec!["read", "write", "delete"]);
perm.add_role("viewer", vec!["read"]);
perm.assign_user_role("alice", "admin");
perm.assign_user_role("bob", "viewer");
assert!(perm.user_has_permission("alice", "write"));
assert!(perm.user_has_permission("bob", "read"));
assert!(!perm.user_has_permission("bob", "write"));
}
#[rstest]
fn test_role_based_permission_no_role() {
let perm = RoleBasedPermission::new();
assert!(!perm.user_has_permission("charlie", "read"));
}
#[rstest]
#[tokio::test]
async fn test_role_permission_trait_with_user_and_role() {
let mut perm = RoleBasedPermission::new();
perm.add_role("user", vec!["read"]);
perm.assign_user_role("alice", "user");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_role_permission_trait_denies_user_without_role() {
let mut perm = RoleBasedPermission::new();
perm.add_role("user", vec!["read"]);
perm.assign_user_role("alice", "user");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("bob")),
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_role_permission_trait_unauthenticated() {
let perm = RoleBasedPermission::new();
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: false,
is_admin: false,
is_active: false,
user: None,
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_role_permission_trait_no_user_in_context() {
let mut perm = RoleBasedPermission::new();
perm.add_role("user", vec!["read"]);
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: None,
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_role_permission_denies_empty_permission_list() {
let mut perm = RoleBasedPermission::new();
perm.add_role("viewer", Vec::<String>::new());
perm.assign_user_role("alice", "viewer");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(!perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_role_permission_with_required_permission_grants() {
let mut perm = RoleBasedPermission::with_required_permission("write");
perm.add_role("editor", vec!["read", "write"]);
perm.assign_user_role("alice", "editor");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(perm.has_permission(&context).await);
}
#[rstest]
#[tokio::test]
async fn test_role_permission_with_required_permission_denies() {
let mut perm = RoleBasedPermission::with_required_permission("delete");
perm.add_role("editor", vec!["read", "write"]);
perm.assign_user_role("alice", "editor");
let request = Request::builder()
.method(Method::GET)
.uri("/")
.body(Bytes::new())
.build()
.unwrap();
let context = PermissionContext {
request: &request,
is_authenticated: true,
is_admin: false,
is_active: true,
user: Some(make_user("alice")),
};
assert!(!perm.has_permission(&context).await);
}
}