session and seat management daemon
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(protocol): expose privileged operation capabilities

+257 -17
+141 -2
ctl/src/main.rs
··· 28 28 leader: u32, 29 29 timestamp: u32, 30 30 active: session_core_v1::ActiveState, 31 + can_modify: bool, 31 32 done: bool, 32 33 } 33 34 ··· 37 38 username: String, 38 39 lingering: bool, 39 40 state: session_core_v1::UserState, 41 + can_modify: bool, 40 42 not_found: bool, 41 43 done: bool, 42 44 } ··· 47 49 mode: session_core_v1::SeatMode, 48 50 current_vt: u32, 49 51 clients: Vec<u32>, 52 + can_modify: bool, 50 53 destroyed: bool, 51 54 done: bool, 52 55 } ··· 85 88 seat_clients: Vec<SeatClient>, 86 89 seat_devices: Vec<SeatDevice>, 87 90 done: bool, 91 + can_control_power: bool, 88 92 error: Option<String>, 89 93 } 90 94 ··· 101 105 session_core_manager::Event::Done => { 102 106 self.done = true; 103 107 } 108 + session_core_manager::Event::CanControlPower { can_control_power } => { 109 + self.can_control_power = can_control_power != 0; 110 + } 104 111 session_core_manager::Event::Error { code: _, message } => { 105 112 self.error = Some(message); 106 113 self.done = true; ··· 127 134 mode: session_core_v1::SeatMode::NonVtBound, 128 135 current_vt: 0, 129 136 clients: Vec::new(), 137 + can_modify: false, 130 138 destroyed: false, 131 139 done: false, 132 140 }); ··· 222 230 username: String::new(), 223 231 lingering: false, 224 232 state: session_core_v1::UserState::Offline, 233 + can_modify: false, 225 234 not_found: false, 226 235 done: false, 227 236 }); ··· 247 256 timestamp: 0, 248 257 done: false, 249 258 active: session_core_v1::ActiveState::Online, 259 + can_modify: false, 250 260 }); 251 261 } 252 262 } ··· 283 293 session_core_session::Event::Active { state } => { 284 294 session.active = state; 285 295 } 296 + session_core_session::Event::CanModify { can_modify } => { 297 + session.can_modify = can_modify != 0; 298 + } 286 299 _ => {} 287 300 } 288 301 } ··· 303 316 session_core_user::Event::Username { username } => user.username = username, 304 317 session_core_user::Event::Lingering { lingering } => user.lingering = lingering != 0, 305 318 session_core_user::Event::State { state } => user.state = state, 319 + session_core_user::Event::CanModify { can_modify } => { 320 + user.can_modify = can_modify != 0; 321 + } 306 322 session_core_user::Event::Done | session_core_user::Event::Ok => { 307 323 user.done = true; 308 324 } ··· 332 348 session_core_seat::Event::Seat { seat } => self.seats[idx].name = seat, 333 349 session_core_seat::Event::Mode { mode } => self.seats[idx].mode = mode, 334 350 session_core_seat::Event::CurrentVt { vt } => self.seats[idx].current_vt = vt, 351 + session_core_seat::Event::CanModify { can_modify } => { 352 + self.seats[idx].can_modify = can_modify != 0; 353 + } 335 354 session_core_seat::Event::ClientAdded { pid } => { 336 355 if !self.seats[idx].clients.contains(&pid) { 337 356 self.seats[idx].clients.push(pid); ··· 650 669 Self::Unlock => object.send_unlock(), 651 670 } 652 671 } 672 + 673 + const fn name(self) -> &'static str { 674 + match self { 675 + Self::Close => "close", 676 + Self::Kill => "kill", 677 + Self::Lock => "lock", 678 + Self::Unlock => "unlock", 679 + } 680 + } 653 681 } 654 682 655 683 #[derive(Clone, Copy)] ··· 663 691 match self { 664 692 Self::Kill => object.send_kill(), 665 693 Self::Terminate => object.send_terminate(), 694 + } 695 + } 696 + 697 + const fn name(self) -> &'static str { 698 + match self { 699 + Self::Kill => "kill", 700 + Self::Terminate => "terminate", 666 701 } 667 702 } 668 703 } ··· 739 774 seat_clients: Vec::new(), 740 775 seat_devices: Vec::new(), 741 776 done: false, 777 + can_control_power: false, 742 778 error: None, 743 779 }; 744 780 ··· 790 826 obj.insert("leader".into(), json!(session.leader)); 791 827 obj.insert("timestamp".into(), json!(session.timestamp)); 792 828 obj.insert("active".into(), json!(active_state_name(session.active))); 829 + obj.insert("can_modify".into(), json!(session.can_modify)); 793 830 serde_json::Value::Object(obj) 794 831 }).collect::<Vec<_>>(), 795 832 })); ··· 938 975 "error:".red(), 939 976 uid.as_raw() 940 977 ))?; 978 + if !session.can_modify { 979 + eprintln!( 980 + "{} cannot {} session {}: permission denied", 981 + "error:".red(), 982 + action.name(), 983 + session.id 984 + ); 985 + return Ok(()); 986 + } 941 987 action.send(&session.object); 942 988 943 989 return Ok(()); ··· 945 991 946 992 let mut unmatched = Vec::new(); 947 993 let mut matched = Vec::new(); 994 + let mut denied = Vec::new(); 948 995 949 996 for id in ids { 950 997 if let Some(session) = state.sessions.iter().find(|session| session.id == *id) { 951 - matched.push(session.object.clone()); 998 + if session.can_modify { 999 + matched.push(session.object.clone()); 1000 + } else { 1001 + denied.push(session.id); 1002 + } 952 1003 } else { 953 1004 unmatched.push(id); 954 1005 } ··· 964 1015 return Ok(()); 965 1016 } 966 1017 1018 + if !denied.is_empty() { 1019 + let ids = denied 1020 + .iter() 1021 + .map(std::string::ToString::to_string) 1022 + .collect::<Vec<_>>() 1023 + .join(", "); 1024 + eprintln!( 1025 + "{} cannot {} session ids without permission: {ids}", 1026 + "error:".red(), 1027 + action.name() 1028 + ); 1029 + return Ok(()); 1030 + } 1031 + 967 1032 for obj in matched { 968 1033 action.send(&obj); 969 1034 } ··· 1013 1078 "username": user.username, 1014 1079 "lingering": user.lingering, 1015 1080 "state": user_state_name(user.state), 1081 + "can_modify": user.can_modify, 1016 1082 }) 1017 1083 }).collect::<Vec<_>>(), 1018 1084 })); ··· 1094 1160 return Ok(()); 1095 1161 } 1096 1162 1163 + let denied = state 1164 + .users 1165 + .iter() 1166 + .filter(|user| !user.not_found && !user.can_modify) 1167 + .map(|user| user.uid) 1168 + .collect::<Vec<_>>(); 1169 + 1170 + if !denied.is_empty() { 1171 + let uids = denied 1172 + .iter() 1173 + .map(std::string::ToString::to_string) 1174 + .collect::<Vec<_>>() 1175 + .join(", "); 1176 + eprintln!( 1177 + "{} cannot {} user ids without permission: {uids}", 1178 + "error:".red(), 1179 + action.name() 1180 + ); 1181 + return Ok(()); 1182 + } 1183 + 1097 1184 for user in &mut state.users { 1098 1185 user.done = false; 1099 1186 action.send(&user.object); ··· 1144 1231 return Ok(()); 1145 1232 } 1146 1233 1234 + let denied = state 1235 + .users 1236 + .iter() 1237 + .filter(|user| !user.not_found && !user.can_modify) 1238 + .map(|user| user.uid) 1239 + .collect::<Vec<_>>(); 1240 + 1241 + if !denied.is_empty() { 1242 + let uids = denied 1243 + .iter() 1244 + .map(std::string::ToString::to_string) 1245 + .collect::<Vec<_>>() 1246 + .join(", "); 1247 + eprintln!( 1248 + "{} cannot {} user ids without permission: {uids}", 1249 + "error:".red(), 1250 + action.name() 1251 + ); 1252 + return Ok(()); 1253 + } 1254 + 1147 1255 for user in &mut state.users { 1148 1256 user.done = false; 1149 1257 action.send(&user.object); ··· 1198 1306 "mode": seat_mode_name(seat.mode), 1199 1307 "current_vt": seat.current_vt, 1200 1308 "clients": seat.clients.len(), 1309 + "can_modify": seat.can_modify, 1201 1310 }) 1202 1311 }).collect::<Vec<_>>(), 1203 1312 })); ··· 1278 1387 "name": seat.name, 1279 1388 "mode": seat_mode_name(seat.mode), 1280 1389 "current_vt": seat.current_vt, 1390 + "can_modify": seat.can_modify, 1281 1391 "clients": clients.iter().map(|client| { 1282 1392 let process_name = process_name(client.pid); 1283 1393 json!({ ··· 1404 1514 1405 1515 let mut unmatched = Vec::new(); 1406 1516 let mut matched = Vec::new(); 1517 + let mut denied = Vec::new(); 1407 1518 1408 1519 for seat_name in &seats { 1409 1520 if let Some(seat) = state ··· 1411 1522 .iter() 1412 1523 .find(|seat| seat.name == *seat_name && !seat.destroyed) 1413 1524 { 1414 - matched.push(seat.object.clone()); 1525 + if seat.can_modify { 1526 + matched.push(seat.object.clone()); 1527 + } else { 1528 + denied.push(seat.name.clone()); 1529 + } 1415 1530 } else { 1416 1531 unmatched.push(seat_name); 1417 1532 } ··· 1427 1542 return Ok(()); 1428 1543 } 1429 1544 1545 + if !denied.is_empty() { 1546 + eprintln!( 1547 + "{} cannot terminate seats without permission: {}", 1548 + "error:".red(), 1549 + denied.join(", ") 1550 + ); 1551 + return Ok(()); 1552 + } 1553 + 1430 1554 for seat in &mut state.seats { 1431 1555 if matched.iter().any(|object| object == &seat.object) { 1432 1556 seat.done = false; ··· 1448 1572 } 1449 1573 Command::Completions { shell: _ } => unreachable!(), 1450 1574 cmd => { 1575 + let command_name = match cmd { 1576 + Command::Reboot => "reboot", 1577 + Command::Poweroff => "power off", 1578 + Command::Hibernate => "hibernate", 1579 + Command::Suspend => "suspend", 1580 + _ => unreachable!(), 1581 + }; 1582 + if !state.can_control_power { 1583 + eprintln!( 1584 + "{} cannot {command_name} while other users have active sessions", 1585 + "error:".red() 1586 + ); 1587 + return Ok(()); 1588 + } 1589 + 1451 1590 state.done = false; 1452 1591 state.error = None; 1453 1592
+40 -7
daemon/src/seat.rs
··· 48 48 world: &mut world::World, 49 49 seat_entity: entity::Entity, 50 50 object: session_core_seat::SessionCoreSeat, 51 + session_entities: &[entity::Entity], 52 + management_access: &crate::Access, 51 53 ) { 52 54 if let Some(name) = world.get::<ecs::SeatName>(seat_entity) { 53 55 object.send_seat(&name.0); ··· 65 67 } 66 68 } 67 69 } 70 + if let Some(client) = object.client() { 71 + object.send_can_modify(u32::from(can_modify( 72 + world, 73 + seat_entity, 74 + session_entities, 75 + client.creds(), 76 + management_access, 77 + ))); 78 + } 68 79 object.send_done(); 69 80 70 81 if let Some(mut objects) = world.get_mut::<ecs::SeatObjects>(seat_entity) { 71 82 objects.0.push(object); 72 83 } 84 + } 85 + 86 + fn can_modify( 87 + world: &world::World, 88 + seat_entity: entity::Entity, 89 + session_entities: &[entity::Entity], 90 + creds: &rustix::net::UCred, 91 + management_access: &crate::Access, 92 + ) -> bool { 93 + if management_access.allows(creds) { 94 + return true; 95 + } 96 + 97 + let Some(name) = world.get::<ecs::SeatName>(seat_entity) else { 98 + return false; 99 + }; 100 + 101 + session::entities_by_seat_name(world, session_entities, &name.0).all(|entity| { 102 + world 103 + .get::<ecs::Uid>(entity) 104 + .is_some_and(|uid| uid.0 == creds.uid.as_raw()) 105 + }) 73 106 } 74 107 75 108 pub fn notify_client_added( ··· 815 848 let Some(client) = object.client() else { 816 849 return; 817 850 }; 818 - if !self.management_access.allows(client.creds()) 819 - && session_entities.iter().any(|entity| { 820 - self.world 821 - .get::<ecs::Uid>(*entity) 822 - .is_none_or(|uid| uid.0 != client.creds().uid.as_raw()) 823 - }) 824 - { 851 + if !can_modify( 852 + &self.world, 853 + seat_entity, 854 + &self.session_entities, 855 + client.creds(), 856 + &self.management_access, 857 + ) { 825 858 object.send_error( 826 859 session_core_v1::ErrorCode::PermissionDenied, 827 860 "permission denied",
+44 -8
daemon/src/session.rs
··· 113 113 }) 114 114 } 115 115 116 + pub fn can_modify( 117 + world: &world::World, 118 + entity: entity::Entity, 119 + creds: &rustix::net::UCred, 120 + ) -> bool { 121 + let session_uid = world.get::<ecs::Uid>(entity).map_or(0, |uid| uid.0); 122 + creds.uid.is_root() || creds.uid.as_raw() == session_uid 123 + } 124 + 116 125 impl<C: crate::cgroup::CgroupManager + 'static> 117 126 hyprwire::Dispatch<session_core_session::SessionCoreSession> for crate::Sessiond<C> 118 127 { ··· 138 147 let Some(client) = object.client() else { 139 148 return; 140 149 }; 141 - let session_uid = self.world.get::<ecs::Uid>(entity).map_or(0, |uid| uid.0); 142 - if !client.creds().uid.is_root() && client.creds().uid.as_raw() != session_uid { 150 + if !can_modify(&self.world, entity, client.creds()) { 143 151 object.send_error( 144 152 session_core_v1::ErrorCode::PermissionDenied, 145 153 "permission denied", ··· 282 290 if let Some(active_state) = world.get::<ecs::ActiveState>(entity) { 283 291 object.send_active(active_state.0); 284 292 } 293 + if let Some(client) = object.client() { 294 + object.send_can_modify(u32::from(can_modify(world, entity, client.creds()))); 295 + } 285 296 if let Some(seat) = world.get::<ecs::SeatName>(entity) { 286 297 object.send_seat(&seat.0); 287 298 } ··· 365 376 .for_each(|session_id| { 366 377 object.send_session_added(session_id.as_raw()); 367 378 }); 379 + if let Some(client) = object.client() { 380 + object.send_can_control_power(u32::from(self.can_control_power(client.creds()))); 381 + } 368 382 object.send_done(); 369 383 self.info_managers.push(object); 370 384 } 371 385 } 372 386 387 + impl<C: cgroup::CgroupManager + 'static> crate::Sessiond<C> { 388 + // Allows to manage power commands (power off, reboot) only 389 + // if no other sessions are active or user has root permissions 390 + fn can_control_power(&self, creds: &rustix::net::UCred) -> bool { 391 + self.management_access.allows(creds) 392 + || entities(&self.world, &self.session_entities) 393 + .filter_map(|entity| self.world.get::<ecs::Uid>(entity)) 394 + .all(|session| session.0 == creds.uid.as_raw()) 395 + } 396 + } 397 + 373 398 impl<C: cgroup::CgroupManager + 'static> hyprwire::Dispatch<session_manager::SessionManager> 374 399 for crate::Sessiond<C> 375 400 { ··· 732 757 return; 733 758 }; 734 759 735 - user::add_object(&mut self.world, entity, session_core_user); 760 + user::add_object( 761 + &mut self.world, 762 + entity, 763 + session_core_user, 764 + &self.management_access, 765 + ); 736 766 } 737 767 session_core_manager::Event::Reboot => { 738 768 let Some(client) = object.client() else { ··· 745 775 .filter(|session| session.0 != client.creds().uid.as_raw()) 746 776 .count(); 747 777 748 - if foreign_session_count > 0 && !self.management_access.allows(client.creds()) { 778 + if !self.can_control_power(client.creds()) { 749 779 log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "reboot denied: multiple sessions active"); 750 780 object.send_error( 751 781 session_core_v1::ErrorCode::ActiveSessions, ··· 796 826 .filter(|session| session.0 != client.creds().uid.as_raw()) 797 827 .count(); 798 828 799 - if foreign_session_count > 0 && !self.management_access.allows(client.creds()) { 829 + if !self.can_control_power(client.creds()) { 800 830 log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "poweroff denied: multiple sessions active"); 801 831 object.send_error( 802 832 session_core_v1::ErrorCode::ActiveSessions, ··· 847 877 .filter(|session| session.0 != client.creds().uid.as_raw()) 848 878 .count(); 849 879 850 - if foreign_session_count > 0 && !self.management_access.allows(client.creds()) { 880 + if !self.can_control_power(client.creds()) { 851 881 log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "suspend denied: multiple sessions active"); 852 882 object.send_error( 853 883 session_core_v1::ErrorCode::ActiveSessions, ··· 882 912 .filter(|session| session.0 != client.creds().uid.as_raw()) 883 913 .count(); 884 914 885 - if foreign_session_count > 0 && !self.management_access.allows(client.creds()) { 915 + if !self.can_control_power(client.creds()) { 886 916 log::warn!(uid = client.creds().uid.as_raw(), pid = client.creds().pid.as_raw_pid(), foreign_session_count; "hibernate denied: multiple sessions active"); 887 917 object.send_error( 888 918 session_core_v1::ErrorCode::ActiveSessions, ··· 916 946 session_core_seat.send_destroyed(); 917 947 return; 918 948 }; 919 - seat::add_object(&mut self.world, seat_entity, session_core_seat); 949 + seat::add_object( 950 + &mut self.world, 951 + seat_entity, 952 + session_core_seat, 953 + &self.session_entities, 954 + &self.management_access, 955 + ); 920 956 } 921 957 session_core_manager::Event::Destroy => { 922 958 self.info_managers.retain(|m| m != object);
+7
daemon/src/user.rs
··· 139 139 world: &mut world::World, 140 140 entity: entity::Entity, 141 141 object: session_core_user::SessionCoreUser, 142 + management_access: &crate::Access, 142 143 ) { 143 144 if let Some(uid) = world.get::<ecs::Uid>(entity) { 144 145 object.send_uid(uid.0); ··· 151 152 } 152 153 if let Some(state) = world.get::<ecs::UserState>(entity) { 153 154 object.send_state(state.0); 155 + } 156 + if let Some(client) = object.client() { 157 + let uid = world.get::<ecs::Uid>(entity).map_or(0, |uid| uid.0); 158 + let can_modify = 159 + management_access.allows(client.creds()) || uid == client.creds().uid.as_raw(); 160 + object.send_can_modify(u32::from(can_modify)); 154 161 } 155 162 object.send_done(); 156 163
+25
protocols/session_core_v1.xml
··· 80 80 <c2s name="destroy" destructor="true"> 81 81 <description summary="destroy this object"/> 82 82 </c2s> 83 + <s2c name="can_control_power"> 84 + <description summary="client may run power operations"> 85 + Non-zero if this client can reboot, power off, suspend, or hibernate 86 + without first closing other users' sessions. 87 + </description> 88 + <arg name="can_control_power" type="uint" summary="non-zero if power operations are currently allowed"/> 89 + </s2c> 83 90 </object> 84 91 <object name="session_core_session" version="1"> 85 92 <description summary="per-session information"> ··· 155 162 <c2s name="destroy" destructor="true"> 156 163 <description summary="destroy this object"/> 157 164 </c2s> 165 + <s2c name="can_modify"> 166 + <description summary="client may modify this session"> 167 + Non-zero if this client may lock, unlock, terminate, or kill this session. 168 + </description> 169 + <arg name="can_modify" type="uint" summary="non-zero if mutating requests are allowed"/> 170 + </s2c> 158 171 </object> 159 172 160 173 <object name="session_core_seat" version="1"> ··· 212 225 <c2s name="destroy" destructor="true"> 213 226 <description summary="destroy this object"/> 214 227 </c2s> 228 + <s2c name="can_modify"> 229 + <description summary="client may modify this seat"> 230 + Non-zero if this client may terminate all sessions on this seat. 231 + </description> 232 + <arg name="can_modify" type="uint" summary="non-zero if mutating requests are allowed"/> 233 + </s2c> 215 234 </object> 216 235 217 236 <object name="session_core_seat_client" version="1"> ··· 355 374 <c2s name="destroy" destructor="true"> 356 375 <description summary="destroy this object"/> 357 376 </c2s> 377 + <s2c name="can_modify"> 378 + <description summary="client may modify this user"> 379 + Non-zero if this client may terminate or kill this user's sessions. 380 + </description> 381 + <arg name="can_modify" type="uint" summary="non-zero if mutating requests are allowed"/> 382 + </s2c> 358 383 </object> 359 384 360 385 <enum name="user_state">