]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
logind: add ListUsers Varlink method
authorYaping Li <202858510+YapingLi04@users.noreply.github.com>
Sun, 10 May 2026 14:50:13 +0000 (14:50 +0000)
committerYaping Li <202858510+YapingLi04@users.noreply.github.com>
Fri, 22 May 2026 02:56:24 +0000 (02:56 +0000)
The Varlink ListUsers method accepts optional UID and PID filters,
folding in the D-Bus GetUser(u) and GetUserByPID(u) lookups.

Passing a unique-key filter (UID and/or PID) yields a single reply on
match, or NoSuchUser on miss. Passing no filter with the 'more' flag
streams the full list; passing no filter without 'more' resolves to
the caller's user (preserving the ergonomic default of GetUser). If
both UID and PID are specified they must reference the same user,
otherwise NoSuchUser is returned.

The UserInfo type in the io.systemd.Login Varlink IDL carries all
user properties matching the D-Bus org.freedesktop.login1.User
interface.

src/login/logind-user.c
src/login/logind-user.h
src/login/logind-varlink.c
src/shared/varlink-io.systemd.Login.c
test/units/TEST-35-LOGIN.sh

index a9e56352f8198722f26484fd2b3f663f40b5cadb..422fcd1b51dbe75acdb4de73d0392731a440334d 100644 (file)
@@ -17,6 +17,7 @@
 #include "format-util.h"
 #include "fs-util.h"
 #include "hashmap.h"
+#include "json-util.h"
 #include "limits-util.h"
 #include "logind-session.h"
 #include "logind.h"
@@ -961,6 +962,77 @@ void user_update_last_session_timer(User *u) {
                           FORMAT_TIMESPAN(user_stop_delay, USEC_PER_MSEC));
 }
 
