diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 4da189e746..af092d3eac 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -33,6 +33,8 @@ pub mod loot_owner; #[cfg(not(target_arch = "wasm32"))] pub mod melee; #[cfg(not(target_arch = "wasm32"))] mod misc; #[cfg(not(target_arch = "wasm32"))] pub mod ori; +#[cfg(not(target_arch = "wasm32"))] +pub mod permissions; #[cfg(not(target_arch = "wasm32"))] pub mod pet; #[cfg(not(target_arch = "wasm32"))] mod phys; #[cfg(not(target_arch = "wasm32"))] mod player; @@ -81,6 +83,7 @@ pub use self::{ fluid_dynamics::Fluid, group::Group, inputs::CanBuild, + permissions::RuleSet, inventory::{ item::{ self, diff --git a/common/src/comp/permissions.rs b/common/src/comp/permissions.rs new file mode 100644 index 0000000000..0364ecc25d --- /dev/null +++ b/common/src/comp/permissions.rs @@ -0,0 +1,150 @@ +use bitflags::bitflags; +use num_traits::FromPrimitive; +use specs::{hibitset::DrainableBitSet, BitSet, Component}; + +#[derive(Clone, Copy, PartialEq, num_derive::FromPrimitive)] +#[repr(u8)] +pub enum Action { + Read = 1, + Write = 2, +} + +bitflags! { + struct Actions: u8 { + const READ = Action::Read as u8; + const WRITE = Action::Write as u8; + const READ_WRITE = Self::READ.bits | Self::WRITE.bits; + } +} + +const OBJ_MULTIPLIER: u32 = 16; + +#[derive(Clone, Copy, PartialEq, num_derive::FromPrimitive)] +#[repr(u16)] +pub enum Object { + ChatGlobal = 0x0001, + ChatWorld = 0x0002, + ChatRegion = 0x0003, + ChatLocal = 0x0004, + CommandTeleport = 0x0005, + CommandKick = 0x0006, + CommandBan = 0x0007, + CommandMute = 0x0008, + BuildCreateBlock = 0x0009, + BuildCreateSprite = 0x000A, + BuildDestroy = 0x000B, +} + +impl Actions { + pub const fn allows(&self, action: Action) -> bool { + match action { + Action::Read => self.contains(Actions::READ), + Action::Write => self.contains(Actions::WRITE), + } + } +} +impl Object { + const fn get_valid_actions(&self) -> Actions { + match self { + Self::ChatGlobal => Actions::READ_WRITE, + Self::ChatWorld => Actions::READ_WRITE, + Self::ChatRegion => Actions::READ_WRITE, + Self::ChatLocal => Actions::READ_WRITE, + Self::CommandTeleport => Actions::WRITE, + Self::CommandKick => Actions::WRITE, + Self::CommandBan => Actions::WRITE, + Self::CommandMute => Actions::WRITE, + Self::BuildCreateBlock => Actions::WRITE, + Self::BuildCreateSprite => Actions::WRITE, + Self::BuildDestroy => Actions::WRITE, + } + } +} + +pub struct ObjectAction { + obj: Object, + act: Action, +} + +impl ObjectAction { + pub fn new(obj: Object, act: Action) -> Self { Self { obj, act } } + + const fn id(&self) -> u32 { self.obj as u32 * OBJ_MULTIPLIER + self.act as u32 } + + pub fn from_id(id: u32) -> Option { + let act = FromPrimitive::from_u32(id.rem_euclid(OBJ_MULTIPLIER)); + let obj = FromPrimitive::from_u32(id.div_euclid(OBJ_MULTIPLIER)); + if let (Some(act), Some(obj)) = (act, obj) { + Some(Self::new(obj, act)) + } else { + None + } + } + + const fn valid(&self) -> bool { self.obj.get_valid_actions().allows(self.act) } +} + +#[derive(Clone, Debug, Default)] +pub struct RuleSet { + allowed: BitSet, +} + +impl Component for RuleSet { + type Storage = specs::VecStorage; +} + +impl RuleSet { + pub fn is_allowed(&self, object_action: ObjectAction) -> bool { + self.allowed.contains(object_action.id()) + } + + pub fn add(&mut self, object_action: ObjectAction) { + if object_action.valid() { + self.allowed.add(object_action.id()); + } + } + + pub fn remove(&mut self, object_action: ObjectAction) { + self.allowed.remove(object_action.id()); + } + + pub fn append(&mut self, mut other: RuleSet) { + for o in other.allowed.drain() { + self.allowed.add(o); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_max_action() { + assert!(OBJ_MULTIPLIER >= Actions::all().bits as u32 + 1); + } + + #[test] + fn ser_and_deser() { + let oa = ObjectAction::new(Object::ChatGlobal, Action::Write); + let i = oa.id(); + let oa2 = ObjectAction::from_id(i).unwrap(); + assert_eq!(oa.id(), oa2.id()); + } + + #[test] + fn check_permissions() { + let mut ruleset = RuleSet::default(); + ruleset.add(ObjectAction::new(Object::ChatGlobal, Action::Read)); + ruleset.add(ObjectAction::new(Object::ChatRegion, Action::Read)); + ruleset.add(ObjectAction::new(Object::ChatGlobal, Action::Write)); + let oa = ObjectAction::new(Object::ChatGlobal, Action::Write); + let oa2 = ObjectAction::new(Object::CommandBan, Action::Write); + let oa3 = ObjectAction::new(Object::ChatGlobal, Action::Read); + let oa4 = ObjectAction::new(Object::ChatRegion, Action::Write); + assert!(ruleset.is_allowed(oa)); + assert!(!ruleset.is_allowed(oa2)); + assert!(ruleset.is_allowed(oa3)); + assert!(!ruleset.is_allowed(oa4)); + } +} diff --git a/common/state/src/state.rs b/common/state/src/state.rs index d088bc84d3..79b1bcaf59 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -203,6 +203,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); diff --git a/server/src/lib.rs b/server/src/lib.rs index bcc50f13e0..29a26e8106 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -68,6 +68,7 @@ use crate::{ rtsim::RtSim, state_ext::StateExt, sys::sentinel::{DeletedEntities, TrackedStorages}, + sys::permissions::UserRoles, }; use censor::Censor; #[cfg(not(feature = "worldgen"))] @@ -343,6 +344,7 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); + state.ecs_mut().register::(); // Load banned words list let banned_words = settings.moderation.load_banned_words(data_dir); diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index 59e2b446f6..bc27ade73c 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -7,6 +7,7 @@ pub mod loot; pub mod metrics; pub mod msg; pub mod object; +pub mod permissions; pub mod persistence; pub mod pets; pub mod sentinel; @@ -40,6 +41,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch::(dispatch_builder, &[]); // don't depend on chunk_serialize, as we assume everything is done in a SlowJow dispatch::(dispatch_builder, &[]); + dispatch::(dispatch_builder, &[]); } pub fn run_sync_systems(ecs: &mut specs::World) { diff --git a/server/src/sys/permissions.rs b/server/src/sys/permissions.rs new file mode 100644 index 0000000000..6467d22d81 --- /dev/null +++ b/server/src/sys/permissions.rs @@ -0,0 +1,58 @@ +use chrono::Utc; +use common::comp::permissions::RuleSet; +use common_ecs::{Job, Origin, Phase, System}; +use specs::{Component, Entities, Join, ReadStorage, WriteStorage}; +use std::sync::Arc; + +pub struct Role { + pub name: String, + rules: RuleSet, +} + +pub struct RoleAccess { + role: Arc, + valid_until: chrono::DateTime, +} + +pub struct UserRoles(Vec); + +impl Component for UserRoles { + type Storage = specs::VecStorage; +} +// This system manages loot that exists in the world +#[derive(Default)] +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + Entities<'a>, + WriteStorage<'a, RuleSet>, + ReadStorage<'a, UserRoles>, + ); + + const NAME: &'static str = "permissions"; + const ORIGIN: Origin = Origin::Server; + const PHASE: Phase = Phase::Create; + + fn run(_job: &mut Job, (entities, mut rule_sets, user_roles): Self::SystemData) { + // recalculate the role_access on top of all individual roles assigned + let now = Utc::now(); + + // Add PreviousPhysCache for all relevant entities + for entity in (&entities, !&rule_sets) + .join() + .map(|(e, _)| e) + .collect::>() + { + let _ = rule_sets.insert(entity, RuleSet::default()); + } + + for (rule_set, user_roles) in (&mut rule_sets, &user_roles).join() { + *rule_set = RuleSet::default(); + for access in &user_roles.0 { + if access.valid_until > now { + rule_set.append(access.role.rules.clone()) + } + } + } + } +}