feat: add user accounts page

Co-authored-by: Antoine C <hi@acolombier.dev>
This commit is contained in:
Michael Murphy 2024-12-11 14:46:36 +01:00 committed by GitHub
parent 00b8b2bb96
commit 8e5afbfbfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1179 additions and 147 deletions

View file

@ -5,6 +5,7 @@ edition = "2021"
license = "GPL-3.0-only"
[dependencies]
accounts-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
anyhow = "1.0"
as-result = "0.2.1"
ashpd = { version = "0.9", default-features = false, features = [
@ -57,7 +58,7 @@ sunrise = "1.0.1"
tachyonix = "0.3.1"
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
tokio = { workspace = true, features = ["fs", "io-util", "sync"] }
tracing = "0.1.40"
tracing = "0.1.41"
tracing-subscriber = "0.3.18"
udev = { version = "0.9.0", optional = true }
upower_dbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true }
@ -65,10 +66,12 @@ bluez-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings", optiona
url = "2.5.2"
xkb-data = "0.2.1"
zbus = { version = "4.4.0", features = ["tokio"], optional = true }
zbus_polkit = { version = "4.0.0" }
ustr = "1.0.0"
fontdb = "0.16.2"
fixed_decimal = "0.5.6"
mime = "0.3.17"
rustix = "0.38.41"
[dependencies.cosmic-settings-subscriptions]
git = "https://github.com/pop-os/cosmic-settings-subscriptions"
@ -104,6 +107,7 @@ linux = [
"page-power",
"page-region",
"page-sound",
"page-users",
"page-window-management",
"page-workspaces",
"xdg-portal",
@ -129,6 +133,7 @@ page-networking = [
page-power = ["dep:upower_dbus", "dep:zbus"]
page-region = ["dep:lichen-system", "dep:locale1"]
page-sound = ["dep:cosmic-settings-subscriptions"]
page-users = ["dep:accounts-zbus"]
page-window-management = ["dep:cosmic-settings-config"]
page-workspaces = ["dep:cosmic-comp-config"]

View file

@ -108,6 +108,7 @@ impl SettingsApp {
PageCommands::Time => self.pages.page_id::<time::Page>(),
#[cfg(feature = "page-input")]
PageCommands::Touchpad => self.pages.page_id::<input::touchpad::Page>(),
#[cfg(feature = "page-users")]
PageCommands::Users => self.pages.page_id::<system::users::Page>(),
#[cfg(feature = "page-networking")]
PageCommands::Vpn => self.pages.page_id::<networking::vpn::Page>(),
@ -504,6 +505,13 @@ impl cosmic::Application for SettingsApp {
}
}
#[cfg(feature = "page-users")]
crate::pages::Message::User(message) => {
if let Some(page) = self.pages.page_mut::<system::users::Page>() {
return page.update(message).map(Into::into);
}
}
#[cfg(feature = "page-input")]
crate::pages::Message::SystemShortcuts(message) => {
if let Some(page) = self

View file

@ -92,6 +92,7 @@ pub enum PageCommands {
#[cfg(feature = "page-input")]
Touchpad,
/// Users settings page
#[cfg(feature = "page-users")]
Users,
/// VPN settings page
#[cfg(feature = "page-networking")]

View file

@ -69,6 +69,8 @@ pub enum Message {
Region(time::region::Message),
#[cfg(feature = "page-sound")]
Sound(sound::Message),
#[cfg(feature = "page-users")]
User(system::users::Message),
#[cfg(feature = "page-input")]
SystemShortcuts(input::keyboard::shortcuts::ShortcutMessage),
#[cfg(feature = "page-input")]

View file

@ -100,6 +100,10 @@ impl page::Page<crate::pages::Message> for Page {
&mut self,
_sender: mpsc::Sender<crate::pages::Message>,
) -> Task<crate::pages::Message> {
if let Some(handle) = self.on_enter_handle.take() {
handle.abort();
}
let (task, on_enter_handle) = Task::future(async move {
let mut list = mime_apps::List::default();
list.load_from_paths(&mime_apps::list_paths());

View file

@ -6,6 +6,7 @@ pub mod about;
#[cfg(feature = "page-default-apps")]
pub mod default_apps;
pub mod firmware;
#[cfg(feature = "page-users")]
pub mod users;
use cosmic_settings_page as page;
@ -29,16 +30,23 @@ impl page::AutoBind<crate::pages::Message> for Page {
fn sub_pages(
mut page: page::Insert<crate::pages::Message>,
) -> page::Insert<crate::pages::Message> {
page = page.sub_page::<users::Page>();
#[cfg(feature = "page-users")]
{
page = page.sub_page::<users::Page>();
}
#[cfg(feature = "page-about")]
{
page = page.sub_page::<about::Page>();
}
page = page.sub_page::<firmware::Page>();
#[cfg(feature = "page-default-apps")]
{
page = page.sub_page::<default_apps::Page>();
}
page
}
}

View file

@ -1,32 +0,0 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use slotmap::SlotMap;
#[derive(Default)]
pub struct Page {
entity: page::Entity,
}
impl page::Page<crate::pages::Message> for Page {
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(Section::default())])
}
fn info(&self) -> page::Info {
page::Info::new("users", "system-users-symbolic")
.title(fl!("users"))
.description(fl!("users", "desc"))
}
}
impl page::AutoBind<crate::pages::Message> for Page {}

View file

@ -0,0 +1,137 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{BufRead, BufReader};
use std::process::Stdio;
use std::str::FromStr;
pub fn passwd(range: (u64, u64)) -> Vec<PasswdUser> {
let spawn_res = std::process::Command::new("getent")
.arg("passwd")
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn();
let mut users = Vec::new();
if let Ok(mut child) = spawn_res {
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
_ => (),
}
if let Ok(user) = line.trim().parse::<PasswdUser>() {
if user.uid >= range.0 && user.uid <= range.1 {
users.push(user);
}
}
}
}
users
}
pub fn group() -> Vec<Group> {
let spawn_res = std::process::Command::new("getent")
.arg("group")
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::piped())
.spawn();
let mut groups = Vec::new();
if let Ok(mut child) = spawn_res {
let stdout = child.stdout.take().unwrap();
let mut reader = BufReader::new(stdout);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
_ => (),
}
if let Ok(group) = line.trim().parse::<Group>() {
groups.push(group);
}
}
}
groups
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Group {
pub uid: u64,
pub name: Box<str>,
pub users: Vec<Box<str>>,
}
impl FromStr for Group {
type Err = ();
fn from_str(line: &str) -> Result<Self, Self::Err> {
let mut fields = line.split(':');
Ok(Group {
name: fields.next().ok_or(())?.into(),
uid: fields.nth(1).ok_or(())?.parse().map_err(|_| ())?,
users: fields
.next()
.ok_or(())?
.split(',')
.map(Box::from)
.collect::<Vec<_>>(),
})
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PasswdUser {
pub uid: u64,
pub username: Box<str>,
pub full_name: Box<str>,
}
impl FromStr for PasswdUser {
type Err = ();
fn from_str(line: &str) -> Result<Self, Self::Err> {
let mut fields = line.split(':');
Ok(PasswdUser {
username: fields.next().ok_or(())?.into(),
uid: fields.nth(1).ok_or(())?.parse().map_err(|_| ())?,
full_name: fields.nth(1).ok_or(())?.split(',').next().ok_or(())?.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::PasswdUser;
#[test]
fn passwd() {
const EXAMPLE: &str =
"speech-dispatcher:x:109:29:Speech Dispatcher,,,:/run/speech-dispatcher:/bin/false";
assert_eq!(
EXAMPLE.parse::<PasswdUser>(),
Ok(PasswdUser {
username: Box::from("speech-dispatcher"),
uid: 109,
full_name: Box::from("Speech Dispatcher")
})
);
}
}

View file

@ -0,0 +1,818 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
mod getent;
use cosmic::{
dialog::file_chooser,
iced::{Alignment, Length},
theme,
widget::{self, column, icon, settings, text},
Apply, Element,
};
use cosmic_settings_page::{self as page, section, Section};
use slab::Slab;
use slotmap::SlotMap;
use std::{
collections::HashMap,
future::Future,
io::{BufRead, BufReader},
path::PathBuf,
sync::Arc,
};
use url::Url;
use zbus_polkit::policykit1::CheckAuthorizationFlags;
use crate::pages;
const DEFAULT_ICON_FILE: &str = "/usr/share/pixmaps/faces/pop-robot.png";
const USERS_ADMIN_POLKIT_POLICY_ID: &str = "com.system76.CosmicSettings.Users.Admin";
#[derive(Clone, Debug, Default)]
pub struct User {
id: u64,
profile_icon: Option<icon::Handle>,
full_name: String,
password: String,
username: String,
full_name_edit: bool,
password_edit: bool,
username_edit: bool,
is_admin: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EditorField {
FullName,
Password,
Username,
}
#[derive(Clone, Debug)]
pub enum Dialog {
AddNewUser(User),
}
#[derive(Clone, Debug)]
pub struct Page {
on_enter_handle: Option<cosmic::iced::task::Handle>,
current_user_id: u64,
entity: page::Entity,
users: Vec<User>,
selected_user_idx: Option<usize>,
dialog: Option<Dialog>,
default_icon: icon::Handle,
password_label: String,
username_label: String,
fullname_label: String,
}
impl Default for Page {
fn default() -> Self {
Self {
on_enter_handle: None,
current_user_id: 0,
entity: page::Entity::default(),
users: Vec::default(),
selected_user_idx: None,
dialog: None,
default_icon: icon::from_path(PathBuf::from(DEFAULT_ICON_FILE)),
password_label: crate::fl!("password"),
username_label: crate::fl!("username"),
fullname_label: crate::fl!("full-name"),
}
}
}
#[derive(Clone, Debug)]
pub enum Message {
ApplyEdit(usize, EditorField),
ChangedAccountType(u64, bool),
DeletedUser(u64),
Dialog(Option<Dialog>),
Edit(usize, EditorField, String),
LoadedIcon(u64, icon::Handle),
LoadPage(u64, Vec<User>),
NewUser(String, String, String, bool),
None,
SelectProfileImage(u64),
SelectedProfileImage(u64, Arc<Result<Url, file_chooser::Error>>),
SelectUser(usize),
SelectedUserDelete(u64),
SelectedUserSetAdmin(u64, bool),
ToggleEdit(usize, EditorField),
}
impl From<Message> for crate::app::Message {
fn from(message: Message) -> Self {
crate::pages::Message::User(message).into()
}
}
impl From<Message> for crate::pages::Message {
fn from(message: Message) -> Self {
crate::pages::Message::User(message)
}
}
impl page::Page<crate::pages::Message> for Page {
fn set_id(&mut self, entity: page::Entity) {
self.entity = entity;
}
fn content(
&self,
sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
) -> Option<page::Content> {
Some(vec![sections.insert(user_list())])
}
fn info(&self) -> page::Info {
page::Info::new("users", "system-users-symbolic")
.title(fl!("users"))
.description(fl!("users", "desc"))
}
fn dialog(&self) -> Option<Element<pages::Message>> {
let dialog = self.dialog.as_ref()?;
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let dialog_element = match dialog {
Dialog::AddNewUser(user) => {
let full_name_input = widget::container(
widget::text_input("", &user.full_name)
.label(&self.fullname_label)
.on_input(|value| {
Message::Dialog(Some(Dialog::AddNewUser(User {
full_name: value,
..user.clone()
})))
}),
)
.padding([0, theme.space_s().into()]);
let username_input = widget::container(
widget::text_input("", &user.username)
.label(&self.username_label)
.on_input(|value| {
Message::Dialog(Some(Dialog::AddNewUser(User {
username: value,
..user.clone()
})))
}),
)
.padding([0, theme.space_s().into()]);
let password_input = widget::container(
widget::text_input("", &user.password)
.password()
.label(&self.password_label)
.on_input(|value| {
Message::Dialog(Some(Dialog::AddNewUser(User {
password: value,
..user.clone()
})))
}),
)
.padding([0, theme.space_s().into()]);
let admin_toggler = widget::toggler(user.is_admin).on_toggle(|value| {
Message::Dialog(Some(Dialog::AddNewUser(User {
is_admin: value,
..user.clone()
})))
});
let add_user_button = widget::button::suggested(fl!("add-user"))
.on_press(())
.apply(Element::from)
.map(|_| {
Message::NewUser(
user.username.clone(),
user.full_name.clone(),
user.password.clone(),
user.is_admin,
)
});
let cancel_button =
widget::button::standard(fl!("cancel")).on_press(Message::Dialog(None));
widget::dialog()
.title(fl!("add-user"))
.control(
widget::ListColumn::default()
.add(full_name_input)
.add(username_input)
.add(password_input)
.add(settings::item_row(vec![
column::with_capacity(2)
.push(text::body(crate::fl!("administrator")))
.push(text::caption(crate::fl!("administrator", "desc")))
.into(),
widget::horizontal_space().width(Length::Fill).into(),
admin_toggler.into(),
])),
)
.primary_action(add_user_button)
.secondary_action(cancel_button)
.apply(cosmic::Element::from)
}
};
dialog_element.map(crate::pages::Message::User).into()
}
fn on_enter(
&mut self,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> cosmic::Task<crate::pages::Message> {
if let Some(handle) = self.on_enter_handle.take() {
handle.abort();
}
let (task, handle) = cosmic::task::future(async { Self::reload().await }).abortable();
self.on_enter_handle = Some(handle);
task
}
fn on_leave(&mut self) -> cosmic::Task<crate::pages::Message> {
if let Some(handle) = self.on_enter_handle.take() {
handle.abort();
}
cosmic::Task::none()
}
}
impl Page {
pub async fn reload() -> Message {
let passwd_users = getent::passwd(uid_range());
let mut users = Vec::with_capacity(passwd_users.len());
let groups = getent::group();
let uid = rustix::process::getuid().as_raw() as u64;
let admin_group = groups.iter().find(|g| &*g.name == "sudo");
let Ok(conn) = zbus::Connection::system().await else {
tracing::error!("unable to access dbus system service");
return Message::LoadPage(uid, Vec::new());
};
for user in passwd_users {
let Ok(user_proxy) = accounts_zbus::UserProxy::from_uid(&conn, user.uid).await else {
continue;
};
users.push(User {
id: user.uid,
profile_icon: user_proxy
.icon_file()
.await
.ok()
.map(|path| icon::from_path(PathBuf::from(path))),
is_admin: match user_proxy.account_type().await {
Ok(1) => true,
Ok(_) => false,
Err(_) => {
admin_group.map_or(false, |group| group.users.contains(&user.username))
}
},
username: String::from(user.username),
full_name: String::from(user.full_name),
password: String::new(),
full_name_edit: false,
password_edit: false,
username_edit: false,
});
}
Message::LoadPage(uid, users)
}
pub fn update(&mut self, message: Message) -> cosmic::Task<crate::app::Message> {
match message {
Message::None => (),
Message::ChangedAccountType(uid, is_admin) => {
for user in &mut self.users {
if user.id == uid {
user.is_admin = is_admin;
return cosmic::Task::none();
}
}
}
Message::LoadedIcon(uid, handle) => {
for user in &mut self.users {
if user.id == uid {
user.profile_icon = Some(handle);
return cosmic::Task::none();
}
}
}
Message::SelectProfileImage(uid) => {
return cosmic::task::future(async move {
let dialog_result = file_chooser::open::Dialog::new()
.title(fl!("wallpaper", "folder-dialog"))
.accept_label(fl!("dialog-add"))
.modal(false)
.open_file()
.await
.map(|response| response.url().to_owned());
Message::SelectedProfileImage(uid, Arc::new(dialog_result))
});
}
Message::SelectedProfileImage(uid, image_result) => {
let url = match Arc::into_inner(image_result).unwrap() {
Ok(url) => url,
Err(why) => {
tracing::error!(?why, "failed to get image file");
return cosmic::Task::none();
}
};
return cosmic::task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return Message::None;
};
let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await else {
return Message::None;
};
let Ok(path) = url.to_file_path() else {
tracing::error!("selected image is not a file path");
return Message::None;
};
let result = request_permission_on_denial(&conn, || {
user.set_icon_file(path.to_str().unwrap())
})
.await;
if let Err(why) = result {
tracing::error!(?why, "failed to set profile icon");
return Message::None;
}
Message::LoadedIcon(uid, icon::from_path(path))
});
}
Message::Edit(id, field, value) => {
if let Some(user) = self.users.get_mut(id) {
match field {
EditorField::FullName => user.full_name = value,
EditorField::Password => user.password = value,
EditorField::Username => user.username = value,
}
}
}
Message::ToggleEdit(id, field) => {
if let Some(user) = self.users.get_mut(id) {
match field {
EditorField::FullName => user.full_name_edit = !user.full_name_edit,
EditorField::Password => user.password_edit = !user.password_edit,
EditorField::Username => user.username_edit = !user.username_edit,
}
}
}
Message::ApplyEdit(id, field) => {
if let Some(user) = self.users.get_mut(id) {
let uid = user.id;
match field {
EditorField::FullName => {
let full_name = user.full_name.clone();
return cosmic::Task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return;
};
let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await
else {
return;
};
_ = request_permission_on_denial(&conn, || {
user.set_real_name(&full_name)
})
.await;
})
.discard();
}
EditorField::Password => {
let password = std::mem::take(&mut user.password);
return cosmic::Task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return;
};
let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await
else {
return;
};
match request_permission_on_denial(&conn, || {
user.set_password(&password, "")
})
.await
{
Err(why) => {
tracing::error!(?why, "failed to set password");
}
Ok(_) => (),
}
})
.discard();
}
EditorField::Username => {
let username = user.username.clone();
return cosmic::Task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return;
};
let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await
else {
return;
};
_ = request_permission_on_denial(&conn, || {
user.set_user_name(&username)
})
.await;
})
.discard();
}
}
}
}
Message::LoadPage(uid, users) => {
self.current_user_id = uid;
self.users = users;
}
Message::SelectUser(user_idx) => {
match self.selected_user_idx {
Some(currently_selected_idx) if currently_selected_idx == user_idx => {
self.selected_user_idx = None;
}
_ => {
self.selected_user_idx = Some(user_idx);
}
};
}
Message::SelectedUserDelete(uid) => {
return cosmic::task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return Message::None;
};
let accounts = accounts_zbus::AccountsProxy::new(&conn).await.unwrap();
let result = request_permission_on_denial(&conn, || {
accounts.delete_user(uid as i64, false)
})
.await;
if let Err(why) = result {
tracing::error!(?why, "failed to delete user account");
return Message::None;
}
Message::DeletedUser(uid)
});
}
Message::DeletedUser(uid) => {
self.users.retain(|user| user.id != uid);
}
Message::Dialog(dialog) => {
self.dialog = dialog;
}
Message::NewUser(username, full_name, password, is_admin) => {
self.dialog = None;
return cosmic::task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return Message::None;
};
let accounts = accounts_zbus::AccountsProxy::new(&conn).await.unwrap();
let user_result = request_permission_on_denial(&conn, || {
accounts.create_user(&username, &full_name, if is_admin { 1 } else { 0 })
})
.await;
let user_object_path = match user_result {
Ok(path) => path,
Err(why) => {
tracing::error!(?why, "failed to create user account");
return Message::None;
}
};
match accounts_zbus::UserProxy::new(&conn, user_object_path).await {
Ok(user) => {
_ = user.set_password(&password, "").await;
_ = user.set_icon_file(DEFAULT_ICON_FILE).await
}
Err(why) => {
tracing::error!(?why, "failed to get user by object path");
}
}
Self::reload().await
});
}
Message::SelectedUserSetAdmin(uid, is_admin) => {
return cosmic::task::future(async move {
let Ok(conn) = zbus::Connection::system().await else {
return Message::None;
};
let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await else {
return Message::None;
};
let result = request_permission_on_denial(&conn, || async {
user.set_account_type(if is_admin { 1 } else { 0 }).await
})
.await;
if let Err(why) = result {
tracing::error!(?why, "failed to change account type of user");
return Message::None;
}
Message::ChangedAccountType(uid, is_admin)
});
}
};
cosmic::Task::none()
}
}
impl page::AutoBind<crate::pages::Message> for Page {}
fn user_list() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
let user_type_standard = descriptions.insert(fl!("users", "standard"));
let user_type_admin = descriptions.insert(fl!("users", "admin"));
Section::default()
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
let descriptions = &section.descriptions;
let theme = cosmic::theme::active();
let theme = theme.cosmic();
let cosmic::cosmic_theme::Spacing {
space_xxs, space_m, ..
} = theme::active().cosmic().spacing;
let users_list = page
.users
.iter()
.enumerate()
.flat_map(|(idx, user)| {
let expanded =
matches!(page.selected_user_idx, Some(user_idx) if user_idx == idx);
let username =
widget::editable_input("", &user.username, user.username_edit, move |_| {
Message::ToggleEdit(idx, EditorField::Username)
})
.on_input(move |name| Message::Edit(idx, EditorField::Username, name))
.on_submit(Message::ApplyEdit(idx, EditorField::Username));
let password =
widget::editable_input("", &user.password, user.password_edit, move |_| {
Message::ToggleEdit(idx, EditorField::Password)
})
.on_input(move |pass| Message::Edit(idx, EditorField::Password, pass))
.on_submit(Message::ApplyEdit(idx, EditorField::Password))
.password();
let fullname = widget::editable_input(
"",
&user.full_name,
user.full_name_edit,
move |_| Message::ToggleEdit(idx, EditorField::FullName),
)
.on_input(move |name| Message::Edit(idx, EditorField::FullName, name))
.on_submit(Message::ApplyEdit(idx, EditorField::FullName));
let fullname_text = text::body(&user.full_name);
let account_type = text::caption(if user.is_admin {
&descriptions[user_type_admin]
} else {
&descriptions[user_type_standard]
});
let expanded_details = expanded.then(|| {
let mut details_list = widget::list_column()
.add(settings::item(&page.fullname_label, fullname))
.add(settings::item(&page.username_label, username))
.add(settings::item(&page.password_label, password))
.add(settings::item_row(vec![
column::with_capacity(2)
.push(text::body(crate::fl!("administrator")))
.push(text::caption(crate::fl!("administrator", "desc")))
.into(),
widget::horizontal_space().width(Length::Fill).into(),
widget::toggler(user.is_admin)
.on_toggle(|enabled| {
Message::SelectedUserSetAdmin(user.id, enabled)
})
.into(),
]));
if page.users.len() > 1 {
details_list = details_list.add(settings::item_row(vec![
widget::horizontal_space().width(Length::Fill).into(),
widget::button::destructive(crate::fl!("remove-user"))
.on_press(Message::SelectedUserDelete(user.id))
.into(),
]));
}
details_list.apply(cosmic::Element::from)
});
let profile_icon_handle = user
.profile_icon
.clone()
.unwrap_or_else(|| page.default_icon.clone());
let profile_icon = widget::button::icon(profile_icon_handle)
.large()
.on_press(Message::SelectProfileImage(user.id));
let account_details_content = settings::item_row(vec![
widget::row::with_capacity(2)
.push(profile_icon)
.push(
column::with_capacity(2)
.push(fullname_text)
.push(account_type),
)
.align_y(Alignment::Center)
.spacing(theme.space_xxs())
.into(),
widget::horizontal_space().width(Length::Fill).into(),
icon::from_name(if expanded {
"go-up-symbolic"
} else {
"go-next-symbolic"
})
.icon()
.size(16)
.into(),
]);
let account_details = Some(
widget::button::custom(account_details_content)
.padding([space_xxs, space_m])
.on_press(Message::SelectUser(idx))
.class(cosmic::theme::Button::ListItem)
.selected(expanded)
.apply(cosmic::Element::from),
);
vec![account_details, expanded_details]
})
.flatten()
.fold(
widget::list_column()
.spacing(0)
.padding(0)
.divider_padding(0)
.list_item_padding(0),
widget::ListColumn::add,
)
.apply(|list| cosmic::Element::from(settings::section::with_column(list)));
let add_user = widget::button::standard(crate::fl!("add-user"))
.on_press(Message::Dialog(Some(Dialog::AddNewUser(User::default()))))
.apply(widget::container)
.width(Length::Fill)
.align_x(Alignment::End);
widget::column::with_capacity(2)
.push(users_list)
.push(add_user)
.spacing(space_m)
.apply(Element::from)
.map(crate::pages::Message::User)
})
}
async fn check_authorization(conn: &zbus::Connection) -> anyhow::Result<()> {
let proxy = zbus_polkit::policykit1::AuthorityProxy::new(conn).await?;
let subject = zbus_polkit::policykit1::Subject::new_for_owner(std::process::id(), None, None)?;
proxy
.check_authorization(
&subject,
USERS_ADMIN_POLKIT_POLICY_ID,
&HashMap::new(),
CheckAuthorizationFlags::AllowUserInteraction.into(),
"",
)
.await?;
Ok(())
}
fn uid_range() -> (u64, u64) {
let (mut min, mut max) = (1000, 60000);
let Ok(file) = std::fs::File::open("/etc/login.defs") else {
return (min, max);
};
let mut reader = BufReader::new(file);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) | Err(_) => break,
_ => (),
}
let line = line.trim();
let variable = if line.starts_with("UID_MIN ") {
&mut min
} else if line.starts_with("UID_MAX ") {
&mut max
} else {
continue;
};
if let Some(value) = line
.split_ascii_whitespace()
.nth(1)
.and_then(|value| value.parse::<u64>().ok())
{
*variable = value;
}
}
(min, max)
}
async fn request_permission_on_denial<T, Fun, Fut>(
conn: &zbus::Connection,
action: Fun,
) -> zbus::Result<T>
where
Fun: Fn() -> Fut,
Fut: Future<Output = zbus::Result<T>>,
{
match action().await {
Ok(value) => Ok(value),
Err(why) => {
if permission_was_denied(&why) {
_ = check_authorization(conn).await;
action().await
} else {
Err(why)
}
}
}
}
fn permission_was_denied(result: &zbus::Error) -> bool {
match result {
zbus::Error::MethodError(name, _, _)
if name.as_str() == "org.freedesktop.Accounts.Error.PermissionDenied" =>
{
true
}
_ => false,
}
}