+static int user_sessions_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+        User *u = ASSERT_PTR(userdata);
+        int r;
+
+        assert(ret);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        LIST_FOREACH(sessions_by_user, session, u->sessions) {
+                r = sd_json_variant_append_arrayb(&v, SD_JSON_BUILD_STRING(session->id));
+                if (r < 0)
+                        return r;
+        }
+
+        *ret = TAKE_PTR(v);
+        return 0;
+}
+
+static int user_context_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+        User *u = ASSERT_PTR(userdata);
+
+        assert(ret);
+        assert(u->user_record);
+
+        int linger = user_check_linger_file(u);
+        if (linger == -ENOMEM)
+                return linger;
+        if (linger < 0)
+                log_warning_errno(linger,
+                                  "Failed to check linger file for user '%s', assuming disabled: %m",
+                                  u->user_record->user_name);
+
+        return sd_json_buildo(
+                        ret,
+                        SD_JSON_BUILD_PAIR_UNSIGNED("UID", u->user_record->uid),
+                        SD_JSON_BUILD_PAIR_UNSIGNED("GID", u->user_record->gid),
+                        SD_JSON_BUILD_PAIR_STRING("Name", u->user_record->user_name),
+                        SD_JSON_BUILD_PAIR_BOOLEAN("Linger", linger > 0),
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("Service", u->service_manager_unit),
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("Slice", u->slice),
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("RuntimePath", u->runtime_path));
+}
+
+static int user_runtime_build_json(sd_json_variant **ret, const char *name, void *userdata) {
+        User *u = ASSERT_PTR(userdata);
+
+        assert(ret);
+
+        dual_timestamp idle_ts;
+        bool idle = user_get_idle_hint(u, &idle_ts);
+
+        return sd_json_buildo(
+                        ret,
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("Display", u->display ? u->display->id : NULL),
+                        JSON_BUILD_PAIR_ENUM("State", user_state_to_string(user_get_state(u))),
+                        JSON_BUILD_PAIR_CALLBACK_NON_NULL("Sessions", user_sessions_build_json, u),
+                        JSON_BUILD_PAIR_DUAL_TIMESTAMP_NON_NULL("Timestamp", &u->timestamp),
+                        SD_JSON_BUILD_PAIR_BOOLEAN("IdleHint", idle),
+                        SD_JSON_BUILD_PAIR_CONDITION(idle, "IdleSinceHint", JSON_BUILD_DUAL_TIMESTAMP(&idle_ts)));
+}
+
+int user_build_json(User *u, sd_json_variant **ret) {
+        assert(u);
+        assert(u->user_record);
+        assert(ret);
+
+        return sd_json_buildo(
+                        ret,
+                        SD_JSON_BUILD_PAIR_CALLBACK("context", user_context_build_json, u),
+                        SD_JSON_BUILD_PAIR_CALLBACK("runtime", user_runtime_build_json, u));
+}
+
 static const char* const user_state_table[_USER_STATE_MAX] = {
         [USER_OFFLINE]   = "offline",
         [USER_OPENING]   = "opening",
index 5e04e25a87c57241a4bfb3c983663ab9bba15ced..b71f27aa75e52885680421b5a27330b532127762 100644 (file)
@@ -83,6 +83,8 @@ int user_check_linger_file(const User *u);
 void user_elect_display(User *u);
 void user_update_last_session_timer(User *u);
 
+int user_build_json(User *u, sd_json_variant **ret);
+
 DECLARE_STRING_TABLE_LOOKUP(user_state, UserState);
 
 DECLARE_STRING_TABLE_LOOKUP(user_gc_mode, UserGCMode);
index 9b58791991671b4504d1c326726907303bcf73d7..e7fd4febd0a03f26efa58beed711d020d914a6cd 100644 (file)
@@ -459,6 +459,133 @@ static int vl_method_list_sessions(sd_varlink *link, sd_json_variant *parameters
         return 0;
 }
 
+static int manager_varlink_get_user_by_uid_or_pidref(
+                Manager *m,
+                uid_t uid,
+                const PidRef *pidref,
+                User **ret) {
+
+        int r;
+
+        assert(m);
+        assert(ret);
+
+        /* Resolves a user by UID and/or PID. At least one filter must be set. If both are set they must
+         * reference the same user, otherwise -ESRCH is returned. Returns -ESRCH on "not found". Returns
+         * negative errno on other failures. */
+
+        User *by_uid = NULL;
+        if (uid_is_valid(uid)) {
+                by_uid = hashmap_get(m->users, UID_TO_PTR(uid));
+                if (!by_uid)
+                        return -ESRCH;
+        }
+
+        User *by_pid = NULL;
+        if (pidref && pidref_is_set(pidref)) {
+                uid_t pid_uid;
+                r = cg_pidref_get_owner_uid(pidref, &pid_uid);
+                if (r < 0) {
+                        if (!IN_SET(r, -ENXIO, -ENOENT))
+                                return log_debug_errno(r, "Failed to acquire owning UID of PID " PID_FMT ": %m", pidref->pid);
+                        /* -ENXIO: PID not in a user-N.slice cgroup (e.g. PID 1).
+                         * -ENOENT: cgroup gone (process terminated).
+                         * Translate to -ESRCH so the caller maps to the NoSuchUser sentinel. */
+                        return -ESRCH;
+                }
+
+                by_pid = hashmap_get(m->users, UID_TO_PTR(pid_uid));
+                if (!by_pid)
+                        return -ESRCH;
+        }
+
+        if (by_uid && by_pid && by_uid != by_pid)
+                return log_debug_errno(SYNTHETIC_ERRNO(ESRCH),
+                                       "Search by UID " UID_FMT " and PID " PID_FMT " resulted in two different users",
+                                       uid, pidref->pid);
+
+        assert(by_uid || by_pid);
+
+        *ret = by_uid ?: by_pid;
+        return 0;
+}
+
+static int emit_user_reply(sd_varlink *link, User *user) {
+        assert(link);
+        assert(user);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+        int r = user_build_json(user, &v);
+        if (r < 0)
+                return r;
+
+        return sd_varlink_reply(link, v);
+}
+
+typedef struct ListUsersParameters {
+        uid_t uid;
+        PidRef pidref;
+} ListUsersParameters;
+
+static void list_users_parameters_done(ListUsersParameters *p) {
+        assert(p);
+        pidref_done(&p->pidref);
+}
+
+static int vl_method_list_users(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+        Manager *m = ASSERT_PTR(userdata);
+        int r;
+
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "UID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, offsetof(ListUsersParameters, uid),    0 },
+                { "PID", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_pidref,     offsetof(ListUsersParameters, pidref), 0 },
+                {}
+        };
+
+        _cleanup_(list_users_parameters_done) ListUsersParameters p = {
+                .uid = UID_INVALID,
+                .pidref = PIDREF_NULL,
+        };
+
+        r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+        if (r != 0)
+                return r;
+
+        /* Unique-key path: UID and/or PID provided. Single reply or NoSuchUser. */
+        if (uid_is_valid(p.uid) || pidref_is_set(&p.pidref)) {
+                r = sd_varlink_set_sentinel(link, "io.systemd.Login.NoSuchUser");
+                if (r < 0)
+                        return r;
+
+                User *user;
+                r = manager_varlink_get_user_by_uid_or_pidref(m, p.uid, &p.pidref, &user);
+                if (r == -ESRCH)
+                        return 0; /* triggers NoSuchUser sentinel */
+                if (r < 0)
+                        return r;
+
+                return emit_user_reply(link, user);
+        }
+
+        /* Streaming path: no filter. Full list, requires 'more' flag. */
+        if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
+                return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, /* parameters= */ NULL);
+
+        /* Empty hashmap is a valid empty stream, not "not found" — see vl_method_list_inhibitors. */
+        r = sd_varlink_set_sentinel(link, /* error_id= */ NULL);
+        if (r < 0)
+                return r;
+
+        User *user;
+        HASHMAP_FOREACH(user, m->users) {
+                r = emit_user_reply(link, user);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
 static int vl_method_release_session(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
         Manager *m = ASSERT_PTR(userdata);
         int r;
@@ -627,6 +754,7 @@ int manager_varlink_init(Manager *m, int fd) {
                         "io.systemd.Login.CreateSession",    vl_method_create_session,
                         "io.systemd.Login.ReleaseSession",   vl_method_release_session,
                         "io.systemd.Login.ListSessions",     vl_method_list_sessions,
+                        "io.systemd.Login.ListUsers",        vl_method_list_users,
                         "io.systemd.Shutdown.PowerOff",      vl_method_power_off,
                         "io.systemd.Shutdown.Reboot",        vl_method_reboot,
                         "io.systemd.Shutdown.Halt",          vl_method_halt,
index 42cd0aa95a082a2a41d2b3a63b7e6ffd4b82d499..1060552b21600fe128d27bff30f319a49dd8b52e 100644 (file)
@@ -156,7 +156,52 @@ static SD_VARLINK_DEFINE_METHOD(
                 SD_VARLINK_FIELD_COMMENT("The identifier string of the session to release. If unspecified or 'self', will return the callers session."),
                 SD_VARLINK_DEFINE_INPUT(Id, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
 
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+                UserContext,
+                SD_VARLINK_FIELD_COMMENT("Numeric UNIX UID"),
+                SD_VARLINK_DEFINE_FIELD(UID, SD_VARLINK_INT, 0),
+                SD_VARLINK_FIELD_COMMENT("Numeric UNIX GID"),
+                SD_VARLINK_DEFINE_FIELD(GID, SD_VARLINK_INT, 0),
+                SD_VARLINK_FIELD_COMMENT("User name"),
+                SD_VARLINK_DEFINE_FIELD(Name, SD_VARLINK_STRING, 0),
+                SD_VARLINK_FIELD_COMMENT("Whether lingering is enabled for this user"),
+                SD_VARLINK_DEFINE_FIELD(Linger, SD_VARLINK_BOOL, 0),
+                SD_VARLINK_FIELD_COMMENT("Service manager unit name"),
+                SD_VARLINK_DEFINE_FIELD(Service, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("User slice unit name"),
+                SD_VARLINK_DEFINE_FIELD(Slice, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Path to the user's runtime directory"),
+                SD_VARLINK_DEFINE_FIELD(RuntimePath, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_STRUCT_TYPE(
+                UserRuntime,
+                SD_VARLINK_FIELD_COMMENT("Display session identifier"),
+                SD_VARLINK_DEFINE_FIELD(Display, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Current state of the user"),
+                SD_VARLINK_DEFINE_FIELD(State, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Identifiers of sessions belonging to this user"),
+                SD_VARLINK_DEFINE_FIELD(Sessions, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+                SD_VARLINK_FIELD_COMMENT("Timestamp when the user was 'started' for the first time"),
+                SD_VARLINK_DEFINE_FIELD_BY_TYPE(Timestamp, Timestamp, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Whether the user is idle"),
+                SD_VARLINK_DEFINE_FIELD(IdleHint, SD_VARLINK_BOOL, 0),
+                SD_VARLINK_FIELD_COMMENT("Timestamp when the user went idle, only present when IdleHint is true"),
+                SD_VARLINK_DEFINE_FIELD_BY_TYPE(IdleSinceHint, Timestamp, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD_FULL(
+                ListUsers,
+                SD_VARLINK_SUPPORTS_MORE,
+                SD_VARLINK_FIELD_COMMENT("If non-null, the UNIX UID of a user to look up."),
+                SD_VARLINK_DEFINE_INPUT(UID, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("If non-null, return the user owning the cgroup containing the process with this PID. If both UID and PID are specified they must reference the same user, otherwise NoSuchUser is returned."),
+                SD_VARLINK_DEFINE_INPUT_BY_TYPE(PID, ProcessId, SD_VARLINK_NULLABLE),
+                SD_VARLINK_FIELD_COMMENT("Configuration of the user"),
+                SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(context, UserContext, 0),
+                SD_VARLINK_FIELD_COMMENT("Runtime information of the user"),
+                SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(runtime, UserRuntime, 0));
+
 static SD_VARLINK_DEFINE_ERROR(NoSuchSession);
+static SD_VARLINK_DEFINE_ERROR(NoSuchUser);
 static SD_VARLINK_DEFINE_ERROR(NoSuchSeat);
 static SD_VARLINK_DEFINE_ERROR(AlreadySessionMember);
 static SD_VARLINK_DEFINE_ERROR(VirtualTerminalAlreadyTaken);
@@ -186,10 +231,18 @@ SD_VARLINK_DEFINE_INTERFACE(
                 &vl_method_ReleaseSession,
                 SD_VARLINK_SYMBOL_COMMENT("Lists current sessions. If an ID or PID filter is provided, returns the single matching session; otherwise streams all current sessions (requires the 'more' flag)."),
                 &vl_method_ListSessions,
+                SD_VARLINK_SYMBOL_COMMENT("Configuration aspects of a user"),
+                &vl_type_UserContext,
+                SD_VARLINK_SYMBOL_COMMENT("Runtime state and dynamic information of a user"),
+                &vl_type_UserRuntime,
+                SD_VARLINK_SYMBOL_COMMENT("Lists current users. If a UID or PID filter is provided, returns the single matching user; otherwise streams all current users (requires the 'more' flag). If called with no parameters and no 'more' flag, resolves to the caller's user."),
+                &vl_method_ListUsers,
                 SD_VARLINK_SYMBOL_COMMENT("No session by this name found"),
                 &vl_error_NoSuchSession,
                 SD_VARLINK_SYMBOL_COMMENT("No seat by this name found"),
                 &vl_error_NoSuchSeat,
+                SD_VARLINK_SYMBOL_COMMENT("No user by this UID found"),
+                &vl_error_NoSuchUser,
                 SD_VARLINK_SYMBOL_COMMENT("Process already member of a session"),
                 &vl_error_AlreadySessionMember,
                 SD_VARLINK_SYMBOL_COMMENT("The specified virtual terminal (VT) is already taken by another session"),
index 74e83c79d4cf25689d7004ab41509a07d800019f..f9d6e853ff162c3fabe30d915eb150b0287a9625 100755 (executable)
@@ -806,7 +806,7 @@ teardown_varlink() (
 )
 
 testcase_varlink() {
-    local session uid session_out
+    local session uid session_out user_out
 
     if [[ ! -c /dev/tty2 ]]; then
         echo "/dev/tty2 does not exist, skipping test ${FUNCNAME[0]}."
@@ -820,6 +820,7 @@ testcase_varlink() {
     : "--- Introspect ---"
     varlinkctl introspect "$VARLINK_SOCKET"
     varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListSessions" >/dev/null
+    varlinkctl introspect "$VARLINK_SOCKET" | grep "method ListUsers" >/dev/null
 
     : "--- Setup test session ---"
     create_session
@@ -879,6 +880,49 @@ testcase_varlink() {
     echo "$list_err" | grep "'more' flag" >/dev/null
     systemctl is-active systemd-logind.service >/dev/null
 
+    : "--- ListUsers: UID filter (single reply) ---"
+    user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers "{\"UID\":$uid}")
+    echo "$user_out" | jq -e ".context.UID == $uid" >/dev/null
+    echo "$user_out" | jq -e '.context.Name == "logind-test-user"' >/dev/null
+    echo "$user_out" | jq -e '.runtime.State' >/dev/null
+    echo "$user_out" | jq -e '.context.Linger == false' >/dev/null
+    # Sessions is now a flat array of session id strings (no wrapper object).
+    echo "$user_out" | jq -e ".runtime.Sessions[] | select(. == \"$session\")" >/dev/null
+
+    : "--- ListUsers: PID filter (single reply) ---"
+    # PID of a process in the test user's session should map back to the user.
+    local pid_user_out
+    pid_user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \
+        "{\"PID\":{\"pid\":$leader_pid}}")
+    echo "$pid_user_out" | jq -e --argjson u "$uid" '.context.UID == $u' >/dev/null
+
+    : "--- ListUsers: UID+PID consistency check ---"
+    # Same user referenced two ways: must succeed.
+    local ok_user_out mismatch_user_err
+    ok_user_out=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \
+        "{\"UID\":$uid,\"PID\":{\"pid\":$leader_pid}}")
+    echo "$ok_user_out" | jq -e --argjson u "$uid" '.context.UID == $u' >/dev/null
+    # Mismatched UID and PID (PID 1 is systemd init, UID 0): must fail with NoSuchUser.
+    mismatch_user_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers \
+        "{\"UID\":$uid,\"PID\":{\"pid\":1}}" 2>&1 || true)
+    echo "$mismatch_user_err" | grep NoSuchUser >/dev/null
+
+    : "--- ListUsers: empty input without --more must require --more ---"
+    # The DescribeUser-style caller-UID fallback was removed; a no-filter call without
+    # --more must fail with the EXPECTED_MORE error.
+    local empty_users_err
+    empty_users_err=$(varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' 2>&1 || true)
+    echo "$empty_users_err" | grep "'more' flag" >/dev/null
+    systemctl is-active systemd-logind.service >/dev/null
+
+    # nonexistent UID
+    (! varlinkctl call "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{"UID":4294967294}')
+
+    : "--- ListUsers: streaming path ---"
+    varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' \
+        | jq --seq -e 'select(.context.Name == "logind-test-user")' >/dev/null
+    test "$(varlinkctl call --more "$VARLINK_SOCKET" io.systemd.Login.ListUsers '{}' | wc -l)" -ge 2
+
     : "--- ReleaseSession: NULL ID resolves to caller's session ---"
     # A caller with a logind session calling ReleaseSession '{}' (no ID) must
     # release its own session. We spawn the call inside a transient unit with