#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"
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",
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);
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;
"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,
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);
&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"),
)
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]}."
: "--- 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
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