diff --git a/src/login/logind-user.c b/src/login/logind-user.c index a9e56352f8..422fcd1b51 100644 --- a/src/login/logind-user.c +++ b/src/login/logind-user.c @@ -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", diff --git a/src/login/logind-user.h b/src/login/logind-user.h index 5e04e25a87..b71f27aa75 100644 --- a/src/login/logind-user.h +++ b/src/login/logind-user.h @@ -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); diff --git a/src/login/logind-varlink.c b/src/login/logind-varlink.c index 9b58791991..e7fd4febd0 100644 --- a/src/login/logind-varlink.c +++ b/src/login/logind-varlink.c @@ -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, diff --git a/src/shared/varlink-io.systemd.Login.c b/src/shared/varlink-io.systemd.Login.c index 42cd0aa95a..1060552b21 100644 --- a/src/shared/varlink-io.systemd.Login.c +++ b/src/shared/varlink-io.systemd.Login.c @@ -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"), diff --git a/test/units/TEST-35-LOGIN.sh b/test/units/TEST-35-LOGIN.sh index 74e83c79d4..f9d6e853ff 100755 --- a/test/units/TEST-35-LOGIN.sh +++ b/test/units/TEST-35-LOGIN.sh @@ -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