From: Lennart Poettering Date: Fri, 3 Jan 2025 20:10:25 +0000 (+0100) Subject: pam: introduce multiple per-user "areas", i.e. "sub-home-directories" of sorts X-Git-Tag: v258-rc1~1410^2~4 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=c747c04146e2a3a53e3c394e912d80892fc3107b;p=thirdparty%2Fsystemd.git pam: introduce multiple per-user "areas", i.e. "sub-home-directories" of sorts --- diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c index 95f719d9120..9e54971d23c 100644 --- a/src/home/pam_systemd_home.c +++ b/src/home/pam_systemd_home.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #include +#include #include #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); diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c index cf46eba1952..eca3283da8e 100644 --- a/src/login/pam_systemd.c +++ b/src/login/pam_systemd.c @@ -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)