mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 08:25:20 +09:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
31
man/run0.xml
31
man/run0.xml
@@ -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>
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -406,6 +406,8 @@ typedef struct UserRecord {
|
||||
|
||||
TmpfsLimit tmp_limit, dev_shm_limit;
|
||||
|
||||
char *default_area;
|
||||
|
||||
sd_json_variant *json;
|
||||
} UserRecord;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user