]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
pam: introduce multiple per-user "areas", i.e. "sub-home-directories" of sorts
authorLennart Poettering <lennart@poettering.net>
Fri, 3 Jan 2025 20:10:25 +0000 (21:10 +0100)
committerLennart Poettering <lennart@poettering.net>
Thu, 6 Feb 2025 08:23:49 +0000 (09:23 +0100)
src/home/pam_systemd_home.c
src/login/pam_systemd.c

index 95f719d91206b2a21e5d176702cb83ed4a2af279..9e54971d23c84388ac0b9a9b4fea0fc5fb8c890b 100644 (file)
@@ -1,6 +1,7 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
 #include <security/pam_ext.h>
+#include <security/pam_misc.h>
 #include <security/pam_modules.h>
 
 #include "sd-bus.h"
@@ -15,6 +16,7 @@
 #include "memory-util.h"
 #include "pam-util.h"
 #include "parse-util.h"
+#include "path-util.h"
 #include "strv.h"
 #include "user-record-util.h"
 #include "user-record.h"
@@ -114,6 +116,20 @@ static int acquire_user_record(
                         return pam_syslog_pam_error(handle, LOG_ERR, PAM_SERVICE_ERR, "User name not set.");
         }
 
+        /* Possibly split out the area name */
+        _cleanup_free_ char *username_without_area = NULL, *area = NULL;
+        const char *carea = strrchr(username, '%');
+        if (carea && (filename_is_valid(carea + 1) || isempty(carea + 1))) {
+                username_without_area = strndup(username, carea - username);
+                if (!username_without_area)
+                        return pam_log_oom(handle);
+
+                username = username_without_area;
+                area = strdup(carea + 1);
+                if (!area)
+                        return pam_log_oom(handle);
+        }
+
         /* Let's bypass all IPC complexity for the two user names we know for sure we don't manage, and for
          * user names we don't consider valid. */
         if (STR_IN_SET(username, "root", NOBODY_USER_NAME) || !valid_user_group_name(username, 0))
@@ -242,6 +258,14 @@ static int acquire_user_record(
                 TAKE_PTR(json_copy);
         }
 
+        /* Let's store the area we parsed out of the name in an env var, so that pam_systemd later can honour it. */
+        if (area) {
+                r = pam_misc_setenv(handle, "XDG_AREA", area, /* readonly= */ 0);
+                if (r != PAM_SUCCESS)
+                        return pam_syslog_pam_error(handle, LOG_ERR, r,
+                                                    "Failed to set environment variable $XDG_AREA to '%s': @PAMERR@", area);
+        }
+
         if (ret_record)
                 *ret_record = TAKE_PTR(ur);
 
index cf46eba1952f3aa651be7ce258065e60c850d273..eca3283da8e65dfbf0806e4884bccb32b3e60c21 100644 (file)
@@ -30,6 +30,7 @@
 #include "cap-list.h"
 #include "capability-util.h"
 #include "cgroup-setup.h"
+#include "chase.h"
 #include "creds-util.h"
 #include "devnum-util.h"
 #include "errno-util.h"
