domain: add Identity & Access entities (User, Role, Permission, Group)
This commit is contained in:
46
crates/domain/src/entities/group.rs
Normal file
46
crates/domain/src/entities/group.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use crate::errors::DomainError;
|
||||||
|
use crate::value_objects::SystemId;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Group {
|
||||||
|
pub group_id: SystemId,
|
||||||
|
pub name: String,
|
||||||
|
pub owner_user_id: SystemId,
|
||||||
|
pub members: HashSet<SystemId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Group {
|
||||||
|
pub fn new(name: impl Into<String>, owner_user_id: SystemId) -> Self {
|
||||||
|
let mut members = HashSet::new();
|
||||||
|
members.insert(owner_user_id);
|
||||||
|
Self {
|
||||||
|
group_id: SystemId::new(),
|
||||||
|
name: name.into(),
|
||||||
|
owner_user_id,
|
||||||
|
members,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||||
|
if self.members.contains(&user_id) {
|
||||||
|
return Err(DomainError::Conflict(format!("User {user_id} is already a member")));
|
||||||
|
}
|
||||||
|
self.members.insert(user_id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_member(&mut self, user_id: SystemId) -> Result<(), DomainError> {
|
||||||
|
if user_id == self.owner_user_id {
|
||||||
|
return Err(DomainError::Validation("Cannot remove the group owner".to_string()));
|
||||||
|
}
|
||||||
|
if !self.members.remove(&user_id) {
|
||||||
|
return Err(DomainError::NotFound(format!("User {user_id} is not a member")));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_member(&self, user_id: &SystemId) -> bool {
|
||||||
|
self.members.contains(user_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,9 @@
|
|||||||
|
pub mod permission;
|
||||||
|
pub mod role;
|
||||||
mod user;
|
mod user;
|
||||||
|
mod group;
|
||||||
|
|
||||||
|
pub use permission::{Permission, PermissionAction, ResourceType};
|
||||||
|
pub use role::Role;
|
||||||
pub use user::User;
|
pub use user::User;
|
||||||
|
pub use group::Group;
|
||||||
|
|||||||
60
crates/domain/src/entities/permission.rs
Normal file
60
crates/domain/src/entities/permission.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum PermissionAction {
|
||||||
|
ReadAsset,
|
||||||
|
ReadMetadata,
|
||||||
|
ReadLocation,
|
||||||
|
ReadPerson,
|
||||||
|
WriteMetadata,
|
||||||
|
DeleteAsset,
|
||||||
|
ManageAccess,
|
||||||
|
ManageUsers,
|
||||||
|
ManageSystem,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum ResourceType {
|
||||||
|
Asset,
|
||||||
|
Album,
|
||||||
|
Collection,
|
||||||
|
Person,
|
||||||
|
Directory,
|
||||||
|
Global,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Permission {
|
||||||
|
pub action: PermissionAction,
|
||||||
|
pub resource_type: ResourceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Permission {
|
||||||
|
pub fn new(action: PermissionAction, resource_type: ResourceType) -> Self {
|
||||||
|
Self { action, resource_type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn viewer_permissions() -> HashSet<Permission> {
|
||||||
|
HashSet::from([
|
||||||
|
Permission::new(PermissionAction::ReadAsset, ResourceType::Global),
|
||||||
|
Permission::new(PermissionAction::ReadMetadata, ResourceType::Global),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contributor_permissions() -> HashSet<Permission> {
|
||||||
|
let mut perms = viewer_permissions();
|
||||||
|
perms.insert(Permission::new(PermissionAction::WriteMetadata, ResourceType::Global));
|
||||||
|
perms
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn admin_permissions() -> HashSet<Permission> {
|
||||||
|
let mut perms = contributor_permissions();
|
||||||
|
perms.insert(Permission::new(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||||
|
perms.insert(Permission::new(PermissionAction::ManageAccess, ResourceType::Global));
|
||||||
|
perms.insert(Permission::new(PermissionAction::ManageUsers, ResourceType::Global));
|
||||||
|
perms.insert(Permission::new(PermissionAction::ManageSystem, ResourceType::Global));
|
||||||
|
perms.insert(Permission::new(PermissionAction::ReadLocation, ResourceType::Global));
|
||||||
|
perms.insert(Permission::new(PermissionAction::ReadPerson, ResourceType::Global));
|
||||||
|
perms
|
||||||
|
}
|
||||||
26
crates/domain/src/entities/role.rs
Normal file
26
crates/domain/src/entities/role.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use crate::value_objects::SystemId;
|
||||||
|
use super::permission::{Permission, PermissionAction, ResourceType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Role {
|
||||||
|
pub role_id: SystemId,
|
||||||
|
pub name: String,
|
||||||
|
pub permissions: HashSet<Permission>,
|
||||||
|
pub is_system_default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Role {
|
||||||
|
pub fn new(name: impl Into<String>, permissions: HashSet<Permission>, is_system_default: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
role_id: SystemId::new(),
|
||||||
|
name: name.into(),
|
||||||
|
permissions,
|
||||||
|
is_system_default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_permission(&self, action: PermissionAction, resource_type: ResourceType) -> bool {
|
||||||
|
self.permissions.contains(&Permission::new(action, resource_type))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,20 @@ use crate::value_objects::{Email, PasswordHash, SystemId};
|
|||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: SystemId,
|
pub id: SystemId,
|
||||||
|
pub username: String,
|
||||||
pub email: Email,
|
pub email: Email,
|
||||||
pub password_hash: PasswordHash,
|
pub password_hash: PasswordHash,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl User {
|
impl User {
|
||||||
pub fn new(id: SystemId, email: Email, password_hash: PasswordHash) -> Self {
|
pub fn new(username: impl Into<String>, email: Email, password_hash: PasswordHash) -> Self {
|
||||||
Self { id, email, password_hash, created_at: Utc::now() }
|
Self {
|
||||||
|
id: SystemId::new(),
|
||||||
|
username: username.into(),
|
||||||
|
email,
|
||||||
|
password_hash,
|
||||||
|
created_at: Utc::now(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
mod entities;
|
||||||
mod events;
|
mod events;
|
||||||
|
mod services;
|
||||||
mod value_objects;
|
mod value_objects;
|
||||||
|
|||||||
41
crates/domain/tests/entities/group.rs
Normal file
41
crates/domain/tests/entities/group.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use domain::entities::Group;
|
||||||
|
use domain::errors::DomainError;
|
||||||
|
use domain::value_objects::SystemId;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn owner_auto_member() {
|
||||||
|
let owner = SystemId::new();
|
||||||
|
let g = Group::new("team", owner);
|
||||||
|
assert!(g.is_member(&owner));
|
||||||
|
assert_eq!(g.members.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_and_remove() {
|
||||||
|
let owner = SystemId::new();
|
||||||
|
let member = SystemId::new();
|
||||||
|
let mut g = Group::new("team", owner);
|
||||||
|
g.add_member(member).unwrap();
|
||||||
|
assert!(g.is_member(&member));
|
||||||
|
assert_eq!(g.members.len(), 2);
|
||||||
|
g.remove_member(member).unwrap();
|
||||||
|
assert!(!g.is_member(&member));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_remove_owner() {
|
||||||
|
let owner = SystemId::new();
|
||||||
|
let mut g = Group::new("team", owner);
|
||||||
|
let result = g.remove_member(owner);
|
||||||
|
assert!(matches!(result, Err(DomainError::Validation(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_add_duplicate() {
|
||||||
|
let owner = SystemId::new();
|
||||||
|
let member = SystemId::new();
|
||||||
|
let mut g = Group::new("team", owner);
|
||||||
|
g.add_member(member).unwrap();
|
||||||
|
let result = g.add_member(member);
|
||||||
|
assert!(matches!(result, Err(DomainError::Conflict(_))));
|
||||||
|
}
|
||||||
4
crates/domain/tests/entities/mod.rs
Normal file
4
crates/domain/tests/entities/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
mod group;
|
||||||
|
mod permission;
|
||||||
|
mod role;
|
||||||
|
mod user;
|
||||||
19
crates/domain/tests/entities/permission.rs
Normal file
19
crates/domain/tests/entities/permission.rs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
use domain::entities::permission::{
|
||||||
|
admin_permissions, contributor_permissions, viewer_permissions,
|
||||||
|
Permission, PermissionAction, ResourceType,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn admin_is_superset_of_contributor() {
|
||||||
|
let admin = admin_permissions();
|
||||||
|
let contrib = contributor_permissions();
|
||||||
|
assert!(contrib.is_subset(&admin));
|
||||||
|
assert!(admin.len() > contrib.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn viewer_cannot_write() {
|
||||||
|
let viewer = viewer_permissions();
|
||||||
|
let write = Permission::new(PermissionAction::WriteMetadata, ResourceType::Global);
|
||||||
|
assert!(!viewer.contains(&write));
|
||||||
|
}
|
||||||
9
crates/domain/tests/entities/role.rs
Normal file
9
crates/domain/tests/entities/role.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
use domain::entities::{Role, PermissionAction, ResourceType};
|
||||||
|
use domain::entities::permission::viewer_permissions;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn role_checks_permission() {
|
||||||
|
let role = Role::new("viewer", viewer_permissions(), true);
|
||||||
|
assert!(role.has_permission(PermissionAction::ReadAsset, ResourceType::Global));
|
||||||
|
assert!(!role.has_permission(PermissionAction::DeleteAsset, ResourceType::Global));
|
||||||
|
}
|
||||||
11
crates/domain/tests/entities/user.rs
Normal file
11
crates/domain/tests/entities/user.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use domain::entities::User;
|
||||||
|
use domain::value_objects::{Email, PasswordHash};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn creates_user_with_unique_id() {
|
||||||
|
let a = User::new("alice", Email::new("a@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||||
|
let b = User::new("bob", Email::new("b@example.com").unwrap(), PasswordHash::from_hash("h".into()));
|
||||||
|
assert_ne!(a.id, b.id);
|
||||||
|
assert_eq!(a.username, "alice");
|
||||||
|
assert_eq!(b.username, "bob");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user