diff --git a/docs/USER_RECORD.md b/docs/USER_RECORD.md
index ae1173c560..24075dc1b1 100644
--- a/docs/USER_RECORD.md
+++ b/docs/USER_RECORD.md
@@ -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.
diff --git a/man/homectl.xml b/man/homectl.xml
index 51e38ee84e..abcdd88529 100644
--- a/man/homectl.xml
+++ b/man/homectl.xml
@@ -416,6 +416,24 @@
+
+
+
+
+ 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 $HOME environment
+ variable is set to ~/Areas/ suffixed by the area name.
+
+ For details on the area concept see
+ pam_systemd_home8. Note
+ that this option just defines the default, which can be overridden at login time.
+
+ When this option is specified with an empty string as value any previously declared default area
+ is removed from the user record.
+
+
+
diff --git a/man/pam_systemd.xml b/man/pam_systemd.xml
index 1093df9f82..f240cc755a 100644
--- a/man/pam_systemd.xml
+++ b/man/pam_systemd.xml
@@ -192,6 +192,39 @@
+
+ area=
+
+ Takes a filename as parameter. If specified and the user logs into their account the
+ $HOME environment variable will be set to ~/Areas/ suffixed
+ by the specified string, but only if that directory exists. Moreover, the
+ $XDG_AREA variable will be set to the (unprefixed) parameter.
+
+ 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
+ pam_systemd_home8
+ do), but this parameter may be used to define a default if that's not provided.
+
+ Note that this only adjusts $HOME during login, it does not affect the
+ otherwise reported home directory of the user. Specifically this means that sshd8 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.
+
+ Note that the default area to log into can also be configured as part of the user account. The
+ area specified via area= 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.
+
+ For details on the area concept see
+ pam_systemd_home8.
+
+
+
+
default-capability-bounding-set=default-capability-ambient-set=
@@ -348,6 +381,16 @@
+
+
+ $XDG_AREA
+
+ 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.
+
+
+ If not set, pam_systemd will initialize
diff --git a/man/pam_systemd_home.xml b/man/pam_systemd_home.xml
index 74c4363b73..ed117cfed2 100644
--- a/man/pam_systemd_home.xml
+++ b/man/pam_systemd_home.xml
@@ -99,6 +99,53 @@
+
+ Home Area Support
+
+ Home directories managed by
+ systemd-homed.service8
+ 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 lennart with a home
+ directory of /home/lennart specifies lennart%versuch1 as account
+ name during login, then pam_systemd_home will execute a login into
+ lennart but ensure that the $HOME variable is set to
+ /home/lennart/Areas/versuch1 instead of the usual
+ /home/lennart.
+
+ 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.
+
+ Note that the default area to log into can also be encoded in the user record, and it can be
+ specified among
+ pam_systemd8
+ configuration parameters. However, an explicit area specified at login time (via the %
+ described above) overrides any such default. Also note that simply suffixing an account with
+ % 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.
+
+ Note that not all login mechanisms are compatible with the % syntax at login
+ time. Most notably ssh8 is not.
+
+ 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 % 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.
+
+ Typically, in order to make use of the mechanism set up an area first, like this:
+
+ lennart@zeta$ mkdir -p ~/Areas
+lennart@zeta$ cp -av /etc/skel ~/Areas/versuch1
+
+ This should be enough to log into the newly created area, either via a regular terminal (using
+ lennart%versuch1 when prompted for a user name), or via
+ run01:
+
+ lennart@zeta$ run0 -a versuch1
+
+
Module Types Provided
diff --git a/man/run0.xml b/man/run0.xml
index 59aa6d05d9..f431358e0a 100644
--- a/man/run0.xml
+++ b/man/run0.xml
@@ -138,7 +138,9 @@
- Switches to the specified user/group instead of root.
+ Switches to the specified user/group. If not specified defaults to
+ root, unless is used (see below), in which case this
+ defaults to the invoking user.
@@ -231,7 +233,7 @@
default if the target user is root or a system user the per-user service manager
is not activated as effect of the run0 invocation, otherwise it is.
- This ultimately controls the $XDG_SESSION_CLASS variable
+ This ultimately controls the $XDG_SESSION_CLASS environment variable
pam_systemd8
respects.
@@ -239,6 +241,31 @@
+
+
+
+
+ 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
+ foobar of an account translates to $HOME being set to
+ ~/Areas/foobar on login.
+
+ If this option is used, the default user to transition to changes from root to the calling
+ user's (but 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.
+
+ This ultimately controls the $XDG_AREA environment variable
+ pam_systemd8
+ respects.
+
+ For details on the area concept see
+ pam_systemd_home8.
+
+
+
+
+
diff --git a/src/core/exec-invoke.c b/src/core/exec-invoke.c
index 24e700a289..265fe648a6 100644
--- a/src/core/exec-invoke.c
+++ b/src/core/exec-invoke.c
@@ -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");
diff --git a/src/home/homectl.c b/src/home/homectl.c
index 0c032c05d1..bfd4b3b574 100644
--- a/src/home/homectl.c
+++ b/src/home/homectl.c
@@ -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;
diff --git a/src/home/pam_systemd_home.c b/src/home/pam_systemd_home.c
index 95f719d912..9e54971d23 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/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c
index f2d306f781..aa2d7f5e64 100644
--- a/src/libsystemd/sd-json/json-util.c
+++ b/src/libsystemd/sd-json/json-util.c
@@ -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;
diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h
index 9150ad1063..7526d2bdf2 100644
--- a/src/libsystemd/sd-json/json-util.h
+++ b/src/libsystemd/sd-json/json-util.h
@@ -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);
diff --git a/src/login/pam_systemd.c b/src/login/pam_systemd.c
index cf46eba195..eca3283da8 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)
diff --git a/src/run/run.c b/src/run/run.c
index 68966ccbda..8d1606af07 100644
--- a/src/run/run.c
+++ b/src/run/run.c
@@ -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;
diff --git a/src/shared/user-record-show.c b/src/shared/user-record-show.c
index af4f7cfb6c..5c5bd15063 100644
--- a/src/shared/user-record-show.c
+++ b/src/shared/user-record-show.c
@@ -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;
diff --git a/src/shared/user-record.c b/src/shared/user-record.c
index 51439f9706..4817bec073 100644
--- a/src/shared/user-record.c
+++ b/src/shared/user-record.c
@@ -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",
diff --git a/src/shared/user-record.h b/src/shared/user-record.h
index d1b9fefa32..8f58c5ca93 100644
--- a/src/shared/user-record.h
+++ b/src/shared/user-record.h
@@ -406,6 +406,8 @@ typedef struct UserRecord {
TmpfsLimit tmp_limit, dev_shm_limit;
+ char *default_area;
+
sd_json_variant *json;
} UserRecord;
diff --git a/test/units/TEST-46-HOMED.sh b/test/units/TEST-46-HOMED.sh
index 3663e53908..5d9799211e 100755
--- a/test/units/TEST-46-HOMED.sh
+++ b/test/units/TEST-46-HOMED.sh
@@ -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