From f9e34aef23e445918b40b23167e8874077874152 Mon Sep 17 00:00:00 2001 From: guolin Date: Thu, 20 Aug 2026 16:14:46 +0800 Subject: [PATCH] feat: support automatic login for user sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add autologin support with dedicated ddm-autologin PAM service and nopasswdlogin group for passwordless login. 新增自动登录功能:提供独立的ddm-autologin PAM服务,并通过 nopasswdlogin用户组支持免密登录。重构Display登录流程,提取 startUserSession()复用会话启动逻辑,登录时根据配置决定直接 自动登录或显示登录界面。 PMS: BUG-294419 Log: 支持用户自动登录 Influence: 配置Autologin后可跳过登录界面直接进入桌面;nopasswdlogin组成员可免密登录。 --- services/CMakeLists.txt | 2 + services/ddm-autologin.pam | 34 +++++++ services/ddm-sysuser.conf.in | 1 + services/ddm.pam | 3 + services/debian.ddm-autologin.pam | 49 +++++++++ services/debian.ddm-sysuser.conf.in | 1 + services/debian.ddm.pam | 4 +- src/common/Configuration.h | 7 ++ src/common/Messages.h | 1 + src/daemon/Auth.cpp | 2 +- src/daemon/Auth.h | 4 + src/daemon/Display.cpp | 153 +++++++++++++++++++++------- src/daemon/Display.h | 20 ++++ 13 files changed, 240 insertions(+), 41 deletions(-) create mode 100644 services/ddm-autologin.pam create mode 100644 services/debian.ddm-autologin.pam diff --git a/services/CMakeLists.txt b/services/CMakeLists.txt index 1e0b7a3..94b8f11 100644 --- a/services/CMakeLists.txt +++ b/services/CMakeLists.txt @@ -9,6 +9,7 @@ endif() if(EXISTS "/etc/debian_version") install(FILES debian.ddm.pam DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d RENAME ddm) + install(FILES debian.ddm-autologin.pam DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d RENAME ddm-autologin) # In debian-based systems, seatd uses video group instead of seat # group, avoid creating seat group mistakenly by specifying @@ -17,6 +18,7 @@ if(EXISTS "/etc/debian_version") install(FILES "${CMAKE_CURRENT_BINARY_DIR}/debian.ddm-sysuser.conf" DESTINATION "${SYSTEMD_SYSUSERS_DIR}" RENAME dde.conf) else() install(FILES ddm.pam DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d RENAME ddm) + install(FILES ddm-autologin.pam DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/pam.d RENAME ddm-autologin) configure_file(ddm-sysuser.conf.in ddm-sysuser.conf) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/ddm-sysuser.conf" DESTINATION "${SYSTEMD_SYSUSERS_DIR}" RENAME dde.conf) endif() diff --git a/services/ddm-autologin.pam b/services/ddm-autologin.pam new file mode 100644 index 0000000..8bc87e9 --- /dev/null +++ b/services/ddm-autologin.pam @@ -0,0 +1,34 @@ +#%PAM-1.0 + +# Block login if shell is nologin or false +auth required pam_succeed_if.so shell notin /sbin/nologin:/usr/sbin/nologin:/bin/false:/usr/bin/false + +# Block login if they are globally disabled +auth requisite pam_nologin.so + +# Load environment from /etc/environment +auth required pam_env.so + +# Allow access without authentication +auth required pam_permit.so + +# Check account is active, change password if required +@include common-account + +# Can't change password +password required pam_deny.so + +# Setup session +session optional pam_keyinit.so force revoke +session required pam_limits.so +session required pam_loginuid.so +session required pam_systemd.so +@include common-session +-session optional pam_gnome_keyring.so auto_start +-session optional pam_kwallet5.so auto_start + +# Load environment from /etc/environment +session required pam_env.so + +# Load environment from /etc/default/locale and ~/.pam_environment +session required pam_env.so envfile=/etc/default/locale user_readenv=1 diff --git a/services/ddm-sysuser.conf.in b/services/ddm-sysuser.conf.in index 6dc4b74..506ceed 100644 --- a/services/ddm-sysuser.conf.in +++ b/services/ddm-sysuser.conf.in @@ -1,6 +1,7 @@ #Type Name ID GECOS Home directory Shell u dde - "DDM Greeter Account" ${STATE_DIR} - g dde - +g nopasswdlogin - m dde dde m dde seat m dde video diff --git a/services/ddm.pam b/services/ddm.pam index e7a8e76..c89c5a3 100644 --- a/services/ddm.pam +++ b/services/ddm.pam @@ -1,5 +1,8 @@ #%PAM-1.0 +# Allow members of the nopasswdlogin group to log in without a password +auth sufficient pam_succeed_if.so user ingroup nopasswdlogin + auth include system-login -auth optional pam_gnome_keyring.so -auth optional pam_kwallet5.so diff --git a/services/debian.ddm-autologin.pam b/services/debian.ddm-autologin.pam new file mode 100644 index 0000000..915245e --- /dev/null +++ b/services/debian.ddm-autologin.pam @@ -0,0 +1,49 @@ +#%PAM-1.0 +auth requisite pam_inhibit_autologin.so +# Block login if shell in nologin or false +auth required pam_succeed_if.so shell notin /sbin/nologin:/usr/sbin/nologin:/bin/false:/usr/bin/false + +# Block login if they are globally disabled +auth requisite pam_nologin.so + +# Load environment from /etc/environment and ~/.pam_environment +session required pam_env.so readenv=1 +session required pam_env.so readenv=1 envfile=/etc/default/locale + +# Unlock keyring when user auto login +-auth optional pam_deepin_keyring.so +-auth optional pam_gnome_keyring.so +-auth optional pam_kwallet5.so + +# Allow access without authentication +auth required pam_succeed_if.so user != root quiet_success +auth required pam_permit.so + +@include common-account + +# SELinux needs to be the first session rule. This ensures that any +# lingering context has been cleared. Without out this it is possible +# that a module could execute code in the wrong domain. +# When the module is present, "required" would be sufficient (When SELinux +# is disabled, this returns success.) +session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close + +session required pam_limits.so +session required pam_loginuid.so +@include common-session + +# SELinux needs to intervene at login time to ensure that the process +# starts in the proper default security context. Only sessions which are +# intended to run in the user's context should be run after this. +session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open +# When the module is present, "required" would be sufficient (When SELinux +# is disabled, this returns success.) + +# Can't change password +# Unlock keyring when user no passwd login +-session optional pam_gnome_keyring.so auto_start +-session optional pam_deepin_keyring.so +-session optional pam_kwallet5.so auto_start +password required pam_deny.so + +@include common-password diff --git a/services/debian.ddm-sysuser.conf.in b/services/debian.ddm-sysuser.conf.in index 4bf82a4..b82c9ce 100644 --- a/services/debian.ddm-sysuser.conf.in +++ b/services/debian.ddm-sysuser.conf.in @@ -1,6 +1,7 @@ #Type Name ID GECOS Home directory Shell u dde - "DDM Greeter Account" ${STATE_DIR} - g dde - +g nopasswdlogin - m dde dde m dde video m dde render diff --git a/services/debian.ddm.pam b/services/debian.ddm.pam index bbf4019..ddc5136 100644 --- a/services/debian.ddm.pam +++ b/services/debian.ddm.pam @@ -4,7 +4,9 @@ auth requisite pam_nologin.so auth required pam_succeed_if.so user != root quiet_success -# auth sufficient pam_succeed_if.so user ingroup nopasswdlogin +# Allow members of the nopasswdlogin group to log in without a password +auth sufficient pam_succeed_if.so user ingroup nopasswdlogin + @include common-auth # gnome_keyring breaks QProcess -auth optional pam_gnome_keyring.so diff --git a/src/common/Configuration.h b/src/common/Configuration.h index b258980..da8c864 100644 --- a/src/common/Configuration.h +++ b/src/common/Configuration.h @@ -73,6 +73,13 @@ namespace DDM { Entry(RememberLastUser, bool, true, _S("Remember the last successfully logged in user")); Entry(RememberLastSession, bool, true, _S("Remember the session of the last successfully logged in user")); ); + + Section(Autologin, + Entry(User, QString, QString(), _S("User to log in automatically (empty disables autologin)")); + Entry(Session, QString, QString(), _S("Session to load for automatic login (overrides the last used session)")); + // TODO: InBackground is not implemented yet. + // Entry(InBackground, bool, false, _S("Load the automatic login session in the background without activating it")); + ); ); Config(StateConfig, []()->QString{auto tmp = getpwnam("ddm"); return tmp ? QString::fromLocal8Bit(tmp->pw_dir) : QStringLiteral(STATE_DIR);}().append(QStringLiteral("/state.conf")), QString(), QString(), diff --git a/src/common/Messages.h b/src/common/Messages.h index c314b05..61226e9 100644 --- a/src/common/Messages.h +++ b/src/common/Messages.h @@ -51,6 +51,7 @@ namespace DDM { UserActivateMessage, SwitchToGreeter, UserLoggedIn, + ShowGreeter, }; enum Capability { diff --git a/src/daemon/Auth.cpp b/src/daemon/Auth.cpp index ecb482b..ee467a4 100644 --- a/src/daemon/Auth.cpp +++ b/src/daemon/Auth.cpp @@ -241,7 +241,7 @@ namespace DDM { Q_ASSERT(!user.isEmpty()); qInfo() << "[Auth] Starting..."; - d->ret = pam_start("ddm", user.toLocal8Bit().constData(), &d->conv, &d->handle); + d->ret = pam_start(pamService.toLocal8Bit().constData(), user.toLocal8Bit().constData(), &d->conv, &d->handle); CHECK_RET_AUTH qInfo() << "[Auth] Authenticating user" << user; diff --git a/src/daemon/Auth.h b/src/daemon/Auth.h index e050ab1..f4965dc 100644 --- a/src/daemon/Auth.h +++ b/src/daemon/Auth.h @@ -30,6 +30,10 @@ namespace DDM { /** Username. Must be set before authenticate() */ QString user{}; + /** PAM service to use for authentication (defaults to "ddm"). Use + * "ddm-autologin" to authenticate without a password. */ + QString pamService{ QStringLiteral("ddm") }; + /** Display sever type of the session. Must be set before startUserProcess() */ Display::DisplayServerType type{}; diff --git a/src/daemon/Display.cpp b/src/daemon/Display.cpp index 0bc69e0..ddea869 100644 --- a/src/daemon/Display.cpp +++ b/src/daemon/Display.cpp @@ -233,10 +233,25 @@ namespace DDM { void Display::connected(QLocalSocket *socket) { // send logged in users (for possible crash recovery) SocketWriter writer(socket); + bool hasActiveUser = false; for (Auth *auth : std::as_const(auths)) { - if (auth->sessionOpened) + if (auth->sessionOpened) { + hasActiveUser = true; writer << quint32(DaemonMessages::UserLoggedIn) << auth->user << auth->xdgSessionId; + } + } + + // Autologin is only tried on the first connection after boot. + if (!m_connectedOnce) { + m_connectedOnce = true; + if (!hasActiveUser && !mainConfig.Autologin.User.get().isEmpty()) { + if (startAutologin()) + return; + } } + + SocketWriter showGreeter(socket); + showGreeter << quint32(DaemonMessages::ShowGreeter); } void Display::login(QLocalSocket *socket, @@ -270,34 +285,6 @@ namespace DDM { if (insertedAuth) auths << auth; - // sanity check - if (!session.isValid()) { - qCritical() << "Invalid session" << session.fileName(); - if (insertedAuth) { - auths.removeAll(auth); - delete auth; - } - return; - } - if (session.xdgSessionType().isEmpty()) { - qCritical() << "Failed to find XDG session type for session" << session.fileName(); - if (insertedAuth) { - auths.removeAll(auth); - delete auth; - } - return; - } - if (session.exec().isEmpty()) { - qCritical() << "Failed to find command for session" << session.fileName(); - if (insertedAuth) { - auths.removeAll(auth); - delete auth; - } - return; - } - - const QString sessionId = QStringLiteral("Session%1").arg(daemonApp->newSessionId()); - // Run password check if (!auth->authenticate(password.toLocal8Bit())) { if (insertedAuth) { @@ -320,6 +307,32 @@ namespace DDM { stateConfig.Last.Session.setDefault(); stateConfig.save(); + if (!startUserSession(auth, session)) + Q_EMIT loginFailed(socket, user); + } + + bool Display::startUserSession(Auth *auth, const Session &session) { + // sanity check + if (!session.isValid()) { + qCritical() << "Invalid session" << session.fileName(); + auths.removeAll(auth); + delete auth; + return false; + } + if (session.xdgSessionType().isEmpty()) { + qCritical() << "Failed to find XDG session type for session" << session.fileName(); + auths.removeAll(auth); + delete auth; + return false; + } + if (session.exec().isEmpty()) { + qCritical() << "Failed to find command for session" << session.fileName(); + auths.removeAll(auth); + delete auth; + return false; + } + + const QString sessionId = QStringLiteral("Session%1").arg(daemonApp->newSessionId()); auth->sessionId = sessionId; // Special preparation for each display server type @@ -331,12 +344,12 @@ namespace DDM { if (session.isSingleMode()) { auth->type = Treeland; const int ownerPid = daemonApp->treelandConnector()->mainPid(); - auth->tty = daemonApp->seatdControl()->createGroupVt(ownerPid, user, sessionId); + auth->tty = daemonApp->seatdControl()->createGroupVt(ownerPid, auth->user, sessionId); if (auth->tty <= 0) { qCritical() << "Failed to allocate grouped VT for Treeland user session"; auths.removeAll(auth); delete auth; - return; + return false; } } else if (session.xdgSessionType() == QLatin1String("x11")) { auth->type = X11; @@ -350,10 +363,10 @@ namespace DDM { qCritical() << "Failed to allocate VT for user session"; auths.removeAll(auth); delete auth; - return; + return false; } - qInfo() << "Authentication succeeded for user" << user << ", opening session" + qInfo() << "Authentication succeeded for user" << auth->user << ", opening session" << session.fileName() << ", command:" << session.exec() << ", VT:" << auth->tty; // Prepare session environment @@ -391,7 +404,7 @@ namespace DDM { m_x11Server = nullptr; auths.removeAll(auth); delete auth; - return; + return false; } m_x11Server->setupDisplay(); auth->display = m_x11Server->display; @@ -408,12 +421,12 @@ namespace DDM { int xdgSessionId = auth->openSession(session.exec(), env, cookie); if (xdgSessionId <= 0) { - qCritical() << "Failed to open logind session for user" << user; + qCritical() << "Failed to open logind session for user" << auth->user; if (auth->type == Treeland) daemonApp->seatdControl()->destroyGroupVt(auth->tty); auths.removeAll(auth); delete auth; - return; + return false; } connect(auth, &Auth::sessionFinished, this, [this, auth]() { @@ -424,12 +437,74 @@ namespace DDM { daemonApp->seatdControl()->destroyGroupVt(auth->tty); delete auth; }); - daemonApp->displayManager()->AddSession(sessionId, name, user, auth->tty); + daemonApp->displayManager()->AddSession(sessionId, name, auth->user, auth->tty); daemonApp->displayManager()->setLastSession(sessionId); if (auth->type == Treeland) - activateSession(user, xdgSessionId); - qInfo() << "Successfully logged in user" << user; + activateSession(auth->user, xdgSessionId); + qInfo() << "Successfully logged in user" << auth->user; + return true; + } + + bool Display::startAutologin() { + const QString user = mainConfig.Autologin.User.get(); + if (user.isEmpty()) + return false; + + if (user == QLatin1String("dde")) { + qWarning() << "Autologin user must not be the greeter user"; + return false; + } + + qInfo() << "Starting automatic login for user" << user; + + // Determine session: autologin-session, or last used session + QString sessionName = mainConfig.Autologin.Session.get(); + if (sessionName.isEmpty()) + sessionName = stateConfig.Last.Session.get(); + if (sessionName.isEmpty()) { + qWarning() << "No session configured for autologin, keeping greeter"; + return false; + } + + Session session(Session::WaylandSession, sessionName); + if (!session.isValid()) + session = Session(Session::X11Session, sessionName); + if (!session.isValid()) { + qCritical() << "Failed to load autologin session" << sessionName; + return false; + } + + // Create Auth with the dedicated autologin PAM service + Auth *auth = new Auth(this, user); + auth->pamService = QStringLiteral("ddm-autologin"); + auths << auth; + + if (!auth->authenticate(QByteArray())) { + qWarning() << "Autologin authentication failed for user" << user; + auths.removeAll(auth); + delete auth; + return false; + } + + // save last user and last session + DaemonApp::instance()->displayManager()->setLastActivatedUser(user); + if (mainConfig.Users.RememberLastUser.get()) + stateConfig.Last.User.set(user); + else + stateConfig.Last.User.setDefault(); + if (mainConfig.Users.RememberLastSession.get()) + stateConfig.Last.Session.set(session.fileName()); + else + stateConfig.Last.Session.setDefault(); + stateConfig.save(); + + if (!startUserSession(auth, session)) { + qWarning() << "Failed to start automatic login session for user" << user; + return false; + } + + return true; } void Display::logout([[maybe_unused]] QLocalSocket *socket, int id) { diff --git a/src/daemon/Display.h b/src/daemon/Display.h index f341f10..4bcd19f 100644 --- a/src/daemon/Display.h +++ b/src/daemon/Display.h @@ -119,9 +119,29 @@ namespace DDM { void loginFailed(QLocalSocket *socket, const QString &user); private: + /** + * Start a user session that has already been authenticated. + * Handles VT allocation, display server startup and logind + * registration. On failure the auth is removed and deleted. + * + * @param auth Authenticated Auth object + * @param session Session to start + * @return true on success, false on failure + */ + bool startUserSession(Auth *auth, const Session &session); + + /** + * Start automatic login for the configured user using the + * "ddm-autologin" PAM service. No-op if autologin is disabled. + */ + bool startAutologin(); + /** Indicates whether the display is started */ bool m_started{ false }; + /** Indicates whether the greeter connected at least once (since start) */ + bool m_connectedOnce{ false }; + /** Treeland display server */ TreelandDisplayServer *m_treeland{ nullptr };