pam: introduce multiple per-user "areas", i.e. "sub-home-directories" of sorts

This commit is contained in:
Lennart Poettering
2025-01-03 21:10:25 +01:00
parent add946e834
commit c747c04146
2 changed files with 117 additions and 1 deletions

View 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);

View 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)