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