From 1d4e32ba82829038b58c0b01904eb5ab6ad331df Mon Sep 17 00:00:00 2001 From: AnErrupTion Date: Thu, 10 Jul 2025 10:06:19 +0200 Subject: [PATCH] List all users in the system (fixes #373) Signed-off-by: AnErrupTion --- res/config.ini | 3 + res/lang/ar.ini | 1 + res/lang/cat.ini | 1 + res/lang/cs.ini | 1 + res/lang/de.ini | 1 + res/lang/en.ini | 1 + res/lang/es.ini | 1 + res/lang/fr.ini | 1 + res/lang/it.ini | 1 + res/lang/pl.ini | 1 + res/lang/pt.ini | 1 + res/lang/pt_BR.ini | 1 + res/lang/ro.ini | 1 + res/lang/ru.ini | 1 + res/lang/sr.ini | 1 + res/lang/sv.ini | 1 + res/lang/tr.ini | 1 + res/lang/uk.ini | 1 + res/lang/zh_CN.ini | 1 + src/UidRange.zig | 6 ++ src/config/Config.zig | 1 + src/config/Lang.zig | 1 + src/main.zig | 124 ++++++++++++++++++++++++++------ src/tui/components/UserList.zig | 45 ++++++++++++ 24 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 src/UidRange.zig create mode 100644 src/tui/components/UserList.zig diff --git a/res/config.ini b/res/config.ini index 8b4a4f9..866b6b2 100644 --- a/res/config.ini +++ b/res/config.ini @@ -184,6 +184,9 @@ load = true # You can also set environment variables in there, they'll persist until logout login_cmd = null +# Path for login.defs file (used for listing all local users on the system) +login_defs_path = /etc/login.defs + # Command executed when logging out # If null, no command will be executed # Important: the session will already be terminated when this command is executed, so diff --git a/res/lang/ar.ini b/res/lang/ar.ini index 9c2f9be..b06f253 100644 --- a/res/lang/ar.ini +++ b/res/lang/ar.ini @@ -41,6 +41,7 @@ err_pwnam = فشل في جلب معلومات المستخدم err_sleep = فشل في تنفيذ أمر sleep err_tty_ctrl = فشل في نقل تحكم الطرفية (TTY) + err_user_gid = فشل في تعيين معرّف المجموعة (GID) للمستخدم err_user_init = فشل في تهيئة بيانات المستخدم err_user_uid = فشل في تعيين معرّف المستخدم (UID) diff --git a/res/lang/cat.ini b/res/lang/cat.ini index 7aa4261..d0ce33e 100644 --- a/res/lang/cat.ini +++ b/res/lang/cat.ini @@ -41,6 +41,7 @@ err_pwnam = error en obtenir la informació de l'usuari + err_user_gid = error en establir el GID de l'usuari err_user_init = error en inicialitzar usuari err_user_uid = error en establir l'UID de l'usuari diff --git a/res/lang/cs.ini b/res/lang/cs.ini index cca457a..7c87ea6 100644 --- a/res/lang/cs.ini +++ b/res/lang/cs.ini @@ -41,6 +41,7 @@ err_pwnam = nelze získat informace o uživateli + err_user_gid = nastavení GID uživatele selhalo err_user_init = inicializace uživatele selhala err_user_uid = nastavení UID uživateli selhalo diff --git a/res/lang/de.ini b/res/lang/de.ini index 32e00a6..44bbc53 100644 --- a/res/lang/de.ini +++ b/res/lang/de.ini @@ -41,6 +41,7 @@ err_pwnam = Abrufen der Benutzerinformationen fehlgeschlagen err_sleep = Sleep-Befehl fehlgeschlagen err_tty_ctrl = Fehler bei der TTY-Uebergabe + err_user_gid = Fehler beim Setzen der Gruppen-ID err_user_init = Nutzer-Initialisierung fehlgeschlagen err_user_uid = Setzen der Benutzer-ID fehlgeschlagen diff --git a/res/lang/en.ini b/res/lang/en.ini index d7076db..9554658 100644 --- a/res/lang/en.ini +++ b/res/lang/en.ini @@ -41,6 +41,7 @@ err_pwnam = failed to get user info err_sleep = failed to execute sleep command err_switch_tty = failed to switch tty err_tty_ctrl = tty control transfer failed +err_no_users = no users found err_user_gid = failed to set user GID err_user_init = failed to initialize user err_user_uid = failed to set user UID diff --git a/res/lang/es.ini b/res/lang/es.ini index fe3e6d9..2bd51a7 100644 --- a/res/lang/es.ini +++ b/res/lang/es.ini @@ -41,6 +41,7 @@ err_pwnam = error al obtener la información del usuario + err_user_gid = error al establecer el GID del usuario err_user_init = error al inicializar usuario err_user_uid = error al establecer el UID del usuario diff --git a/res/lang/fr.ini b/res/lang/fr.ini index 6d46a1d..e024820 100644 --- a/res/lang/fr.ini +++ b/res/lang/fr.ini @@ -41,6 +41,7 @@ err_pwnam = échec de lecture des infos utilisateur err_sleep = échec de l'exécution de la commande de veille err_switch_tty = échec du changement de terminal err_tty_ctrl = échec du transfert de contrôle du terminal +err_no_users = aucun utilisateur trouvé err_user_gid = échec de modification du GID err_user_init = échec d'initialisation de l'utilisateur err_user_uid = échec de modification du UID diff --git a/res/lang/it.ini b/res/lang/it.ini index 4bff87e..3a7f5a8 100644 --- a/res/lang/it.ini +++ b/res/lang/it.ini @@ -41,6 +41,7 @@ err_pwnam = impossibile ottenere dati utente + err_user_gid = impossibile impostare GID utente err_user_init = impossibile inizializzare utente err_user_uid = impossible impostare UID utente diff --git a/res/lang/pl.ini b/res/lang/pl.ini index cfc5722..b3eee3d 100644 --- a/res/lang/pl.ini +++ b/res/lang/pl.ini @@ -41,6 +41,7 @@ err_pwnam = nie udało się uzyskać informacji o użytkowniku err_sleep = nie udało się wykonać polecenia sleep err_tty_ctrl = nie udało się przekazać kontroli tty + err_user_gid = nie udało się ustawić GID użytkownika err_user_init = nie udało się zainicjalizować użytkownika err_user_uid = nie udało się ustawić UID użytkownika diff --git a/res/lang/pt.ini b/res/lang/pt.ini index df1db13..587d3b4 100644 --- a/res/lang/pt.ini +++ b/res/lang/pt.ini @@ -41,6 +41,7 @@ err_pwnam = erro ao obter informação do utilizador + err_user_gid = erro ao definir o GID do utilizador err_user_init = erro ao iniciar o utilizador err_user_uid = erro ao definir o UID do utilizador diff --git a/res/lang/pt_BR.ini b/res/lang/pt_BR.ini index a7cee7b..2485522 100644 --- a/res/lang/pt_BR.ini +++ b/res/lang/pt_BR.ini @@ -41,6 +41,7 @@ err_pwnam = não foi possível obter informações do usuário + err_user_gid = não foi possível definir o GID do usuário err_user_init = não foi possível iniciar o usuário err_user_uid = não foi possível definir o UID do usuário diff --git a/res/lang/ro.ini b/res/lang/ro.ini index 4289b7f..74c0461 100644 --- a/res/lang/ro.ini +++ b/res/lang/ro.ini @@ -49,6 +49,7 @@ err_perm_user = nu s-a putut face downgrade permisiunilor de utilizator + login = utilizator logout = opreşte sesiunea diff --git a/res/lang/ru.ini b/res/lang/ru.ini index b9cfb1a..103f135 100644 --- a/res/lang/ru.ini +++ b/res/lang/ru.ini @@ -41,6 +41,7 @@ err_pwnam = не удалось получить информацию о пол + err_user_gid = не удалось установить GID пользователя err_user_init = не удалось инициализировать пользователя err_user_uid = не удалось установить UID пользователя diff --git a/res/lang/sr.ini b/res/lang/sr.ini index 61ec4c0..c63fe28 100644 --- a/res/lang/sr.ini +++ b/res/lang/sr.ini @@ -41,6 +41,7 @@ err_pwnam = neuspijesno skupljanje informacija o korisniku + err_user_gid = neuspijesno postavljanje korisničkog GID-a err_user_init = neuspijensa inicijalizacija korisnika err_user_uid = neuspijesno postavljanje UID-a korisnika diff --git a/res/lang/sv.ini b/res/lang/sv.ini index 9ccd0f3..17359cc 100644 --- a/res/lang/sv.ini +++ b/res/lang/sv.ini @@ -41,6 +41,7 @@ err_pwnam = misslyckades att hämta användarinfo + err_user_gid = misslyckades att ställa in användar-GID err_user_init = misslyckades att initialisera användaren err_user_uid = misslyckades att ställa in användar-UID diff --git a/res/lang/tr.ini b/res/lang/tr.ini index d553495..535015c 100644 --- a/res/lang/tr.ini +++ b/res/lang/tr.ini @@ -41,6 +41,7 @@ err_pwnam = kullanici bilgileri alinamadi + err_user_gid = kullanici icin GID ayarlanamadi err_user_init = kullanici oturumu baslatilamadi err_user_uid = kullanici icin UID ayarlanamadi diff --git a/res/lang/uk.ini b/res/lang/uk.ini index ffae75c..9e6de2d 100644 --- a/res/lang/uk.ini +++ b/res/lang/uk.ini @@ -41,6 +41,7 @@ err_pwnam = не вдалося отримати дані користувача + err_user_gid = не вдалося змінити GID користувача err_user_init = не вдалося ініціалізувати користувача err_user_uid = не вдалося змінити UID користувача diff --git a/res/lang/zh_CN.ini b/res/lang/zh_CN.ini index 6f7ba41..9afdc66 100644 --- a/res/lang/zh_CN.ini +++ b/res/lang/zh_CN.ini @@ -41,6 +41,7 @@ err_pwnam = 获取用户信息失败 + err_user_gid = 设置用户GID失败 err_user_init = 初始化用户失败 err_user_uid = 设置用户UID失败 diff --git a/src/UidRange.zig b/src/UidRange.zig new file mode 100644 index 0000000..13eb979 --- /dev/null +++ b/src/UidRange.zig @@ -0,0 +1,6 @@ +const std = @import("std"); + +// We set both values to 0 by default so that, in case they aren't present in +// the login.defs for some reason, then only the root username will be shown +uid_min: std.c.uid_t = 0, +uid_max: std.c.uid_t = 0, diff --git a/src/config/Config.zig b/src/config/Config.zig index 4a95b40..92e0552 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -50,6 +50,7 @@ input_len: u8 = 34, lang: []const u8 = "en", load: bool = true, login_cmd: ?[]const u8 = null, +login_defs_path: []const u8 = "/etc/login.defs", logout_cmd: ?[]const u8 = null, margin_box_h: u8 = 2, margin_box_v: u8 = 1, diff --git a/src/config/Lang.zig b/src/config/Lang.zig index 3761fa7..7175e0f 100644 --- a/src/config/Lang.zig +++ b/src/config/Lang.zig @@ -46,6 +46,7 @@ err_pwnam: []const u8 = "failed to get user info", err_sleep: []const u8 = "failed to execute sleep command", err_switch_tty: []const u8 = "failed to switch tty", err_tty_ctrl: []const u8 = "tty control transfer failed", +err_no_users: []const u8 = "no users found", err_user_gid: []const u8 = "failed to set user GID", err_user_init: []const u8 = "failed to initialize user", err_user_uid: []const u8 = "failed to set user UID", diff --git a/src/main.zig b/src/main.zig index b52e779..0cc2c6e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -18,12 +18,15 @@ const TerminalBuffer = @import("tui/TerminalBuffer.zig"); const Session = @import("tui/components/Session.zig"); const Text = @import("tui/components/Text.zig"); const InfoLine = @import("tui/components/InfoLine.zig"); +const UserList = @import("tui/components/UserList.zig"); const Config = @import("config/Config.zig"); const Lang = @import("config/Lang.zig"); const Save = @import("config/Save.zig"); const migrator = @import("config/migrator.zig"); const SharedError = @import("SharedError.zig"); +const UidRange = @import("UidRange.zig"); +const StringList = std.ArrayListUnmanaged([]const u8); const Ini = ini.Ini; const DisplayServer = enums.DisplayServer; const Entry = Environment.Entry; @@ -308,7 +311,21 @@ pub fn main() !void { try crawl(&session, lang, dir, .custom); } - var login = Text.init(allocator, &buffer, false, null); + var usernames = try getAllUsernames(allocator, config.login_defs_path); + defer { + for (usernames.items) |username| allocator.free(username); + usernames.deinit(allocator); + } + + if (usernames.items.len == 0) { + // If we have no usernames, simply add an error to the info line. + // This effectively means you can't login, since there would be no local + // accounts *and* no root account...but at this point, if that's the + // case, you have bigger problems to deal with in the first place. :D + try info_line.addMessage(lang.err_no_users, config.error_bg, config.error_fg); + } + + var login = try UserList.init(allocator, &buffer, usernames); defer login.deinit(); var password = Text.init(allocator, &buffer, true, config.asterisk); @@ -320,9 +337,17 @@ pub fn main() !void { // Load last saved username and desktop selection, if any if (config.load) { if (save.user) |user| { - try login.text.appendSlice(login.allocator, user); - login.end = user.len; - login.cursor = login.end; + // Find user with saved name, and switch over to it + // If it doesn't exist (anymore), we don't change the value + // Note that we could instead save the username index, but migrating + // from the raw username to an index is non-trivial and I'm lazy :P + for (usernames.items, 0..) |username, i| { + if (std.mem.eql(u8, username, user)) { + login.label.current = i; + break; + } + } + active_input = .password; } @@ -338,15 +363,13 @@ pub fn main() !void { const coordinates = buffer.calculateComponentCoordinates(); info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); + login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); switch (active_input) { .info_line => info_line.label.handle(null, insert_mode), .session => session.label.handle(null, insert_mode), - .login => login.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, + .login => login.label.handle(null, insert_mode), .password => password.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -464,7 +487,7 @@ pub fn main() !void { const coordinates = buffer.calculateComponentCoordinates(); info_line.label.position(coordinates.start_x, coordinates.y, coordinates.full_visible_length, null); session.label.position(coordinates.x, coordinates.y + 2, coordinates.visible_length, config.text_in_center); - login.position(coordinates.x, coordinates.y + 4, coordinates.visible_length); + login.label.position(coordinates.x, coordinates.y + 4, coordinates.visible_length, config.text_in_center); password.position(coordinates.x, coordinates.y + 6, coordinates.visible_length); resolution_changed = false; @@ -473,9 +496,7 @@ pub fn main() !void { switch (active_input) { .info_line => info_line.label.handle(null, insert_mode), .session => session.label.handle(null, insert_mode), - .login => login.handle(null, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, + .login => login.label.handle(null, insert_mode), .password => password.handle(null, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -571,7 +592,7 @@ pub fn main() !void { } session.label.draw(); - login.draw(); + login.label.draw(); password.draw(); } else { std.Thread.sleep(std.time.ns_per_ms * 10); @@ -660,10 +681,7 @@ pub fn main() !void { }, termbox.TB_KEY_CTRL_C => run = false, termbox.TB_KEY_CTRL_U => { - if (active_input == .login) { - login.clear(); - update = true; - } else if (active_input == .password) { + if (active_input == .password) { password.clear(); update = true; } @@ -726,7 +744,7 @@ pub fn main() !void { defer file.close(); const save_data = Save{ - .user = login.text.items, + .user = login.getCurrentUser(), .session_index = session.label.current, }; ini.writeFromStruct(save_data, file.writer(), null, .{}) catch break :save_last_settings; @@ -739,7 +757,7 @@ pub fn main() !void { defer shared_err.deinit(); { - const login_text = try allocator.dupeZ(u8, login.text.items); + const login_text = try allocator.dupeZ(u8, login.getCurrentUser()); defer allocator.free(login_text); const password_text = try allocator.dupeZ(u8, password.text.items); defer allocator.free(password_text); @@ -845,9 +863,7 @@ pub fn main() !void { switch (active_input) { .info_line => info_line.label.handle(&event, insert_mode), .session => session.label.handle(&event, insert_mode), - .login => login.handle(&event, insert_mode) catch { - try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); - }, + .login => login.label.handle(&event, insert_mode), .password => password.handle(&event, insert_mode) catch { try info_line.addMessage(lang.err_alloc, config.error_bg, config.error_fg); }, @@ -934,6 +950,70 @@ fn crawl(session: *Session, lang: Lang, path: []const u8, display_server: Displa } } +fn getAllUsernames(allocator: std.mem.Allocator, login_defs_path: []const u8) !StringList { + const uid_range = try getUserIdRange(allocator, login_defs_path); + + var usernames: StringList = .empty; + var maybe_entry = interop.pwd.getpwent(); + + while (maybe_entry != null) { + const entry = maybe_entry.*; + + // We check if the UID is equal to 0 because we always want to add root + // as a username (even if you can't log into it) + if (entry.pw_uid >= uid_range.uid_min and entry.pw_uid <= uid_range.uid_max or entry.pw_uid == 0) { + const pw_name_slice = entry.pw_name[0..std.mem.len(entry.pw_name)]; + const username = try allocator.dupe(u8, pw_name_slice); + + try usernames.append(allocator, username); + } + + maybe_entry = interop.pwd.getpwent(); + } + + interop.pwd.endpwent(); + return usernames; +} + +// This is very bad parsing, but we only need to get 2 values... and the format +// of the file doesn't seem to be standard? So this should be fine... +fn getUserIdRange(allocator: std.mem.Allocator, login_defs_path: []const u8) !UidRange { + const login_defs_file = try std.fs.cwd().openFile(login_defs_path, .{}); + defer login_defs_file.close(); + + const login_defs_buffer = try login_defs_file.readToEndAlloc(allocator, std.math.maxInt(u16)); + defer allocator.free(login_defs_buffer); + + var iterator = std.mem.splitScalar(u8, login_defs_buffer, '\n'); + var uid_range = UidRange{}; + + while (iterator.next()) |line| { + const trimmed_line = std.mem.trim(u8, line, " \n\r\t"); + + if (std.mem.startsWith(u8, trimmed_line, "UID_MIN")) { + uid_range.uid_min = try parseValue(std.c.uid_t, "UID_MIN", trimmed_line); + } else if (std.mem.startsWith(u8, trimmed_line, "UID_MAX")) { + uid_range.uid_max = try parseValue(std.c.uid_t, "UID_MAX", trimmed_line); + } + } + + return uid_range; +} + +fn parseValue(comptime T: type, name: []const u8, buffer: []const u8) !T { + var iterator = std.mem.splitAny(u8, buffer, " \t"); + var maybe_value: ?T = null; + + while (iterator.next()) |slice| { + // Skip the slice if it's empty (whitespace) or is the name of the + // property (e.g. UID_MIN or UID_MAX) + if (slice.len == 0 or std.mem.eql(u8, slice, name)) continue; + maybe_value = std.fmt.parseInt(T, slice, 10) catch continue; + } + + return maybe_value orelse error.ValueNotFound; +} + fn adjustBrightness(allocator: std.mem.Allocator, cmd: []const u8) !void { var brightness = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", cmd }, allocator); brightness.stdout_behavior = .Ignore; diff --git a/src/tui/components/UserList.zig b/src/tui/components/UserList.zig new file mode 100644 index 0000000..41eff39 --- /dev/null +++ b/src/tui/components/UserList.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const TerminalBuffer = @import("../TerminalBuffer.zig"); +const generic = @import("generic.zig"); + +const StringList = std.ArrayListUnmanaged([]const u8); +const Allocator = std.mem.Allocator; + +const UsernameText = generic.CyclableLabel([]const u8); + +const UserList = @This(); + +label: UsernameText, + +pub fn init(allocator: Allocator, buffer: *TerminalBuffer, usernames: StringList) !UserList { + var userList = UserList{ + .label = UsernameText.init(allocator, buffer, drawItem), + }; + + for (usernames.items) |username| { + if (username.len == 0) continue; + + try userList.label.addItem(username); + } + + return userList; +} + +pub fn deinit(self: *UserList) void { + self.label.deinit(); +} + +pub fn getCurrentUser(self: UserList) []const u8 { + return self.label.list.items[self.label.current]; +} + +fn drawItem(label: *UsernameText, username: []const u8, _: usize, _: usize) bool { + const length = @min(username.len, label.visible_length - 3); + if (length == 0) return false; + + const x = if (label.text_in_center) (label.x + (label.visible_length - username.len) / 2) else (label.x + 2); + label.first_char_x = x + username.len; + + label.buffer.drawLabel(username, x, label.y); + return true; +}