homed: introduce "area" concept (i.e. secondary home directories stored below the primary one, of which one can pick one at login) (#36149)

This adds a new concept to homed/pam_systemd/pam_systemd_homed: "areas"
are secondary home dirs you can have inside your primary home dir, below
an `~/Areas/` hierarchy. You can log into these secondary dirs if you
specify "user%area" as user at login time.

This is quite useful for maintaining multiple sets of user resources
within the same user account with the same access privileges. The
intended usecase for me is utimately WSL-like stuff: you start a local
unpriv container which gets the host's home dir mounted in and fully
accessible, without this also meaning that the user account gets the
full set of settings and so on propagated down.

Codewise the concept is really simple: whenever an area name like
"foobar" is specified we simply change a $HOME of /home/lennart into
/home/lennart/Areas/foobar. In a way this PR adds more docs than code
for all this.

This also makes this feature directly accessible via "run0 -a foobar"
(for opening a new session in the 'foobar' area).

To be truly fun to use on text (i.e. getty) logins, a util-linux version
containing https://github.com/util-linux/util-linux/pull/3354 is best,
since otherwise $HOME is updated to /home/lennart/Areas/foobar, but the
cwd is still at /home/lennart.
This commit is contained in:
Lennart Poettering
2025-02-06 12:47:04 +01:00
committed by GitHub
16 changed files with 438 additions and 29 deletions

View File

@@ -632,6 +632,16 @@ default limit.
`devShmLimit`, `devShmLimitScale` → Similar to the previous two, but apply to
`/dev/shm/` rather than `/tmp/`.
`defaultArea` → The default home directory "area" to enter after logging
in. Areas are named subdirectories below `~/Areas/` of the user, and can be used
to maintain separate secondary home directories within the primary home
directory of the user. Typically, a home directory "area" can be specified at
login time, but if that's not done, and `defaultArea` is set, this area is
selected. The value must be a string that qualifies as a valid filename. After
login, the `$HOME` environment variable will point to `~/Areas/` of the user,
suffixed by the selected area name, and `$XDG_AREA` will be set to the area
string (unprefixed).
`privileged` → An object, which contains the fields of the `privileged` section
of the user record, see below.

View File

@@ -416,6 +416,24 @@
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
<varlistentry>
<term><option>--default-area=<replaceable>AREA</replaceable></option></term>
<listitem><para>Takes a string identifying a home directory "area" to use as default. Areas are
secondary home directories within the primary home directory of a user. When logging in a user can
specify the area they wish to log into, which ensures that the <varname>$HOME</varname> environment
variable is set to <filename>~/Areas/</filename> suffixed by the area name.</para>
<para>For details on the area concept see
<citerefentry><refentrytitle>pam_systemd_home</refentrytitle><manvolnum>8</manvolnum></citerefentry>. Note
that this option just defines the default, which can be overridden at login time.</para>
<para>When this option is specified with an empty string as value any previously declared default area
is removed from the user record.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@@ -192,6 +192,39 @@
<xi:include href="version-info.xml" xpointer="v240"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>area=</varname></term>
<listitem><para>Takes a filename as parameter. If specified and the user logs into their account the
<varname>$HOME</varname> environment variable will be set to <filename>~/Areas/</filename> suffixed
by the specified string, but only if that directory exists. Moreover, the
<varname>$XDG_AREA</varname> variable will be set to the (unprefixed) parameter.</para>
<para>This functionality may be used to maintain multiple separate secondary home directories within
the primary home directory of the user. Typically, the area to log into is specified while logging
in, if the account permits that (accounts provided by
<citerefentry><refentrytitle>pam_systemd_home</refentrytitle><manvolnum>8</manvolnum></citerefentry>
do), but this parameter may be used to define a default if that's not provided.</para>
<para>Note that this only adjusts <varname>$HOME</varname> during login, it does not affect the
otherwise reported home directory of the user. Specifically this means that <citerefentry
project='man-pages'><refentrytitle>sshd</refentrytitle><manvolnum>8</manvolnum></citerefentry> will
continue to look for SSH keys of the user only in the primary home directory of the user, not in any
of their areas.</para>
<para>Note that the default area to log into can also be configured as part of the user account. The
area specified via <varname>area=</varname> overrides the default area configured there. Also note
that if the area is specified explicitly by the user at login time, it overrides both. Also note that
setting this parameter to an empty string has the effect of undoing the effect of any default area
configured as part of the user record, i.e. ensuring the user logs into the primary home directory of
their account.</para>
<para>For details on the area concept see
<citerefentry><refentrytitle>pam_systemd_home</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>default-capability-bounding-set=</varname></term>
<term><varname>default-capability-ambient-set=</varname></term>
@@ -348,6 +381,16 @@
<xi:include href="version-info.xml" xpointer="v209"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>$XDG_AREA</varname></term>
<listitem><para>If an area (secondary home directories of the user, within the primary home
directory) to log into has been selected this variable is set to the area name (without any path
prefix). It is otherwise unset. For details about areas, see above.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
</variablelist>
<para>If not set, <command>pam_systemd</command> will initialize

View File

@@ -99,6 +99,53 @@
</variablelist>
</refsect1>
<refsect1>
<title>Home Area Support</title>
<para>Home directories managed by
<citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
support multiple home "areas", which are additional secondary home directories of the user within the
primary home directory. An example: at login time if a user <literal>lennart</literal> with a home
directory of <filename>/home/lennart</filename> specifies <literal>lennart%versuch1</literal> as account
name during login, then <command>pam_systemd_home</command> will execute a login into
<literal>lennart</literal> but ensure that the <varname>$HOME</varname> variable is set to
<filename>/home/lennart/Areas/versuch1</filename> instead of the usual
<filename>/home/lennart</filename>.</para>
<para>This is particularly useful when sharing the same home directory between multiple systems (for
example between a host and a VM), with the desire to share the home directory to a large degree, but
still have separate session configuration in place.</para>
<para>Note that the default area to log into can also be encoded in the user record, and it can be
specified among
<citerefentry><refentrytitle>pam_systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>
configuration parameters. However, an explicit area specified at login time (via the <literal>%</literal>
described above) overrides any such default. Also note that simply suffixing an account with
<literal>%</literal> at login time (i.e. specifying an empty area name) has the effect of ensuring a
login into the primary home directory, overriding any default area configuration via the user record or
PAM.</para>
<para>Note that not all login mechanisms are compatible with the <literal>%</literal> syntax at login
time. Most notably <citerefentry
project='man-pages'><refentrytitle>ssh</refentrytitle><manvolnum>8</manvolnum></citerefentry> is not.</para>
<para>Note that the area directory to log into must exist for the area specification to be respected. If
an area is specified during login via the <literal>%</literal> logic (or the other mentioned mechanisms)
and it does not actually exist the request will be ignored, and the user will log into the primary home
directory instead.</para>
<para>Typically, in order to make use of the mechanism set up an area first, like this:</para>
<programlisting>lennart@zeta$ mkdir -p ~/Areas
lennart@zeta$ cp -av /etc/skel ~/Areas/versuch1</programlisting>
<para>This should be enough to log into the newly created area, either via a regular terminal (using
<literal>lennart%versuch1</literal> when prompted for a user name), or via
<citerefentry><refentrytitle>run0</refentrytitle><manvolnum>1</manvolnum></citerefentry>:</para>
<programlisting>lennart@zeta$ run0 -a versuch1</programlisting>
</refsect1>
<refsect1>
<title>Module Types Provided</title>

View File

@@ -138,7 +138,9 @@
<term><option>--group=</option></term>
<term><option>-g</option></term>
<listitem><para>Switches to the specified user/group instead of root.</para>
<listitem><para>Switches to the specified user/group. If not specified defaults to
<literal>root</literal>, unless <option>--area=</option> is used (see below), in which case this
defaults to the invoking user.</para>
<xi:include href="version-info.xml" xpointer="v256"/>
</listitem>
@@ -231,7 +233,7 @@
default if the target user is <literal>root</literal> or a system user the per-user service manager
is not activated as effect of the <command>run0</command> invocation, otherwise it is.</para>
<para>This ultimately controls the <varname>$XDG_SESSION_CLASS</varname> variable
<para>This ultimately controls the <varname>$XDG_SESSION_CLASS</varname> environment variable
<citerefentry><refentrytitle>pam_systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>
respects.</para>
@@ -239,6 +241,31 @@
</listitem>
</varlistentry>
<varlistentry>
<term><option>-a <replaceable>AREA</replaceable></option></term>
<term><option>--area=<replaceable>AREA</replaceable></option></term>
<listitem><para>Controls the "area" of the target account to log into. Areas are secondary home
directories within the primary home directory of the target user, i.e. logging into area
<literal>foobar</literal> of an account translates to <varname>$HOME</varname> being set to
<filename>~/Areas/foobar</filename> on login.</para>
<para>If this option is used, the default user to transition to changes from root to the calling
user's (but <option>--user=</option> takes precedence, see above). Or in other words, just specifying
an area without a user is a mechanism to create a new session of the calling user, just with a
different area.</para>
<para>This ultimately controls the <varname>$XDG_AREA</varname> environment variable
<citerefentry><refentrytitle>pam_systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>
respects.</para>
<para>For details on the area concept see
<citerefentry><refentrytitle>pam_systemd_home</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--machine=</option></term>

View File

@@ -3619,7 +3619,8 @@ static int apply_working_directory(
const ExecContext *context,
const ExecParameters *params,
ExecRuntime *runtime,
const char *home) {
const char *pwent_home,
const char *const *env) {
const char *wd;
int r;
@@ -3627,10 +3628,15 @@ static int apply_working_directory(
assert(context);
if (context->working_directory_home) {
if (!home)
return -ENXIO;
/* Preferably use the data from $HOME, in case it was updated by a PAM module */
wd = strv_env_get((char**) env, "HOME");
if (!wd) {
/* If that's not available, use the data from the struct passwd entry: */
if (!pwent_home)
return -ENXIO;
wd = home;
wd = pwent_home;
}
} else
wd = empty_to_root(context->working_directory);
@@ -4393,7 +4399,7 @@ int exec_invoke(
_cleanup_free_ gid_t *supplementary_gids = NULL;
const char *username = NULL, *groupname = NULL;
_cleanup_free_ char *home_buffer = NULL, *memory_pressure_path = NULL, *own_user = NULL;
const char *home = NULL, *shell = NULL;
const char *pwent_home = NULL, *shell = NULL;
char **final_argv = NULL;
dev_t journal_stream_dev = 0;
ino_t journal_stream_ino = 0;
@@ -4645,7 +4651,7 @@ int exec_invoke(
u = NULL;
if (u) {
r = get_fixed_user(u, &username, &uid, &gid, &home, &shell);
r = get_fixed_user(u, &username, &uid, &gid, &pwent_home, &shell);
if (r < 0) {
*exit_status = EXIT_USER;
return log_exec_error_errno(context, params, r, "Failed to determine user credentials: %m");
@@ -4677,7 +4683,7 @@ int exec_invoke(
params->user_lookup_fd = safe_close(params->user_lookup_fd);
r = acquire_home(context, &home, &home_buffer);
r = acquire_home(context, &pwent_home, &home_buffer);
if (r < 0) {
*exit_status = EXIT_CHDIR;
return log_exec_error_errno(context, params, r, "Failed to determine $HOME for the invoking user: %m");
@@ -4983,7 +4989,7 @@ int exec_invoke(
params,
cgroup_context,
n_fds,
home,
pwent_home,
username,
shell,
journal_stream_dev,
@@ -5505,7 +5511,7 @@ int exec_invoke(
* running this service might have the correct privilege to change to the working directory. Also, it
* is absolutely 💣 crucial 💣 we applied all mount namespacing rearrangements before this, so that
* the cwd cannot be used to pin directories outside of the sandbox. */
r = apply_working_directory(context, params, runtime, home);
r = apply_working_directory(context, params, runtime, pwent_home, (const char* const*) accum_env);
if (r < 0) {
*exit_status = EXIT_CHDIR;
return log_exec_error_errno(context, params, r, "Changing to the requested working directory failed: %m");

View File

@@ -2774,6 +2774,7 @@ static int help(int argc, char *argv[], void *userdata) {
" --setenv=VARIABLE[=VALUE] Set an environment variable at log-in\n"
" --timezone=TIMEZONE Set a time-zone\n"
" --language=LOCALE Set preferred languages\n"
" --default-area=AREA Select default area\n"
"\n%4$sAuthentication User Record Properties:%5$s\n"
" --ssh-authorized-keys=KEYS\n"
" Specify SSH public keys\n"
@@ -2984,6 +2985,7 @@ static int parse_argv(int argc, char *argv[]) {
ARG_LOGIN_BACKGROUND,
ARG_TMP_LIMIT,
ARG_DEV_SHM_LIMIT,
ARG_DEFAULT_AREA,
};
static const struct option options[] = {
@@ -3086,6 +3088,7 @@ static int parse_argv(int argc, char *argv[]) {
{ "login-background", required_argument, NULL, ARG_LOGIN_BACKGROUND },
{ "tmp-limit", required_argument, NULL, ARG_TMP_LIMIT },
{ "dev-shm-limit", required_argument, NULL, ARG_DEV_SHM_LIMIT },
{ "default-area", required_argument, NULL, ARG_DEFAULT_AREA },
{}
};
@@ -4569,6 +4572,24 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
case ARG_DEFAULT_AREA:
if (isempty(optarg)) {
r = drop_from_identity("defaultArea");
if (r < 0)
return r;
break;
}
if (!filename_is_valid(optarg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parameter for default area field not valid: %s", optarg);
r = sd_json_variant_set_field_string(&arg_identity_extra, "defaultArea", optarg);
if (r < 0)
return log_error_errno(r, "Failed to set default area field: %m");
break;
case '?':
return -EINVAL;

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

@@ -166,6 +166,28 @@ int json_dispatch_path(const char *name, sd_json_variant *variant, sd_json_dispa
return 0;
}
int json_dispatch_filename(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
char **s = ASSERT_PTR(userdata);
const char *n;
if (sd_json_variant_is_null(variant)) {
*s = mfree(*s);
return 0;
}
if (!sd_json_variant_is_string(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a string.", strna(name));
n = sd_json_variant_string(variant);
if (!filename_is_valid(n))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid file name.", strna(name));
if (free_and_strdup(s, n) < 0)
return json_log_oom(variant, flags);
return 0;
}
int json_variant_new_pidref(sd_json_variant **ret, PidRef *pidref) {
sd_id128_t boot_id = SD_ID128_NULL;
int r;

View File

@@ -114,6 +114,7 @@ int json_dispatch_const_user_group_name(const char *name, sd_json_variant *varia
int json_dispatch_in_addr(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_const_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_filename(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_pidref(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_devnum(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_ifindex(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);

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)

View File

@@ -91,6 +91,7 @@ static char *arg_background = NULL;
static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
static char *arg_shell_prompt_prefix = NULL;
static int arg_lightweight = -1;
static char *arg_area = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_description, freep);
STATIC_DESTRUCTOR_REGISTER(arg_environment, strv_freep);
@@ -103,6 +104,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_exec_path, freep);
STATIC_DESTRUCTOR_REGISTER(arg_background, freep);
STATIC_DESTRUCTOR_REGISTER(arg_shell_prompt_prefix, freep);
STATIC_DESTRUCTOR_REGISTER(arg_area, freep);
static int help(void) {
_cleanup_free_ char *link = NULL;
@@ -206,6 +208,7 @@ static int help_sudo_mode(void) {
" --shell-prompt-prefix=PREFIX Set $SHELL_PROMPT_PREFIX\n"
" --lightweight=BOOLEAN Control whether to register a session with service manager\n"
" or without\n"
" -a --area=AREA Home area to log into\n"
"\nSee the %s for details.\n",
program_invocation_short_name,
ansi_highlight(),
@@ -824,6 +827,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
{ "pipe", no_argument, NULL, ARG_PIPE },
{ "shell-prompt-prefix", required_argument, NULL, ARG_SHELL_PROMPT_PREFIX },
{ "lightweight", required_argument, NULL, ARG_LIGHTWEIGHT },
{ "area", required_argument, NULL, 'a' },
{},
};
@@ -835,7 +839,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
/* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
* that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
optind = 0;
while ((c = getopt_long(argc, argv, "+hVu:g:D:", options, NULL)) >= 0)
while ((c = getopt_long(argc, argv, "+hVu:g:D:a:", options, NULL)) >= 0)
switch (c) {
@@ -942,6 +946,17 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
return r;
break;
case 'a':
/* We allow an empty --area= specification to allow logging into the primary home directory */
if (!isempty(optarg) && !filename_is_valid(optarg))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid area name, refusing: %s", optarg);
r = free_and_strdup_warn(&arg_area, optarg);
if (r < 0)
return r;
break;
case '?':
return -EINVAL;
@@ -949,6 +964,14 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
assert_not_reached();
}
if (!arg_exec_user && arg_area) {
/* If the user specifies --area= but not --user= then consider this an area switch request,
* and default to logging into our own account */
arg_exec_user = getusername_malloc();
if (!arg_exec_user)
return log_oom();
}
if (!arg_working_directory) {
if (arg_exec_user) {
/* When switching to a specific user, also switch to its home directory. */
@@ -1075,26 +1098,39 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
return log_error_errno(r, "Failed to set $SHELL_PROMPT_PREFIX environment variable: %m");
}
/* When using run0 to acquire privileges temporarily, let's not pull in session manager by
* default. Note that pam_logind/systemd-logind doesn't distinguish between run0-style privilege
* escalation on a TTY and first class (getty-style) TTY logins (and thus gives root a per-session
* manager for interactive TTY sessions), hence let's override the logic explicitly here. We only do
* this for root though, under the assumption that if a regular user temporarily transitions into
* another regular user it's a better default that the full user environment is uniformly
* available. */
if (arg_lightweight < 0 && !strv_env_get(arg_environment, "XDG_SESSION_CLASS") && privileged_execution())
arg_lightweight = true;
if (!strv_env_get(arg_environment, "XDG_SESSION_CLASS")) {
if (arg_lightweight >= 0) {
const char *class =
arg_lightweight ? (arg_stdio == ARG_STDIO_PTY ? (privileged_execution() ? "user-early-light" : "user-light") : "background-light") :
(arg_stdio == ARG_STDIO_PTY ? (privileged_execution() ? "user-early" : "user") : "background");
/* If logging into an area, imply lightweight mode */
if (arg_lightweight < 0 && !isempty(arg_area))
arg_lightweight = true;
log_debug("Setting XDG_SESSION_CLASS to '%s'.", class);
/* When using run0 to acquire privileges temporarily, let's not pull in session manager by
* default. Note that pam_logind/systemd-logind doesn't distinguish between run0-style privilege
* escalation on a TTY and first class (getty-style) TTY logins (and thus gives root a per-session
* manager for interactive TTY sessions), hence let's override the logic explicitly here. We only do
* this for root though, under the assumption that if a regular user temporarily transitions into
* another regular user it's a better default that the full user environment is uniformly
* available. */
if (arg_lightweight < 0 && privileged_execution())
arg_lightweight = true;
r = strv_env_assign(&arg_environment, "XDG_SESSION_CLASS", class);
if (arg_lightweight >= 0) {
const char *class =
arg_lightweight ? (arg_stdio == ARG_STDIO_PTY ? (privileged_execution() ? "user-early-light" : "user-light") : "background-light") :
(arg_stdio == ARG_STDIO_PTY ? (privileged_execution() ? "user-early" : "user") : "background");
log_debug("Setting XDG_SESSION_CLASS to '%s'.", class);
r = strv_env_assign(&arg_environment, "XDG_SESSION_CLASS", class);
if (r < 0)
return log_error_errno(r, "Failed to set $XDG_SESSION_CLASS environment variable: %m");
}
}
if (arg_area) {
r = strv_env_assign(&arg_environment, "XDG_AREA", arg_area);
if (r < 0)
return log_error_errno(r, "Failed to set $XDG_SESSION_CLASS environment variable: %m");
return log_error_errno(r, "Failed to set $XDG_AREA environment variable: %m");
}
return 1;

View File

@@ -273,6 +273,9 @@ void user_record_show(UserRecord *hr, bool show_full_group_info) {
printf("\n");
}
if (hr->default_area)
printf("Default Area: %s\n", hr->default_area);
if (hr->blob_directory) {
_cleanup_free_ char **filenames = NULL;
size_t n_filenames = 0;

View File

@@ -216,6 +216,8 @@ static UserRecord* user_record_free(UserRecord *h) {
strv_free(h->self_modifiable_blobs);
strv_free(h->self_modifiable_privileged);
free(h->default_area);
sd_json_variant_unref(h->json);
return mfree(h);
@@ -1316,6 +1318,7 @@ static int dispatch_per_machine(const char *name, sd_json_variant *variant, sd_j
{ "tmpLimitScale", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_tmpfs_limit_scale, offsetof(UserRecord, tmp_limit), 0, },
{ "devShmLimit", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_tmpfs_limit, offsetof(UserRecord, dev_shm_limit), 0, },
{ "devShmLimitScale", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_tmpfs_limit_scale, offsetof(UserRecord, dev_shm_limit), 0, },
{ "defaultArea", SD_JSON_VARIANT_STRING, json_dispatch_filename, offsetof(UserRecord, default_area), 0 },
{},
};
@@ -1369,6 +1372,7 @@ static int dispatch_status(const char *name, sd_json_variant *variant, sd_json_d
{ "fallbackShell", SD_JSON_VARIANT_STRING, json_dispatch_filename_or_path, offsetof(UserRecord, fallback_shell), 0 },
{ "fallbackHomeDirectory", SD_JSON_VARIANT_STRING, json_dispatch_home_directory, offsetof(UserRecord, fallback_home_directory), 0 },
{ "useFallback", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(UserRecord, use_fallback), 0 },
{ "defaultArea", SD_JSON_VARIANT_STRING, json_dispatch_filename, offsetof(UserRecord, default_area), 0 },
{},
};
@@ -1670,6 +1674,7 @@ int user_record_load(UserRecord *h, sd_json_variant *v, UserRecordLoadFlags load
{ "tmpLimitScale", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_tmpfs_limit_scale, offsetof(UserRecord, tmp_limit), 0, },
{ "devShmLimit", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_tmpfs_limit, offsetof(UserRecord, dev_shm_limit), 0, },
{ "devShmLimitScale", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_tmpfs_limit_scale, offsetof(UserRecord, dev_shm_limit), 0, },
{ "defaultArea", SD_JSON_VARIANT_STRING, json_dispatch_filename, offsetof(UserRecord, default_area), 0 },
{ "secret", SD_JSON_VARIANT_OBJECT, dispatch_secret, 0, 0 },
{ "privileged", SD_JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
@@ -2229,6 +2234,7 @@ const char** user_record_self_modifiable_fields(UserRecord *h) {
"additionalLanguages",
"preferredSessionLauncher",
"preferredSessionType",
"defaultArea",
/* Authentication methods */
"pkcs11TokenUri",

View File

@@ -406,6 +406,8 @@ typedef struct UserRecord {
TmpfsLimit tmp_limit, dev_shm_limit;
char *default_area;
sd_json_variant *json;
} UserRecord;

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
# shellcheck disable=SC2016
set -eux
set -o pipefail
@@ -668,6 +670,55 @@ if findmnt -n -o options /tmp | grep -q usrquota ; then
homectl remove tmpfsquota
fi
NEWPASSWORD=quux homectl create subareatest --storage=subvolume -P
run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest mkdir Areas
run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest cp -av /etc/skel Areas/furb
run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest cp -av /etc/skel Areas/molb
run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest ln -s /home/srub Areas/srub
run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest ln -s /root Areas/root
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $HOME')" = "/home/subareatest"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo x$XDG_AREA')" = "x"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $HOME')" = "/home/subareatest/Areas/furb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_AREA')" = "furb"
PASSWORD=quux homectl update subareatest --default-area=molb
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $HOME')" = "/home/subareatest/Areas/molb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest sh -c 'echo $XDG_AREA')" = "molb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $HOME')" = "/home/subareatest/Areas/furb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_AREA')" = "furb"
# Install a PK rule that allows 'subareatest' user to invoke run0 without password, just for testing
cat > /usr/share/polkit-1/rules.d/subareatest.rules <<'EOF'
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.user == "subareatest") {
return polkit.Result.YES;
}
});
EOF
# Test "recursive" operation
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb sh -c 'echo $HOME')" = "/home/subareatest/Areas/molb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb sh -c 'echo $XDG_AREA')" = "molb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $HOME')" = "/home/subareatest/Areas/furb"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a molb run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a furb sh -c 'echo $XDG_AREA')" = "furb"
# Test symlinked area
mkdir -p /home/srub
chown subareatest:subareatest /home/srub
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a srub sh -c 'echo $HOME')" = "/home/srub"
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a srub sh -c 'echo $XDG_AREA')" = "srub"
# Verify that login into an area not owned by target user will be redirected to main area
test "$(run0 --property=SetCredential=pam.authtok.systemd-run0:quux -u subareatest -a root sh -c 'echo x$XDG_AREA')" = "x"
systemctl stop user@"$(id -u subareatest)".service
wait_for_state subareatest inactive
homectl remove subareatest
systemd-analyze log-level info
touch /testok