@@ -120,6 +121,7 @@ static int parse_argv(
                 const char **class,
                 const char **type,
                 const char **desktop,
+                const char **area,
                 bool *debug,
                 uint64_t *default_capability_bounding_set,
                 uint64_t *default_capability_ambient_set) {
@@ -145,6 +147,13 @@ static int parse_argv(
                         if (desktop)
                                 *desktop = p;
 
+                } else if ((p = startswith(argv[i], "area="))) {
+
+                        if (!isempty(p) && !filename_is_valid(p))
+                                pam_syslog(handle, LOG_WARNING, "Area name specified among PAM module parameters is not valid, ignoring: %m");
+                        else if (area)
+                                *area = p;
+
                 } else if (streq(argv[i], "debug")) {
                         if (debug)
                                 *debug = true;
@@ -854,6 +863,7 @@ typedef struct SessionContext {
         const char *cpu_weight;
         const char *io_weight;
         const char *runtime_max_sec;
+        const char *area;
         bool incomplete;
 } SessionContext;
 
@@ -1043,6 +1053,14 @@ static void session_context_mangle(
         }
 
         c->remote = !isempty(c->remote_host) && !is_localhost(c->remote_host);
+
+        if (!c->area)
+                c->area = ur->default_area;
+
+        if (!isempty(c->area) && !filename_is_valid(c->area)) {
+                pam_syslog_pam_error(handle, LOG_WARNING, 0, "Specified area '%s' is not a valid filename, ignoring area request.", c->area);
+                c->area = NULL;
+        }
 }
 
 static bool can_use_varlink(const SessionContext *c) {
@@ -1374,6 +1392,73 @@ static int import_shell_credentials(pam_handle_t *handle) {
         return PAM_SUCCESS;
 }
 
+static int update_home_env(
+                pam_handle_t *handle,
+                UserRecord *ur,
+                const char *area,
+                bool debug) {
+
+        int r;
+
+        assert(handle);
+        assert(ur);
+
+        const char *h = ASSERT_PTR(user_record_home_directory(ur));
+
+        /* If an empty area string is specified, this means an explicit: do not use the area logic, normalize this here */
+        area = empty_to_null(area);
+
+        _cleanup_free_ char *ha = NULL;
+        if (area) {
+                _cleanup_free_ char *j = path_join(h, "Areas", area);
+                if (!j)
+                        return pam_log_oom(handle);
+
+                _cleanup_close_ int fd = -EBADF;
+                r = chase(j, /* root= */ NULL, CHASE_MUST_BE_DIRECTORY, &ha, &fd);
+                if (r < 0) {
+                        /* Log the precise error */
+                        pam_syslog_errno(handle, LOG_WARNING, r, "Path '%s' of requested user area '%s' is not accessible, reverting to regular home directory: %m", j, area);
+
+                        /* Also tell the user directly at login, but a bit more vague */
+                        pam_info(handle, "Path '%s' of requested user area '%s' is not accessible, reverting to regular home directory.", j, area);
+                        area = NULL;
+                } else {
+                        /* Validate that the target is definitely owned by user */
+                        struct stat st;
+                        if (fstat(fd, &st) < 0)
+                                return pam_syslog_errno(handle, LOG_ERR, errno, "Unable to fstat() target area directory '%s': %m", ha);
+
+                        if (st.st_uid != ur->uid) {
+                                pam_syslog(handle, LOG_ERR, "Path '%s' of requested user area '%s' is not owned by user, reverting to regular home directory.", ha, area);
+
+                                /* Also tell the user directly at login. */
+                                pam_info(handle, "Path '%s' of requested user area '%s' is not owned by user, reverting to regular home directory.", ha, area);
+                                area = NULL;
+                        } else {
+                                pam_debug_syslog(handle, debug, "Area '%s' selected, setting $HOME to '%s': %m", area, ha);
+                                h = ha;
+                        }
+                }
+        }
+
+        if (area) {
+                r = update_environment(handle, "XDG_AREA", area);
+                if (r != PAM_SUCCESS)
+                        return r;
+        } else if (pam_getenv(handle, "XDG_AREA")) {
+                /* Unset the $XDG_AREA variable if set. Note that pam_putenv() would log nastily behind our
+                 * back if we call it without $XDG_AREA actually being set. Hence we check explicitly if it's
+                 * set before. */
+                r = pam_putenv(handle, "XDG_AREA");
+                if (!IN_SET(r, PAM_SUCCESS, PAM_BAD_ITEM))
+                        pam_syslog_pam_error(handle, LOG_WARNING, r,
+                                             "Failed to unset XDG_AREA environment variable, ignoring: @PAMERR@");
+        }
+
+        return update_environment(handle, "HOME", h);
+}
+
 _public_ PAM_EXTERN int pam_sm_open_session(
                 pam_handle_t *handle,
                 int flags,
@@ -1386,13 +1471,14 @@ _public_ PAM_EXTERN int pam_sm_open_session(
         pam_log_setup();
 
         uint64_t default_capability_bounding_set = UINT64_MAX, default_capability_ambient_set = UINT64_MAX;
-        const char *class_pam = NULL, *type_pam = NULL, *desktop_pam = NULL;
+        const char *class_pam = NULL, *type_pam = NULL, *desktop_pam = NULL, *area_pam = NULL;
         bool debug = false;
         if (parse_argv(handle,
                        argc, argv,
                        &class_pam,
                        &type_pam,
                        &desktop_pam,
+                       &area_pam,
                        &debug,
                        &default_capability_bounding_set,
                        &default_capability_ambient_set) < 0)
@@ -1421,6 +1507,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
         c.type = getenv_harder(handle, "XDG_SESSION_TYPE", type_pam);
         c.class = getenv_harder(handle, "XDG_SESSION_CLASS", class_pam);
         c.desktop = getenv_harder(handle, "XDG_SESSION_DESKTOP", desktop_pam);
+        c.area = getenv_harder(handle, "XDG_AREA", area_pam);
         c.incomplete = getenv_harder_bool(handle, "XDG_SESSION_INCOMPLETE", false);
 
         r = pam_get_data_many(
@@ -1444,6 +1531,10 @@ _public_ PAM_EXTERN int pam_sm_open_session(
         if (r != PAM_SUCCESS)
                 return r;
 
+        r = update_home_env(handle, ur, c.area, debug);
+        if (r != PAM_SUCCESS)
+                return r;
+
         if (default_capability_ambient_set == UINT64_MAX)
                 default_capability_ambient_set = pick_default_capability_ambient_set(ur, c.service, c.seat);
 
@@ -1469,6 +1560,7 @@ _public_ PAM_EXTERN int pam_sm_close_session(
                        /* class= */ NULL,
                        /* type= */ NULL,
                        /* desktop= */ NULL,
+                       /* area= */ NULL,
                        &debug,
                        /* default_capability_bounding_set */ NULL,
                        /* default_capability_ambient_set= */ NULL) < 0)