1
1
mirror of https://github.com/systemd/systemd-stable.git synced 2025-01-27 14:03:43 +03:00

Merge pull request #14375 from poettering/userdb

New varlink API for user and group management
This commit is contained in:
Lennart Poettering 2020-01-15 17:41:29 +01:00 committed by GitHub
commit dd1b23a313
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 10976 additions and 906 deletions

View File

@ -73,6 +73,13 @@ All tools:
appropriate path under /run. This variable is also set by the manager when
RuntimeDirectory= is used, see systemd.exec(5).
* `$SYSTEMD_CRYPT_PREFIX` — if set configures the hash method prefix to use for
UNIX crypt() when generating passwords. By default the system's "preferred
method" is used, but this can be overridden with this environment
variable. Takes a prefix such as `$6$` or `$y$`. (Note that this is only
honoured on systems built with libxcrypt and is ignored on systems using
glibc's original, internal crypt() implementation.)
systemctl:
* `$SYSTEMCTL_FORCE_BUS=1` — if set, do not connect to PID1's private D-Bus

158
docs/GROUP_RECORD.md Normal file
View File

@ -0,0 +1,158 @@
---
title: JSON Group Records
category: Interfaces
layout: default
---
# JSON Group Records
Long story short: JSON Group Records are to `struct group` what [JSON User
Records](https://systemd.io/USER_RECORD.md) are to `struct passwd`.
Conceptually, much of what applies to JSON user records also applies to JSON
group records. They also consist of seven sections, with similar properties and
they carry some identical (or at least very similar) fields.
## Fields in the `regular` section
`groupName` → A string with the UNIX group name. Matches the `gr_name` field of
UNIX/glibc NSS `struct group`, or the shadow structure `struct sgrp`'s
`sg_namp` field.
`realm` → The "realm" the group belongs to, conceptually identical to the same
field of user records. A string in DNS domain name syntax.
`disposition` → The disposition of the group, conceptually identical to the
same field of user records. A string.
`service` → A string, an identifier for the service managing this group record
(this field is typically in reverse domain name syntax.)
`lastChangeUSec` → An unsigned 64bit integer, a timestamp (in µs since the UNIX
epoch 1970) of the last time the group record has been modified. (Covers only
the `regular`, `perMachine` and `privileged` sections).
`gid` → An unsigned integer in the range 0…4294967295: the numeric UNIX group
ID (GID) to use for the group. This corresponds to the `gr_gid` field of
`struct group`.
`members` → An array of strings, listing user names that are members of this
group. Note that JSON user records also contain a `memberOf` field, or in other
words a group membership can either be denoted in the JSON user record or in
the JSON group record, or in both. The list of memberships should be determined
as the combination of both lists (plus optionally others). If a user is listed
as member of a group and doesn't exist it should be ignored. This field
corresponds to the `gr_mem` field of `struct group` and the `sg_mem` field of
`struct sgrp`.
`administrators` → Similarly, an array of strings, listing user names that
shall be considered "administrators" of this group. This field corresponds to
the `sg_adm` field of `struct sgrp`.
`privileged`/`perMachine`/`binding`/`status`/`signature`/`secret` → The
objects/arrays for the other six group record sections. These are organized the
same way as for the JSON user records, and have the same semantics.
## Fields in the `privileged` section
The following fields are defined:
`hashedPassword` → An array of strings with UNIX hashed passwords; see the
matching field for user records for details. This field corresponds to the
`sg_passwd` field of `struct sgrp` (and `gr_passwd` of `struct group` in a
way).
## Fields in the `perMachine` section
`matchMachineId`/`matchHostname` → Strings, match expressions similar as for
user records, see the user record documentation for details.
The following fields are defined for the `perMachine` section and are defined
equivalent to the fields of the same name in the `regular` section, and
override those:
`gid`, `members`, `administrators`
## Fields in the `binding` section
The following fields are defined for the `binding` section, and are equivalent
to the fields of the same name in the `regular` and `perMachine` sections:
`gid`
## Fields in the `status` section
The following fields are defined in the `status` section, and are mostly
equivalent to the fields of the same name in the `regular` section, though with
slightly different conceptual semantics, see the same fields in the user record
documentation:
`service`
## Fields in the `signature` section
The fields in this section are defined identically to those in the matching
section in the user record.
## Fields in the `secret` section
Currently no fields are defined in this section for group records.
## Mapping to `struct group` and `struct sgrp`
When mapping classic UNIX group records (i.e. `struct group` and `struct sgrp`)
to JSON group records the following mappings should be applied:
| Structure | Field | Section | Field | Condition |
|----------------|-------------|--------------|------------------|----------------------------|
| `struct group` | `gr_name` | `regular` | `groupName` | |
| `struct group` | `gr_passwd` | `privileged` | `password` | (See notes below) |
| `struct group` | `gr_gid` | `regular` | `gid` | |
| `struct group` | `gr_mem` | `regular` | `members` | |
| `struct sgrp` | `sg_namp` | `regular` | `groupName` | |
| `struct sgrp` | `sg_passwd` | `privileged` | `password` | (See notes below) |
| `struct sgrp` | `sg_adm` | `regular` | `administrators` | |
| `struct sgrp` | `sg_mem` | `regular` | `members` | |
At this time almost all Linux machines employ shadow passwords, thus the
`gr_passwd` field in `struct group` is set to `"x"`, and the actual password
is stored in the shadow entry `struct sgrp`'s field `sg_passwd`.
## Extending These Records
The same logic and recommendations apply as for JSON user records.
## Examples
A reasonable group record for a system group might look like this:
```json
{
"groupName" : "systemd-resolve",
"gid" : 193,
"status" : {
"6b18704270e94aa896b003b4340978f1" : {
"service" : "io.systemd.NameServiceSwitch"
}
}
}
```
And here's a more complete one for a regular group:
```json
{
"groupName" : "grobie",
"binding" : {
"6b18704270e94aa896b003b4340978f1" : {
"gid" : 60232
}
},
"disposition" : "regular",
"status" : {
"6b18704270e94aa896b003b4340978f1" : {
"service" : "io.systemd.Home"
}
}
}
```

267
docs/USER_GROUP_API.md Normal file
View File

@ -0,0 +1,267 @@
---
title: User/Group Record Lookup API via Varlink
category: Interfaces
layout: default
---
# User/Group Record Lookup API via Varlink
JSON User/Group Records (as described in the [JSON User
Records](https://systemd.io/USER_RECORD) and [JSON Group
Records](https://systemd.io/GROUP_RECORD) documents) that are defined on the
local system may be queried with a [Varlink](https://varlink.org/) API. This
API takes both the role of what
[`getpwnam(3)`](http://man7.org/linux/man-pages/man3/getpwnam.3.html) and
related calls are for `struct passwd`, as well as the interfaces modules
implementing the [glibc Name Service Switch
(NSS)](https://www.gnu.org/software/libc/manual/html_node/Name-Service-Switch.html)
expose. Or in other words, it both allows applications to efficiently query
user/group records from local services, and allows local subsystems to provide
user/group records efficiently to local applications.
This simple API only exposes only three method calls, and requires only a small
subset of the Varlink functionality.
## Why Varlink?
The API described in this document is based on a simple subset of the
mechanisms described by [Varlink](https://varlink.org/). The choice of
preferring Varlink over D-Bus and other IPCs in this context was made for three
reasons:
1. User/Group record resolution should work during early boot and late shutdown
without special handling. This is very hard to do with D-Bus, as the broker
service for D-Bus generally runs as regular system daemon and is hence only
available at the latest boot stage.
2. The JSON user/group records are native JSON data, hence picking an IPC
system that natively operates with JSON data is natural and clean.
3. IPC systems such as D-Bus do not provide flow control and are thus unusable
for streaming data. They are useful to pass around short control messages,
but as soon as potentially many and large objects shall be transferred,
D-Bus is not suitable, as any such streaming of messages would be considered
flooding in D-Bus' logic, and thus possibly result in termination of
communication. Since the APIs defined in this document need to support
enumerating potentially large numbers of users and groups, D-Bus is simply
not an appropriate option.
## Concepts
Each subsystem that needs to define users and groups on the local system is
supposed to implement this API, and offer its interfaces on a Varlink
`AF_UNIX`/`SOCK_STREAM` file system socket bound into the
`/run/systemd/userdb/` directory. When a client wants to look up a user or
group record, it contacts all sockets bound in this directory in parallel, and
enqueues the same query to each. The first positive reply is then returned to
the application, or if all fail the last seen error is returned
instead. (Alternatively a special Varlink service is available,
`io.systemd.Multiplexer` which acts as frontend and will do the parallel
queries on behalf of the client, drastically simplifying client
development. This service is not available during earliest boot and final
shutdown phases.)
Unlike with glibc NSS there's no order or programmatic expression language
defined in which queries are issued to the various services. Instead, all
queries are always enqueued in parallel to all defined services, in order to
make look-ups efficient, and the simple rule of "first successful lookup wins"
is unconditionally followed for user and group look-ups (though not for
membership lookups, see below).
This simple scheme only works safely as long as every service providing
user/group records carefully makes sure not to answer with conflicting
records. This API does not define any mechanisms for dealing with user/group
name/ID collisions during look-up nor during record registration. It assumes
the various subsystems that want to offer user and group records to the rest of
the system have made sufficiently sure in advance that their definitions do not
collide with those of other services. Clients are not expected to merge
multiple definitions for the same user or group, and will also not be able to
detect conflicts and suppress such conflicting records.
It is recommended to name the sockets in the directory in reverse domain name
notation, but this is neither required nor enforced.
## Well-Known Services
Any subsystem that wants to provide user/group records can do so, simply by
binding a socket in the aforementioned directory. By default two
services are listening there, that have special relevance:
1. `io.systemd.NameServiceSwitch` → This service makes the classic UNIX/glibc
NSS user/group records available as JSON User/Group records. Any such
records are automatically converted as needed, and possibly augmented with
information from the shadow databases.
2. `io.systemd.Multiplexer` → This service multiplexes client queries to all
other running services. It's supposed to simplify client development: in
order to look up or enumerate user/group records it's sufficient to talk to
one service instead of all of them in parallel. Note that it is not availabe
during earliest boot and final shutdown phases, hence for programs running
in that context it is preferable to implement the parallel lookup
themselves.
Both these services are implemented by the same daemon
`systemd-userdbd.service`.
Note that these services currently implement a subset of Varlink only. For
example, introspection is not available, and the resolver logic is not used.
## Other Services
The `systemd` project provides two other services implementing this
interface. Specifically:
1. `io.systemd.DynamicUser` → This service is implemented by the service
manager itself, and provides records for the users and groups synthesized
via `DynamicUser=` in unit files.
2. `io.systemd.Home` → This service is implemented by `systemd-homed.service`
and provides records for the users and groups defined by the home
directories it manages.
Other projects are invited to implement these services too. For example it
would make sense for LDAP/ActiveDirectory projects to implement these
interfaces, which would provide them a way to do per-user resource management
enforced by systemd and defined directly in LDAP directories.
## Compatibility with NSS
Two-way compatibility with classic UNIX/glibc NSS user/group records is
provided. When using the Varlink API, lookups into databases provided only via
NSS (and not natively via Varlink) are handled by the
`io.systemd.NameServiceSwitch` service (see above). When using the NSS API
(i.e. `getpwnam()` and friends) the `nss-systemd` module will automatically
synthesize NSS records for users/groups natively defined via a Varlink
API. Special care is taken to avoid recursion between these two compatibility
mechanisms.
Subsystems that shall provide user/group records to the system may choose
between offering them via an NSS module or via a this Varlink API, either way
all records are accessible via both APIs, due to the bidirectional
forwarding. It is also possible to provide the same records via both APIs
directly, but in that case the compatibility logic must be turned off. There
are mechanisms in place for this, please contact the systemd project for
details, as these are currently not documented.
## Caching of User Records
This API defines no concepts for caching records. If caching is desired it
should be implemented in the subsystems that provide the user records, not in
the clients consuming them.
## Method Calls
```
interface io.systemd.UserDatabase
method GetUserRecord(
uid : ?int,
userName : ?string,
service : string
) -> (
record : object,
incomplete : boolean
)
method GetGroupRecord(
gid : ?int,
groupName : ?string,
service : string
) -> (
record : object,
incomplete : boolean
)
method GetMemberships(
userName : ?string,
groupName : ?string,
service : string
) -> (
userName : string,
groupName : string
)
error NoRecordFound()
error BadService()
error ServiceNotAvailable()
error ConflictingRecordFound()
```
The `GetUserRecord` method looks up or enumerates a user record. If the `uid`
parameter is set it specifies the numeric UNIX UID to search for. If the
`userName` parameter is set it specifies the name of the user to search
for. Typically, only one of the two parameters are set, depending whether a
look-up by UID or by name is desired. However, clients may also specify both
parameters, in which case a record matching both will be returned, and if only
one exists that matches one of the two parameters but not the other an error of
`ConflictingRecordFound` is returned. If neither of the two parameters are set
the whole user database is enumerated. In this case the method call needs to be
made with `more` set, so that multiple method call replies may be generated as
effect, each carrying one user record.
The `service` parameter is mandatory and should be set to the service name
being talked to (i.e. to the same name as the `AF_UNIX` socket path, with the
`/run/systemd/userdb/` prefix removed). This is useful to allow implementation
of multiple services on the same socket (which is used by
`systemd-userdbd.service`).
The method call returns one or more user records, depending which type of query is
used (see above). The record is returned in the `record` field. The
`incomplete` field indicates whether the record is complete. Services providing
user record lookup should only pass the `privileged` section of user records to
clients that either match the user the record is about or to sufficiently
privileged clients, for all others the section must be removed so that no
sensitive data is leaked this way. The `incomplete` parameter should indicate
whether the record has been modified like this or not (i.e. it is `true` if a
`privileged` section existed in the user record and was removed, and `false` if
no `privileged` section existed or one existed but hasn't been removed).
If no user record matching the specified UID or name is known the error
`NoRecordFound` is returned (this is also returned if neither UID nor name are
specified, and hence enumeration requested but the subsystem currently has no
users defined).
If a method call with an incorrectly set `service` field is received
(i.e. either not set at all, or not to the service's own name) a `BadService`
error is generated. Finally, `ServiceNotAvailable` should be returned when the
backing subsystem is not operational for some reason and hence no information
about existence or non-existence of a record can be returned nor any user
record at all. (The `service` field is defined in order to allow implementation
of daemons that provide multiple distinct user/group services over the same
`AF_UNIX` socket: in order to correctly determine which service a client wants
to talk to the client needs to provide the name in each request.)
The `GetGroupRecord` method call works analogously but for groups.
The `GetMemberships` method call may be used to inquire about group
memberships. The `userName` and `groupName` arguments take what the name
suggests. If one of the two is specified all matching memberships are returned,
if neither is specified all known memberships of any user and any group are
returned. The return value is a pair of user name and group name, where the
user is a member of the group. If both arguments are specified the specified
membership will be tested for, but no others, and the pair is returned if it is
defined. Unless both arguments are specified the method call needs to be made
with `more` set, so that multiple replies can be returned (since typically
there are are multiple members per group and also multiple groups a user is
member of). As with `GetUserRecord` and `GetGroupRecord` the `service`
parameter needs to contain the name of the service being talked to, in order to
allow implementation of multiple service within the same IPC socket. In case no
matching membership is known `NoRecordFound` is returned. The other two errors
are also generated in the same cases as for `GetUserRecord` and
`GetGroupRecord`.
Unlike with `GetUserRecord` and `GetGroupRecord` the lists of memberships
returned by services are always combined. Thus unlike the other two calls a
membership lookup query has to wait for the last simultaneous query to complete
before the complete list is acquired.
Note that only the `GetMemberships` call is authoritative about memberships of
users in groups. i.e. it should not be considered sufficient to check the
`memberOf` field of user records and the `members` field of group records to
acquire the full list of memberships. The full list can only bet determined by
`GetMemberships`, and as mentioned requires merging of these lists of all local
services. Result of this is that it can be one service that defines a user A,
and another service that defines a group B, and a third service that declares
that A is a member of B.
And that's really all there is to it.

1023
docs/USER_RECORD.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
# This file is part of systemd.
passwd: compat mymachines systemd
group: compat mymachines systemd
group: compat [SUCCESS=merge] mymachines [SUCCESS=merge] systemd
shadow: compat
hosts: files mymachines resolve [!UNAVAIL=return] dns myhostname

View File

@ -18,7 +18,7 @@
<refnamediv>
<refname>nss-systemd</refname>
<refname>libnss_systemd.so.2</refname>
<refpurpose>Provide UNIX user and group name resolution for dynamic users and groups.</refpurpose>
<refpurpose>Provide UNIX user and group name resolution for user/group lookup via Varlink</refpurpose>
</refnamediv>
<refsynopsisdiv>
@ -28,16 +28,24 @@
<refsect1>
<title>Description</title>
<para><command>nss-systemd</command> is a plug-in module for the GNU Name Service Switch (NSS) functionality of the
GNU C Library (<command>glibc</command>), providing UNIX user and group name resolution for dynamic users and
groups allocated through the <varname>DynamicUser=</varname> option in systemd unit files. See
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for details on
this option.</para>
<para><command>nss-systemd</command> is a plug-in module for the GNU Name Service Switch (NSS)
functionality of the GNU C Library (<command>glibc</command>), providing UNIX user and group name
resolution for services implementing the <ulink url="https://systemd.io/USER_GROUP_API">User/Group Record
Lookup API via Varlink</ulink>, such as the system and service manager
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry> (for its
<varname>DynamicUser=</varname> feature, see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details) or
<citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
<para>This module also ensures that the root and nobody users and groups (i.e. the users/groups with the UIDs/GIDs
0 and 65534) remain resolvable at all times, even if they aren't listed in <filename>/etc/passwd</filename> or
<filename>/etc/group</filename>, or if these files are missing.</para>
<para>This module preferably utilizes
<citerefentry><refentrytitle>systemd-userdbd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
for resolving users and groups, but also works without the service running.</para>
<para>To activate the NSS module, add <literal>systemd</literal> to the lines starting with
<literal>passwd:</literal> and <literal>group:</literal> in <filename>/etc/nsswitch.conf</filename>.</para>
@ -54,7 +62,7 @@
<!-- synchronize with other nss-* man pages and factory/etc/nsswitch.conf -->
<programlisting>passwd: compat mymachines <command>systemd</command>
group: compat mymachines <command>systemd</command>
group: compat [SUCCESS=merge] mymachines [SUCCESS=merge] <command>systemd</command>
shadow: compat
hosts: files mymachines resolve [!UNAVAIL=return] dns myhostname

View File

@ -32,6 +32,10 @@
<citerefentry><refentrytitle>systemd-logind.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
and hence the systemd control group hierarchy.</para>
<para>The module also applies various resource management and runtime parameters to the new session, as
configured in the <ulink url="https://systemd.io/USER_RECORD">JSON User Record</ulink> of the user, when
one is defined.</para>
<para>On login, this module — in conjunction with <filename>systemd-logind.service</filename> — ensures the
following:</para>
@ -48,7 +52,12 @@
<listitem><para>A new systemd scope unit is created for the session. If this is the first concurrent session of
the user, an implicit per-user slice unit below <filename>user.slice</filename> is automatically created and the
scope placed into it. An instance of the system service <filename>user@.service</filename>, which runs the
systemd user manager instance, is started. </para></listitem>
systemd user manager instance, is started.</para></listitem>
<listitem><para>The <literal>$TZ</literal>, <literal>$EMAIL</literal> and <literal>$LANG</literal>
environment variables are configured for the user, based on the respective data from the user's JSON
record (if it is defined). Moreover, any environment variables explicitly configured in the user record
are imported, and the umask, nice level, and resource limits initialized.</para></listitem>
</orderedlist>
<para>On logout, this module ensures the following:</para>
@ -172,6 +181,15 @@
is not set if the current user is not the original user of the session.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>$TZ</varname></term>
<term><varname>$EMAIL</varname></term>
<term><varname>$LANG</varname></term>
<listitem><para>If a JSON user record is known for the user logging in these variables are
initialized from the respective data in the record.</para></listitem>
</varlistentry>
</variablelist>
<para>The following environment variables are read by the module and may be used by the PAM service to pass
@ -286,14 +304,23 @@ pam_set_data(handle, "systemd.runtime_max_sec", (void *)"3600", cleanup);
<refsect1>
<title>Example</title>
<para>Here's an example PAM configuration fragment that allows users sessions to be managed by
<filename>systemd-logind.service</filename>:</para>
<programlisting>#%PAM-1.0
auth required pam_unix.so
auth required pam_nologin.so
account required pam_unix.so
password required pam_unix.so
session required pam_unix.so
session required pam_loginuid.so
session required pam_systemd.so</programlisting>
auth sufficient pam_unix.so
auth required pam_deny.so
account required pam_nologin.so
account sufficient pam_unix.so
account required pam_permit.so
password sufficient pam_unix.so sha512 shadow try_first_pass try_authtok
password required pam_deny.so
-session optional pam_loginuid.so
-session optional pam_systemd.so
session required pam_unix.so</programlisting>
</refsect1>
<refsect1>
@ -303,6 +330,7 @@ session required pam_systemd.so</programlisting>
<citerefentry><refentrytitle>systemd-logind.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>logind.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
<citerefentry><refentrytitle>loginctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>pam_systemd_home</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>pam.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>pam.d</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>pam</refentrytitle><manvolnum>8</manvolnum></citerefentry>,

View File

@ -821,6 +821,7 @@ manpages = [
['systemd-update-utmp', 'systemd-update-utmp-runlevel.service'],
'ENABLE_UTMP'],
['systemd-user-sessions.service', '8', ['systemd-user-sessions'], 'HAVE_PAM'],
['systemd-userdbd.service', '8', ['systemd-userdbd'], 'ENABLE_USERDB'],
['systemd-vconsole-setup.service',
'8',
['systemd-vconsole-setup'],
@ -952,6 +953,7 @@ manpages = [
['udev_new', '3', ['udev_ref', 'udev_unref'], ''],
['udevadm', '8', [], ''],
['user@.service', '5', ['user-runtime-dir@.service'], ''],
['userdbctl', '1', [], 'ENABLE_USERDB'],
['vconsole.conf', '5', [], 'ENABLE_VCONSOLE']
]
# Really, do not edit.

View File

@ -0,0 +1,69 @@
<?xml version='1.0'?> <!--*-nxml-*-->
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1+ -->
<refentry id="systemd-userdbd.service" conditional='ENABLE_USERDB'>
<refentryinfo>
<title>systemd-userdbd.service</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>systemd-userdbd.service</refentrytitle>
<manvolnum>8</manvolnum>
</refmeta>
<refnamediv>
<refname>systemd-userdbd.service</refname>
<refname>systemd-userdbd</refname>
<refpurpose>JSON User/Group Record Query Multiplexer/NSS Compatibility</refpurpose>
</refnamediv>
<refsynopsisdiv>
<para><filename>systemd-userdbd.service</filename></para>
<para><filename>/usr/lib/systemd/systemd-userdbd</filename></para>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>systemd-userdbd</command> is a system service that multiplexes user/group lookups to all
local services that provide JSON user/group record definitions to the system. In addition it synthesizes
JSON user/group records from classic UNIX/glibc NSS user/group records in order to provide full backwards
compatibility.</para>
<para>Most of <command>systemd-userdbd</command>'s functionality is accessible through the
<citerefentry><refentrytitle>userdbctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
command.</para>
<para>The user and group records this service provides access to follow the <ulink
url="https://systemd.io/USER_RECORD">JSON User Record</ulink> and <ulink
url="https://systemd.io/GROUP_RECORD">JSON Group Record</ulink> definitions. This service implements the
<ulink url="https://systemd.io/USER_GROUP_API">User/Group Record Lookup API via Varlink</ulink>, and
multiplexes access other services implementing this API, too. It is thus both server and client of this
API.</para>
<para>This service provides two distinct <ulink url="https://varlink.org/">Varlink</ulink> services:
<constant>io.systemd.Multiplexer</constant> provides a single, unified API for querying JSON user and
group records. Internally it talks to all other user/group record services running on the system in
parallel and forwards any information discovered. This simplifies clients substantially since they need
to talk to a single service only instead of all of them in
parallel. <constant>io.systemd.NameSeviceSwitch</constant> provides compatibility with classic UNIX/glibc
NSS user records, i.e. converts <type>struct passwd</type> and <type>struct group</type> records as
acquired with APIs such as <citerefentry
project='man-pages'><refentrytitle>getpwnam</refentrytitle><manvolnum>1</manvolnum></citerefentry> to JSON
user/group records, thus hiding the differences between the services as much as possible.</para>
</refsect1>
<refsect1>
<title>See Also</title>
<para>
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>userdbctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
</para>
</refsect1>
</refentry>

258
man/userdbctl.xml Normal file
View File

@ -0,0 +1,258 @@
<?xml version='1.0'?>
<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
<!-- SPDX-License-Identifier: LGPL-2.1+ -->
<refentry id="userdbctl" conditional='ENABLE_USERDB'
xmlns:xi="http://www.w3.org/2001/XInclude">
<refentryinfo>
<title>userdbctl</title>
<productname>systemd</productname>
</refentryinfo>
<refmeta>
<refentrytitle>userdbctl</refentrytitle>
<manvolnum>1</manvolnum>
</refmeta>
<refnamediv>
<refname>userdbctl</refname>
<refpurpose>Inspect users, groups and group memberships</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>userdbctl</command>
<arg choice="opt" rep="repeat">OPTIONS</arg>
<arg choice="req">COMMAND</arg>
<arg choice="opt" rep="repeat">NAME</arg>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>userdbctl</command> may be used to inspect user and groups (as well as group memberships)
of the system. This client utility inquires user/group information provided by various system services,
both operating on JSON user/group records (as defined by the <ulink
url="https://systemd.io/USER_RECORD">JSON User Record</ulink> and <ulink
url="https://systemd.io/GROUP_RECORD">JSON Group Record</ulink> definitions), and classic UNIX NSS/glibc
user and group records. This tool is primarily a client to the <ulink
url="https://systemd.io/USER_GROUP_API">User/Group Record Lookup API via Varlink</ulink>.</para>
</refsect1>
<refsect1>
<title>Options</title>
<para>The following options are understood:</para>
<variablelist>
<varlistentry>
<term><option>--output=</option><replaceable>MODE</replaceable></term>
<listitem><para>Choose the output mode, takes one of <literal>classic</literal>,
<literal>friendly</literal>, <literal>table</literal>, <literal>json</literal>. If
<literal>classic</literal>, an output very close to the format of <filename>/etc/passwd</filename> or
<filename>/etc/group</filename> is generated. If <literal>friendly</literal> a more comprehensive and
user friendly, human readable output is generated; if <literal>table</literal> a minimal, tabular
output is generated; if <literal>json</literal> a JSON formatted output is generated. Defaults to
<literal>friendly</literal> if a user/group is specified on the command line,
<literal>table</literal> otherwise.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--service=</option><replaceable>SERVICE</replaceable><optional>:<replaceable>SERVICE…</replaceable></optional></term>
<term><option>-s</option> <replaceable>SERVICE</replaceable>:<replaceable>SERVICE…</replaceable></term>
<listitem><para>Controls which services to query for users/groups. Takes a list of one or more
service names, separated by <literal>:</literal>. See below for a list of well-known service
names. If not specified all available services are queried at once.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--with-nss=</option><replaceable>BOOL</replaceable></term>
<listitem><para>Controls whether to include classic glibc/NSS user/group lookups in the output. If
<option>--with-nss=no</option> is used any attempts to resolve or enumerate users/groups provided
only via glibc NSS is suppressed. If <option>--with-nss=yes</option> is specified such users/groups
are included in the output (which is the default).</para></listitem>
</varlistentry>
<varlistentry>
<term><option>--synthesize=</option><replaceable>BOOL</replaceable></term>
<listitem><para>Controls whether to synthesize records for the root and nobody users/groups if they
aren't defined otherwise. By default (or <literal>yes</literal>) such records are implicitly
synthesized if otherwise missing since they have special significance to the OS. When
<literal>no</literal> this synthesizing is turned off.</para></listitem>
</varlistentry>
<varlistentry>
<term><option>-N</option></term>
<listitem><para>This option is short for <option>--with-nss=no</option>
<option>--synthesize=no</option>. Use this option to show only records that are natively defined as
JSON user or group records, with all NSS/glibc compatibility and all implicit synthesis turned
off.</para></listitem>
</varlistentry>
<xi:include href="standard-options.xml" xpointer="no-pager" />
<xi:include href="standard-options.xml" xpointer="no-legend" />
<xi:include href="standard-options.xml" xpointer="help" />
<xi:include href="standard-options.xml" xpointer="version" />
</variablelist>
</refsect1>
<refsect1>
<title>Commands</title>
<para>The following commands are understood:</para>
<variablelist>
<varlistentry>
<term><command>user</command> <optional><replaceable>USER</replaceable></optional></term>
<listitem><para>List all known users records or show details of one or more specified user
records. Use <option>--output=</option> to tweak output mode.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>group</command> <optional><replaceable>GROUP</replaceable></optional></term>
<listitem><para>List all known group records or show details of one or more specified group
records. Use <option>--output=</option> to tweak output mode.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>users-in-group</command> <optional><replaceable>GROUP</replaceable></optional></term>
<listitem><para>List users that are members of the specified groups. If no groups are specified list
all user/group memberships defined. Use <option>--output=</option> to tweak output
mode.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>groups-of-user</command> <optional><replaceable>USER</replaceable></optional></term>
<listitem><para>List groups that the specified users are members of. If no users are specified list
all user/group memberships defined (in this case <command>groups-of-user</command> and
<command>users-in-group</command> are equivalent). Use <option>--output=</option> to tweak output
mode.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>services</command></term>
<listitem><para>List all services currently providing user/group definitions to the system. See below
for a list of well-known services providing user information.</para></listitem>
</varlistentry>
<varlistentry>
<term><command>ssh-authorized-keys</command></term>
<listitem><para>This operation is not a public, user-facing interface. It is used to allow the SSH daemon to pick
up authorized keys from user records, see below.</para></listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1>
<title>Well-Known Services</title>
<para>The <command>userdbctl services</command> command will list all currently running services that
provide user or group definitions to the system. The following are well-known services are shown among
this list.</para>
<variablelist>
<varlistentry>
<term><constant>io.systemd.DynamicUser</constant></term>
<listitem><para>This service is provided by the system service manager itself (i.e. PID 1) and
makes all users (and their groups) synthesized through the <varname>DynamicUser=</varname> setting in
service unit files available to the system (see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details about this setting).</para></listitem>
</varlistentry>
<varlistentry>
<term><constant>io.systemd.Home</constant></term>
<listitem><para>This service is provided by
<citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and makes all users (and their groups) belonging to home directories managed by that service
available to the system.</para></listitem>
</varlistentry>
<varlistentry>
<term><constant>io.systemd.Multiplexer</constant></term>
<listitem><para>This service is provided by
<citerefentry><refentrytitle>systemd-userdbd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and multiplexes user/group look-ups to all other running lookup services. This is the primary entry point
for user/group record clients, as it simplifies client side implementation substantially since they
can ask a single service for lookups instead of asking all running services in parallel.
<command>userdbctl</command> uses this service preferably, too, unless <option>--with-nss=</option>
or <option>--service=</option> are used, in which case finer control over the services to talk to is
required.</para></listitem>
</varlistentry>
<varlistentry>
<term><constant>io.systemd.NameSeviceSwitch</constant></term>
<listitem><para>This service is (also) provided by
<citerefentry><refentrytitle>systemd-userdbd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>
and converts classic NSS/glibc user and group records to JSON user/group records, providing full
backwards compatibility. Use <option>--with-nss=no</option> to disable this compatibility, see
above. Note that compatibility is actually provided in both directions:
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry> will
automatically synthesize classic NSS/glibc user/group records from all JSON user/group records
provided to the system, thus using both APIs is mostly equivalent and provides access to the same
data, however the NSS/glibc APIs necessarily expose a more reduced set of fields
only.</para></listitem>
</varlistentry>
</variablelist>
<para>Note that <command>userdbctl</command> has internal support for NSS-based lookups too. This means
that if neither <constant>io.systemd.Multiplexer</constant> nor
<constant>io.systemd.NameSeviceSwitch</constant> are running look-ups into the the basic user/group
databases will still work.</para>
</refsect1>
<refsect1>
<title>Integration with SSH</title>
<para>The <command>userdbctl</command> tool may be used to make the list of SSH authorized keys possibly
contained in a user record available to the SSH daemon for authentication. For that configure the
following in <citerefentry
project='openssh'><refentrytitle>sshd_config</refentrytitle><manvolnum>5</manvolnum></citerefentry>:</para>
<programlisting>
AuthorizedKeysCommand /usr/bin/userdbctl ssh-authorized-keys %u
AuthorizedKeysCommandUser root
</programlisting>
</refsect1>
<refsect1>
<title>Exit status</title>
<para>On success, 0 is returned, a non-zero failure code otherwise.</para>
</refsect1>
<xi:include href="less-variables.xml" />
<refsect1>
<title>See Also</title>
<para>
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-userdbd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>systemd-homed.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
<citerefentry project='man-pages'><refentrytitle>getent</refentrytitle><manvolnum>1</manvolnum></citerefentry>
</para>
</refsect1>
</refentry>

View File

@ -243,6 +243,7 @@ conf.set_quoted('SYSTEMD_EXPORT_PATH', join_paths(rootlib
conf.set_quoted('VENDOR_KEYRING_PATH', join_paths(rootlibexecdir, 'import-pubring.gpg'))
conf.set_quoted('USER_KEYRING_PATH', join_paths(pkgsysconfdir, 'import-pubring.gpg'))
conf.set_quoted('DOCUMENT_ROOT', join_paths(pkgdatadir, 'gatewayd'))
conf.set_quoted('SYSTEMD_USERWORK_PATH', join_paths(rootlibexecdir, 'systemd-userwork'))
conf.set10('MEMORY_ACCOUNTING_DEFAULT', memory_accounting_default)
conf.set_quoted('MEMORY_ACCOUNTING_DEFAULT_YES_NO', memory_accounting_default ? 'yes' : 'no')
conf.set('STATUS_UNIT_FORMAT_DEFAULT', 'STATUS_UNIT_FORMAT_' + status_unit_format_default.to_upper())
@ -1322,6 +1323,7 @@ foreach term : ['utmp',
'localed',
'machined',
'portabled',
'userdb',
'networkd',
'timedated',
'timesyncd',
@ -1538,6 +1540,7 @@ subdir('src/kernel-install')
subdir('src/locale')
subdir('src/machine')
subdir('src/portable')
subdir('src/userdb')
subdir('src/nspawn')
subdir('src/resolve')
subdir('src/timedate')
@ -1564,7 +1567,7 @@ test_dlopen = executable(
build_by_default : want_tests != 'false')
foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'],
['systemd', 'ENABLE_NSS_SYSTEMD'],
['systemd', 'ENABLE_NSS_SYSTEMD', 'src/nss-systemd/userdb-glue.c src/nss-systemd/userdb-glue.h'],
['mymachines', 'ENABLE_NSS_MYMACHINES'],
['resolve', 'ENABLE_NSS_RESOLVE']]
@ -1575,9 +1578,14 @@ foreach tuple : [['myhostname', 'ENABLE_NSS_MYHOSTNAME'],
sym = 'src/nss-@0@/nss-@0@.sym'.format(module)
version_script_arg = join_paths(project_source_root, sym)
sources = ['src/nss-@0@/nss-@0@.c'.format(module)]
if tuple.length() > 2
sources += tuple[2].split()
endif
nss = shared_library(
'nss_' + module,
'src/nss-@0@/nss-@0@.c'.format(module),
sources,
disable_mempool_c,
version : '2',
include_directories : includes,
@ -1974,6 +1982,35 @@ if conf.get('ENABLE_PORTABLED') == 1
public_programs += exe
endif
if conf.get('ENABLE_USERDB') == 1
executable('systemd-userwork',
systemd_userwork_sources,
include_directories : includes,
link_with : [libshared],
dependencies : [threads],
install_rpath : rootlibexecdir,
install : true,
install_dir : rootlibexecdir)
executable('systemd-userdbd',
systemd_userdbd_sources,
include_directories : includes,
link_with : [libshared],
dependencies : [threads],
install_rpath : rootlibexecdir,
install : true,
install_dir : rootlibexecdir)
executable('userdbctl',
userdbctl_sources,
include_directories : includes,
link_with : [libshared],
dependencies : [threads],
install_rpath : rootlibexecdir,
install : true,
install_dir : rootbindir)
endif
foreach alias : ['halt', 'poweroff', 'reboot', 'runlevel', 'shutdown', 'telinit']
meson.add_install_script(meson_make_symlink,
join_paths(rootbindir, 'systemctl'),
@ -3252,6 +3289,7 @@ foreach tuple : [
['logind'],
['machined'],
['portabled'],
['userdb'],
['importd'],
['hostnamed'],
['timedated'],

View File

@ -94,6 +94,8 @@ option('machined', type : 'boolean',
description : 'install the systemd-machined stack')
option('portabled', type : 'boolean',
description : 'install the systemd-portabled stack')
option('userdb', type : 'boolean',
description : 'install the systemd-userdbd stack')
option('networkd', type : 'boolean',
description : 'install the systemd-networkd stack')
option('timedated', type : 'boolean',

View File

@ -139,6 +139,38 @@ enum nss_status _nss_##module##_getgrgid_r( \
char *buffer, size_t buflen, \
int *errnop) _public_
#define NSS_PWENT_PROTOTYPES(module) \
enum nss_status _nss_##module##_endpwent( \
void) _public_; \
enum nss_status _nss_##module##_setpwent( \
int stayopen) _public_; \
enum nss_status _nss_##module##_getpwent_r( \
struct passwd *result, \
char *buffer, \
size_t buflen, \
int *errnop) _public_;
#define NSS_GRENT_PROTOTYPES(module) \
enum nss_status _nss_##module##_endgrent( \
void) _public_; \
enum nss_status _nss_##module##_setgrent( \
int stayopen) _public_; \
enum nss_status _nss_##module##_getgrent_r( \
struct group *result, \
char *buffer, \
size_t buflen, \
int *errnop) _public_;
#define NSS_INITGROUPS_PROTOTYPE(module) \
enum nss_status _nss_##module##_initgroups_dyn( \
const char *user, \
gid_t group, \
long int *start, \
long int *size, \
gid_t **groupsp, \
long int limit, \
int *errnop) _public_;
typedef enum nss_status (*_nss_gethostbyname4_r_t)(
const char *name,
struct gaih_addrtuple **pat,

View File

@ -945,40 +945,3 @@ int fgetsgent_sane(FILE *stream, struct sgrp **sg) {
return !!s;
}
#endif
int make_salt(char **ret) {
static const char table[] =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789"
"./";
uint8_t raw[16];
char *salt, *j;
size_t i;
int r;
/* This is a bit like crypt_gensalt_ra(), but doesn't require libcrypt, and doesn't do anything but
* SHA512, i.e. is legacy-free and minimizes our deps. */
assert_cc(sizeof(table) == 64U + 1U);
/* Insist on the best randomness by setting RANDOM_BLOCK, this is about keeping passwords secret after all. */
r = genuine_random_bytes(raw, sizeof(raw), RANDOM_BLOCK);
if (r < 0)
return r;
salt = new(char, 3+sizeof(raw)+1+1);
if (!salt)
return -ENOMEM;
/* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */
j = stpcpy(salt, "$6$");
for (i = 0; i < sizeof(raw); i++)
j[i] = table[raw[i] & 63];
j[i++] = '$';
j[i] = 0;
*ret = salt;
return 0;
}

View File

@ -137,6 +137,4 @@ int fgetsgent_sane(FILE *stream, struct sgrp **sg);
int putsgent_sane(const struct sgrp *sg, FILE *stream);
#endif
int make_salt(char **ret);
bool is_nologin_shell(const char *shell);

310
src/core/core-varlink.c Normal file
View File

@ -0,0 +1,310 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "core-varlink.h"
#include "mkdir.h"
#include "user-util.h"
#include "varlink.h"
typedef struct LookupParameters {
const char *user_name;
const char *group_name;
union {
uid_t uid;
gid_t gid;
};
const char *service;
} LookupParameters;
static int build_user_json(const char *user_name, uid_t uid, JsonVariant **ret) {
assert(user_name);
assert(uid_is_valid(uid));
assert(ret);
return json_build(ret, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(user_name)),
JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(uid)),
JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(uid)),
JSON_BUILD_PAIR("realName", JSON_BUILD_STRING("Dynamic User")),
JSON_BUILD_PAIR("homeDirectory", JSON_BUILD_STRING("/")),
JSON_BUILD_PAIR("shell", JSON_BUILD_STRING(NOLOGIN)),
JSON_BUILD_PAIR("locked", JSON_BUILD_BOOLEAN(true)),
JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")),
JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic"))))));
}
static bool user_match_lookup_parameters(LookupParameters *p, const char *name, uid_t uid) {
assert(p);
if (p->user_name && !streq(name, p->user_name))
return false;
if (uid_is_valid(p->uid) && uid != p->uid)
return false;
return true;
}
static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 },
{ "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
{ "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
{}
};
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
LookupParameters p = {
.uid = UID_INVALID,
};
_cleanup_free_ char *found_name = NULL;
uid_t found_uid = UID_INVALID, uid;
Manager *m = userdata;
const char *un;
int r;
assert(parameters);
assert(m);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
if (!streq_ptr(p.service, "io.systemd.DynamicUser"))
return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
if (uid_is_valid(p.uid))
r = dynamic_user_lookup_uid(m, p.uid, &found_name);
else if (p.user_name)
r = dynamic_user_lookup_name(m, p.user_name, &found_uid);
else {
Iterator i;
DynamicUser *d;
HASHMAP_FOREACH(d, m->dynamic_users, i) {
r = dynamic_user_current(d, &uid);
if (r == -EAGAIN) /* not realized yet? */
continue;
if (r < 0)
return r;
if (!user_match_lookup_parameters(&p, d->name, uid))
continue;
if (v) {
r = varlink_notify(link, v);
if (r < 0)
return r;
v = json_variant_unref(v);
}
r = build_user_json(d->name, uid, &v);
if (r < 0)
return r;
}
if (!v)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_reply(link, v);
}
if (r == -ESRCH)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
if (r < 0)
return r;
uid = uid_is_valid(found_uid) ? found_uid : p.uid;
un = found_name ?: p.user_name;
if (!user_match_lookup_parameters(&p, un, uid))
return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
r = build_user_json(un, uid, &v);
if (r < 0)
return r;
return varlink_reply(link, v);
}
static int build_group_json(const char *group_name, gid_t gid, JsonVariant **ret) {
assert(group_name);
assert(gid_is_valid(gid));
assert(ret);
return json_build(ret, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("record", JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(group_name)),
JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(gid)),
JSON_BUILD_PAIR("service", JSON_BUILD_STRING("io.systemd.DynamicUser")),
JSON_BUILD_PAIR("disposition", JSON_BUILD_STRING("dynamic"))))));
}
static bool group_match_lookup_parameters(LookupParameters *p, const char *name, gid_t gid) {
assert(p);
if (p->group_name && !streq(name, p->group_name))
return false;
if (gid_is_valid(p->gid) && gid != p->gid)
return false;
return true;
}
static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 },
{ "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
{ "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
{}
};
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
LookupParameters p = {
.gid = GID_INVALID,
};
_cleanup_free_ char *found_name = NULL;
uid_t found_gid = GID_INVALID, gid;
Manager *m = userdata;
const char *gn;
int r;
assert(parameters);
assert(m);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
if (!streq_ptr(p.service, "io.systemd.DynamicUser"))
return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
if (gid_is_valid(p.gid))
r = dynamic_user_lookup_uid(m, (uid_t) p.gid, &found_name);
else if (p.group_name)
r = dynamic_user_lookup_name(m, p.group_name, (uid_t*) &found_gid);
else {
DynamicUser *d;
Iterator i;
HASHMAP_FOREACH(d, m->dynamic_users, i) {
uid_t uid;
r = dynamic_user_current(d, &uid);
if (r == -EAGAIN)
continue;
if (r < 0)
return r;
if (!group_match_lookup_parameters(&p, d->name, (gid_t) uid))
continue;
if (v) {
r = varlink_notify(link, v);
if (r < 0)
return r;
v = json_variant_unref(v);
}
r = build_group_json(d->name, (gid_t) uid, &v);
if (r < 0)
return r;
}
if (!v)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_reply(link, v);
}
if (r == -ESRCH)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
if (r < 0)
return r;
gid = gid_is_valid(found_gid) ? found_gid : p.gid;
gn = found_name ?: p.group_name;
if (!group_match_lookup_parameters(&p, gn, gid))
return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
r = build_group_json(gn, gid, &v);
if (r < 0)
return r;
return varlink_reply(link, v);
}
static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), JSON_SAFE },
{ "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), JSON_SAFE },
{ "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
{}
};
LookupParameters p = {};
int r;
assert(parameters);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
if (!streq_ptr(p.service, "io.systemd.DynamicUser"))
return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
/* We don't support auxiliary groups with dynamic users. */
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
}
int manager_varlink_init(Manager *m) {
_cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL;
int r;
assert(m);
if (m->varlink_server)
return 0;
if (!MANAGER_IS_SYSTEM(m))
return 0;
r = varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID);
if (r < 0)
return log_error_errno(r, "Failed to allocate varlink server object: %m");
varlink_server_set_userdata(s, m);
r = varlink_server_bind_method_many(
s,
"io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record,
"io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record,
"io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships);
if (r < 0)
return log_error_errno(r, "Failed to register varlink methods: %m");
(void) mkdir_p("/run/systemd/userdb", 0755);
r = varlink_server_listen_address(s, "/run/systemd/userdb/io.systemd.DynamicUser", 0666);
if (r < 0)
return log_error_errno(r, "Failed to bind to varlink socket: %m");
r = varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL);
if (r < 0)
return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
m->varlink_server = TAKE_PTR(s);
return 0;
}
void manager_varlink_done(Manager *m) {
assert(m);
m->varlink_server = varlink_server_unref(m->varlink_server);
}

7
src/core/core-varlink.h Normal file
View File

@ -0,0 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include "manager.h"
int manager_varlink_init(Manager *m);
void manager_varlink_done(Manager *m);

View File

@ -529,7 +529,6 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) {
int r;
assert(d);
assert(ret);
/* Get the currently assigned UID for the user, if there's any. This simply pops the data from the storage socket, and pushes it back in right-away. */
@ -545,7 +544,9 @@ int dynamic_user_current(DynamicUser *d, uid_t *ret) {
if (r < 0)
return r;
*ret = uid;
if (ret)
*ret = uid;
return 0;
}
@ -732,7 +733,6 @@ int dynamic_user_lookup_name(Manager *m, const char *name, uid_t *ret) {
assert(m);
assert(name);
assert(ret);
/* A friendly call for translating a dynamic user's name into its UID */

View File

@ -29,6 +29,7 @@
#include "bus-util.h"
#include "clean-ipc.h"
#include "clock-util.h"
#include "core-varlink.h"
#include "dbus-job.h"
#include "dbus-manager.h"
#include "dbus-unit.h"
@ -44,8 +45,8 @@
#include "fileio.h"
#include "fs-util.h"
#include "hashmap.h"
#include "io-util.h"
#include "install.h"
#include "io-util.h"
#include "label.h"
#include "locale-setup.h"
#include "log.h"
@ -1346,6 +1347,7 @@ Manager* manager_free(Manager *m) {
lookup_paths_flush_generator(&m->lookup_paths);
bus_done(m);
manager_varlink_done(m);
exec_runtime_vacuum(m);
hashmap_free(m->exec_runtime_by_id);
@ -1703,6 +1705,10 @@ int manager_startup(Manager *m, FILE *serialization, FDSet *fds) {
log_warning_errno(r, "Failed to deserialized tracked clients, ignoring: %m");
m->deserialized_subscribed = strv_free(m->deserialized_subscribed);
r = manager_varlink_init(m);
if (r < 0)
log_warning_errno(r, "Failed to set up Varlink server, ignoring: %m");
/* Third, fire things up! */
manager_coldplug(m);

View File

@ -16,6 +16,7 @@
#include "list.h"
#include "prioq.h"
#include "ratelimit.h"
#include "varlink.h"
struct libmnt_monitor;
typedef struct Unit Unit;
@ -422,6 +423,8 @@ struct Manager {
unsigned notifygen;
bool honor_device_enumeration;
VarlinkServer *varlink_server;
};
static inline usec_t manager_default_timeout_abort_usec(Manager *m) {

View File

@ -22,6 +22,8 @@ libcore_sources = '''
bpf-firewall.h
cgroup.c
cgroup.h
core-varlink.c
core-varlink.h
dbus-automount.c
dbus-automount.h
dbus-cgroup.c

View File

@ -4,19 +4,6 @@
#include <getopt.h>
#include <unistd.h>
#if HAVE_CRYPT_H
/* libxcrypt is a replacement for glibc's libcrypt, and libcrypt might be
* removed from glibc at some point. As part of the removal, defines for
* crypt(3) are dropped from unistd.h, and we must include crypt.h instead.
*
* Newer versions of glibc (v2.0+) already ship crypt.h with a definition
* of crypt(3) as well, so we simply include it if it is present. MariaDB,
* MySQL, PostgreSQL, Perl and some other wide-spread packages do it the
* same way since ages without any problems.
*/
# include <crypt.h>
#endif
#include "sd-id128.h"
#include "alloc-util.h"
@ -28,6 +15,7 @@
#include "fs-util.h"
#include "hostname-util.h"
#include "kbd-util.h"
#include "libcrypt-util.h"
#include "locale-util.h"
#include "main-func.h"
#include "memory-util.h"

View File

@ -27,6 +27,7 @@
#include "terminal-util.h"
#include "udev-util.h"
#include "user-util.h"
#include "userdb.h"
void manager_reset_config(Manager *m) {
assert(m);
@ -137,21 +138,18 @@ int manager_add_session(Manager *m, const char *id, Session **ret_session) {
int manager_add_user(
Manager *m,
uid_t uid,
gid_t gid,
const char *name,
const char *home,
UserRecord *ur,
User **ret_user) {
User *u;
int r;
assert(m);
assert(name);
assert(ur);
u = hashmap_get(m->users, UID_TO_PTR(uid));
u = hashmap_get(m->users, UID_TO_PTR(ur->uid));
if (!u) {
r = user_new(&u, m, uid, gid, name, home);
r = user_new(&u, m, ur);
if (r < 0)
return r;
}
@ -167,32 +165,35 @@ int manager_add_user_by_name(
const char *name,
User **ret_user) {
const char *home = NULL;
uid_t uid;
gid_t gid;
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int r;
assert(m);
assert(name);
r = get_user_creds(&name, &uid, &gid, &home, NULL, 0);
r = userdb_by_name(name, 0, &ur);
if (r < 0)
return r;
return manager_add_user(m, uid, gid, name, home, ret_user);
return manager_add_user(m, ur, ret_user);
}
int manager_add_user_by_uid(Manager *m, uid_t uid, User **ret_user) {
struct passwd *p;
int manager_add_user_by_uid(
Manager *m,
uid_t uid,
User **ret_user) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int r;
assert(m);
assert(uid_is_valid(uid));
errno = 0;
p = getpwuid(uid);
if (!p)
return errno_or_else(ENOENT);
r = userdb_by_uid(uid, 0, &ur);
if (r < 0)
return r;
return manager_add_user(m, uid, p->pw_gid, p->pw_name, p->pw_dir, ret_user);
return manager_add_user(m, ur, ret_user);
}
int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **ret) {

View File

@ -536,8 +536,8 @@ static int method_list_sessions(sd_bus_message *message, void *userdata, sd_bus_
r = sd_bus_message_append(reply, "(susso)",
session->id,
(uint32_t) session->user->uid,
session->user->name,
(uint32_t) session->user->user_record->uid,
session->user->user_record->user_name,
session->seat ? session->seat->id : "",
p);
if (r < 0)
@ -577,8 +577,8 @@ static int method_list_users(sd_bus_message *message, void *userdata, sd_bus_err
return -ENOMEM;
r = sd_bus_message_append(reply, "(uso)",
(uint32_t) user->uid,
user->name,
(uint32_t) user->user_record->uid,
user->user_record->user_name,
p);
if (r < 0)
return r;
@ -1488,7 +1488,7 @@ static int have_multiple_sessions(
* count, and non-login sessions do not count either. */
HASHMAP_FOREACH(session, m->sessions, i)
if (session->class == SESSION_USER &&
session->user->uid != uid)
session->user->user_record->uid != uid)
return true;
return false;

View File

@ -119,7 +119,7 @@ int seat_save(Seat *s) {
"ACTIVE=%s\n"
"ACTIVE_UID="UID_FMT"\n",
s->active->id,
s->active->user->uid);
s->active->user->user_record->uid);
}
if (s->sessions) {
@ -137,7 +137,7 @@ int seat_save(Seat *s) {
LIST_FOREACH(sessions_by_seat, i, s->sessions)
fprintf(f,
UID_FMT"%c",
i->user->uid,
i->user->user_record->uid,
i->sessions_by_seat_next ? ' ' : '\n');
}
@ -216,8 +216,8 @@ int seat_apply_acls(Seat *s, Session *old_active) {
r = devnode_acl_all(s->id,
false,
!!old_active, old_active ? old_active->user->uid : 0,
!!s->active, s->active ? s->active->user->uid : 0);
!!old_active, old_active ? old_active->user->user_record->uid : 0,
!!s->active, s->active ? s->active->user->user_record->uid : 0);
if (r < 0)
return log_error_errno(r, "Failed to apply ACLs: %m");

View File

@ -43,7 +43,7 @@ static int property_get_user(
if (!p)
return -ENOMEM;
return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->uid, p);
return sd_bus_message_append(reply, "(uo)", (uint32_t) s->user->user_record->uid, p);
}
static int property_get_name(
@ -61,7 +61,7 @@ static int property_get_name(
assert(reply);
assert(s);
return sd_bus_message_append(reply, "s", s->user->name);
return sd_bus_message_append(reply, "s", s->user->user_record->user_name);
}
static int property_get_seat(
@ -168,7 +168,7 @@ int bus_session_method_terminate(sd_bus_message *message, void *userdata, sd_bus
"org.freedesktop.login1.manage",
NULL,
false,
s->user->uid,
s->user->user_record->uid,
&s->manager->polkit_registry,
error);
if (r < 0)
@ -210,7 +210,7 @@ int bus_session_method_lock(sd_bus_message *message, void *userdata, sd_bus_erro
"org.freedesktop.login1.lock-sessions",
NULL,
false,
s->user->uid,
s->user->user_record->uid,
&s->manager->polkit_registry,
error);
if (r < 0)
@ -246,7 +246,7 @@ static int method_set_idle_hint(sd_bus_message *message, void *userdata, sd_bus_
if (r < 0)
return r;
if (uid != 0 && uid != s->user->uid)
if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set idle hint");
r = session_set_idle_hint(s, b);
@ -279,7 +279,7 @@ static int method_set_locked_hint(sd_bus_message *message, void *userdata, sd_bu
if (r < 0)
return r;
if (uid != 0 && uid != s->user->uid)
if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may set locked hint");
session_set_locked_hint(s, b);
@ -318,7 +318,7 @@ int bus_session_method_kill(sd_bus_message *message, void *userdata, sd_bus_erro
"org.freedesktop.login1.manage",
NULL,
false,
s->user->uid,
s->user->user_record->uid,
&s->manager->polkit_registry,
error);
if (r < 0)
@ -354,7 +354,7 @@ static int method_take_control(sd_bus_message *message, void *userdata, sd_bus_e
if (r < 0)
return r;
if (uid != 0 && (force || uid != s->user->uid))
if (uid != 0 && (force || uid != s->user->user_record->uid))
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may take control");
r = session_set_controller(s, sd_bus_message_get_sender(message), force, true);
@ -523,7 +523,7 @@ static int method_set_brightness(sd_bus_message *message, void *userdata, sd_bus
if (r < 0)
return r;
if (uid != 0 && uid != s->user->uid)
if (uid != 0 && uid != s->user->user_record->uid)
return sd_bus_error_setf(error, SD_BUS_ERROR_ACCESS_DENIED, "Only owner of session may change brightness.");
r = sd_device_new_from_subsystem_sysname(&d, subsystem, name);
@ -822,7 +822,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) {
"session_fd=%d seat=%s vtnr=%u",
s->id,
p,
(uint32_t) s->user->uid,
(uint32_t) s->user->user_record->uid,
s->user->runtime_path,
fifo_fd,
s->seat ? s->seat->id : "",
@ -834,7 +834,7 @@ int session_send_create_reply(Session *s, sd_bus_error *error) {
p,
s->user->runtime_path,
fifo_fd,
(uint32_t) s->user->uid,
(uint32_t) s->user->user_record->uid,
s->seat ? s->seat->id : "",
(uint32_t) s->vtnr,
false);

View File

@ -230,8 +230,8 @@ int session_save(Session *s) {
"IS_DISPLAY=%i\n"
"STATE=%s\n"
"REMOTE=%i\n",
s->user->uid,
s->user->name,
s->user->user_record->uid,
s->user->user_record->user_name,
session_is_active(s),
s->user->display == s,
session_state_to_string(session_get_state(s)),
@ -641,7 +641,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
if (!scope)
return log_oom();
description = strjoina("Session ", s->id, " of user ", s->user->name);
description = strjoina("Session ", s->id, " of user ", s->user->user_record->user_name);
r = manager_start_scope(
s->manager,
@ -657,7 +657,7 @@ static int session_start_scope(Session *s, sd_bus_message *properties, sd_bus_er
"systemd-user-sessions.service",
s->user->runtime_dir_service,
s->user->service),
s->user->home,
user_record_home_directory(s->user->user_record),
properties,
error,
&s->scope_job);
@ -698,9 +698,9 @@ int session_start(Session *s, sd_bus_message *properties, sd_bus_error *error) {
log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO,
"MESSAGE_ID=" SD_MESSAGE_SESSION_START_STR,
"SESSION_ID=%s", s->id,
"USER_ID=%s", s->user->name,
"USER_ID=%s", s->user->user_record->user_name,
"LEADER="PID_FMT, s->leader,
LOG_MESSAGE("New session %s of user %s.", s->id, s->user->name));
LOG_MESSAGE("New session %s of user %s.", s->id, s->user->user_record->user_name));
if (!dual_timestamp_is_set(&s->timestamp))
dual_timestamp_get(&s->timestamp);
@ -750,7 +750,10 @@ static int session_stop_scope(Session *s, bool force) {
s->scope_job = mfree(s->scope_job);
/* Optionally, let's kill everything that's left now. */
if (force || manager_shall_kill(s->manager, s->user->name)) {
if (force ||
(s->user->user_record->kill_processes != 0 &&
(s->user->user_record->kill_processes > 0 ||
manager_shall_kill(s->manager, s->user->user_record->user_name)))) {
r = manager_stop_unit(s->manager, s->scope, &error, &s->scope_job);
if (r < 0) {
@ -766,7 +769,7 @@ static int session_stop_scope(Session *s, bool force) {
* Session stop is quite significant on its own, let's log it. */
log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO,
"SESSION_ID=%s", s->id,
"USER_ID=%s", s->user->name,
"USER_ID=%s", s->user->user_record->user_name,
"LEADER="PID_FMT, s->leader,
LOG_MESSAGE("Session %s logged out. Waiting for processes to exit.", s->id));
}
@ -824,7 +827,7 @@ int session_finalize(Session *s) {
log_struct(s->class == SESSION_BACKGROUND ? LOG_DEBUG : LOG_INFO,
"MESSAGE_ID=" SD_MESSAGE_SESSION_STOP_STR,
"SESSION_ID=%s", s->id,
"USER_ID=%s", s->user->name,
"USER_ID=%s", s->user->user_record->user_name,
"LEADER="PID_FMT, s->leader,
LOG_MESSAGE("Removed session %s.", s->id));
@ -1189,7 +1192,7 @@ static int session_prepare_vt(Session *s) {
if (vt < 0)
return vt;
r = fchown(vt, s->user->uid, -1);
r = fchown(vt, s->user->user_record->uid, -1);
if (r < 0) {
r = log_error_errno(errno,
"Cannot change owner of /dev/tty%u: %m",

View File

@ -15,6 +15,60 @@
#include "strv.h"
#include "user-util.h"
static int property_get_uid(
sd_bus *bus,
const char *path,
const char *interface,
const char *property,
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
User *u = userdata;
assert(bus);
assert(reply);
assert(u);
return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->uid);
}
static int property_get_gid(
sd_bus *bus,
const char *path,
const char *interface,
const char *property,
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
User *u = userdata;
assert(bus);
assert(reply);
assert(u);
return sd_bus_message_append(reply, "u", (uint32_t) u->user_record->gid);
}
static int property_get_name(
sd_bus *bus,
const char *path,
const char *interface,
const char *property,
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
User *u = userdata;
assert(bus);
assert(reply);
assert(u);
return sd_bus_message_append(reply, "s", u->user_record->user_name);
}
static BUS_DEFINE_PROPERTY_GET2(property_get_state, "s", User, user_get_state, user_state_to_string);
static int property_get_display(
@ -152,7 +206,7 @@ int bus_user_method_terminate(sd_bus_message *message, void *userdata, sd_bus_er
"org.freedesktop.login1.manage",
NULL,
false,
u->uid,
u->user_record->uid,
&u->manager->polkit_registry,
error);
if (r < 0)
@ -181,7 +235,7 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error *
"org.freedesktop.login1.manage",
NULL,
false,
u->uid,
u->user_record->uid,
&u->manager->polkit_registry,
error);
if (r < 0)
@ -206,9 +260,9 @@ int bus_user_method_kill(sd_bus_message *message, void *userdata, sd_bus_error *
const sd_bus_vtable user_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_PROPERTY("UID", "u", bus_property_get_uid, offsetof(User, uid), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("GID", "u", bus_property_get_gid, offsetof(User, gid), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Name", "s", NULL, offsetof(User, name), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("UID", "u", property_get_uid, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("GID", "u", property_get_gid, 0, SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Name", "s", property_get_name, 0, SD_BUS_VTABLE_PROPERTY_CONST),
BUS_PROPERTY_DUAL_TIMESTAMP("Timestamp", offsetof(User, timestamp), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("RuntimePath", "s", NULL, offsetof(User, runtime_path), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Service", "s", NULL, offsetof(User, service), SD_BUS_VTABLE_PROPERTY_CONST),
@ -276,7 +330,7 @@ char *user_bus_path(User *u) {
assert(u);
if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->uid) < 0)
if (asprintf(&s, "/org/freedesktop/login1/user/_"UID_FMT, u->user_record->uid) < 0)
return NULL;
return s;
@ -345,7 +399,7 @@ int user_send_signal(User *u, bool new_user) {
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
new_user ? "UserNew" : "UserRemoved",
"uo", (uint32_t) u->uid, p);
"uo", (uint32_t) u->user_record->uid, p);
}
int user_send_changed(User *u, const char *properties, ...) {

View File

@ -37,10 +37,7 @@
int user_new(User **ret,
Manager *m,
uid_t uid,
gid_t gid,
const char *name,
const char *home) {
UserRecord *ur) {
_cleanup_(user_freep) User *u = NULL;
char lu[DECIMAL_STR_MAX(uid_t) + 1];
@ -48,7 +45,13 @@ int user_new(User **ret,
assert(ret);
assert(m);
assert(name);
assert(ur);
if (!ur->user_name)
return -EINVAL;
if (!uid_is_valid(ur->uid))
return -EINVAL;
u = new(User, 1);
if (!u)
@ -56,28 +59,17 @@ int user_new(User **ret,
*u = (User) {
.manager = m,
.uid = uid,
.gid = gid,
.user_record = user_record_ref(ur),
.last_session_timestamp = USEC_INFINITY,
};
u->name = strdup(name);
if (!u->name)
if (asprintf(&u->state_file, "/run/systemd/users/" UID_FMT, ur->uid) < 0)
return -ENOMEM;
u->home = strdup(home);
if (!u->home)
if (asprintf(&u->runtime_path, "/run/user/" UID_FMT, ur->uid) < 0)
return -ENOMEM;
path_simplify(u->home, true);
if (asprintf(&u->state_file, "/run/systemd/users/"UID_FMT, uid) < 0)
return -ENOMEM;
if (asprintf(&u->runtime_path, "/run/user/"UID_FMT, uid) < 0)
return -ENOMEM;
xsprintf(lu, UID_FMT, uid);
xsprintf(lu, UID_FMT, ur->uid);
r = slice_build_subslice(SPECIAL_USER_SLICE, lu, &u->slice);
if (r < 0)
return r;
@ -90,7 +82,7 @@ int user_new(User **ret,
if (r < 0)
return r;
r = hashmap_put(m->users, UID_TO_PTR(uid), u);
r = hashmap_put(m->users, UID_TO_PTR(ur->uid), u);
if (r < 0)
return r;
@ -129,9 +121,9 @@ User *user_free(User *u) {
if (u->slice)
hashmap_remove_value(u->manager->user_units, u->slice, u);
hashmap_remove_value(u->manager->users, UID_TO_PTR(u->uid), u);
hashmap_remove_value(u->manager->users, UID_TO_PTR(u->user_record->uid), u);
(void) sd_event_source_unref(u->timer_event_source);
sd_event_source_unref(u->timer_event_source);
u->service_job = mfree(u->service_job);
@ -140,8 +132,8 @@ User *user_free(User *u) {
u->slice = mfree(u->slice);
u->runtime_path = mfree(u->runtime_path);
u->state_file = mfree(u->state_file);
u->name = mfree(u->name);
u->home = mfree(u->home);
user_record_unref(u->user_record);
return mfree(u);
}
@ -169,7 +161,7 @@ static int user_save_internal(User *u) {
"NAME=%s\n"
"STATE=%s\n" /* friendly user-facing state */
"STOPPING=%s\n", /* low-level state */
u->name,
u->user_record->user_name,
user_state_to_string(user_get_state(u)),
yes_no(u->stopping));
@ -363,6 +355,100 @@ static void user_start_service(User *u) {
"Failed to start user service '%s', ignoring: %s", u->service, bus_error_message(&error, r));
}
static int update_slice_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
_cleanup_(user_record_unrefp) UserRecord *ur = userdata;
assert(m);
assert(ur);
if (sd_bus_message_is_method_error(m, NULL)) {
log_warning_errno(sd_bus_message_get_errno(m),
"Failed to update slice of %s, ignoring: %s",
ur->user_name,
sd_bus_message_get_error(m)->message);
return 0;
}
log_debug("Successfully set slice parameters of %s.", ur->user_name);
return 0;
}
static int user_update_slice(User *u) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
int r;
assert(u);
if (u->user_record->tasks_max == UINT64_MAX &&
u->user_record->memory_high == UINT64_MAX &&
u->user_record->memory_max == UINT64_MAX &&
u->user_record->cpu_weight == UINT64_MAX &&
u->user_record->io_weight == UINT64_MAX)
return 0;
r = sd_bus_message_new_method_call(
u->manager->bus,
&m,
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"SetUnitProperties");
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_append(m, "sb", u->slice, true);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_message_open_container(m, 'a', "(sv)");
if (r < 0)
return bus_log_create_error(r);
if (u->user_record->tasks_max != UINT64_MAX) {
r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", u->user_record->tasks_max);
if (r < 0)
return bus_log_create_error(r);
}
if (u->user_record->memory_max != UINT64_MAX) {
r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", u->user_record->memory_max);
if (r < 0)
return bus_log_create_error(r);
}
if (u->user_record->memory_high != UINT64_MAX) {
r = sd_bus_message_append(m, "(sv)", "MemoryHigh", "t", u->user_record->memory_high);
if (r < 0)
return bus_log_create_error(r);
}
if (u->user_record->cpu_weight != UINT64_MAX) {
r = sd_bus_message_append(m, "(sv)", "CPUWeight", "t", u->user_record->cpu_weight);
if (r < 0)
return bus_log_create_error(r);
}
if (u->user_record->io_weight != UINT64_MAX) {
r = sd_bus_message_append(m, "(sv)", "IOWeight", "t", u->user_record->io_weight);
if (r < 0)
return bus_log_create_error(r);
}
r = sd_bus_message_close_container(m);
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_call_async(u->manager->bus, NULL, m, update_slice_callback, u->user_record, 0);
if (r < 0)
return log_error_errno(r, "Failed to change user slice properties: %m");
/* Ref the user record pointer, so that the slot keeps it pinned */
user_record_ref(u->user_record);
return 0;
}
int user_start(User *u) {
assert(u);
@ -376,12 +462,15 @@ int user_start(User *u) {
u->stopping = false;
if (!u->started)
log_debug("Starting services for new user %s.", u->name);
log_debug("Starting services for new user %s.", u->user_record->user_name);
/* Save the user data so far, because pam_systemd will read the XDG_RUNTIME_DIR out of it while starting up
* systemd --user. We need to do user_save_internal() because we have not "officially" started yet. */
user_save_internal(u);
/* Set slice parameters */
(void) user_update_slice(u);
/* Start user@UID.service */
user_start_service(u);
@ -460,7 +549,7 @@ int user_finalize(User *u) {
* done. This is called as a result of an earlier user_done() when all jobs are completed. */
if (u->started)
log_debug("User %s logged out.", u->name);
log_debug("User %s logged out.", u->user_record->user_name);
LIST_FOREACH(sessions_by_user, s, u->sessions) {
k = session_finalize(s);
@ -474,8 +563,8 @@ int user_finalize(User *u) {
* cases, as we shouldn't accidentally remove a system service's IPC objects while it is running, just because
* a cronjob running as the same user just finished. Hence: exclude system users generally from IPC clean-up,
* and do it only for normal users. */
if (u->manager->remove_ipc && !uid_is_system(u->uid)) {
k = clean_ipc_by_uid(u->uid);
if (u->manager->remove_ipc && !uid_is_system(u->user_record->uid)) {
k = clean_ipc_by_uid(u->user_record->uid);
if (k < 0)
r = k;
}
@ -531,7 +620,7 @@ int user_check_linger_file(User *u) {
_cleanup_free_ char *cc = NULL;
char *p = NULL;
cc = cescape(u->name);
cc = cescape(u->user_record->user_name);
if (!cc)
return -ENOMEM;
@ -567,6 +656,18 @@ static bool user_unit_active(User *u) {
return false;
}
static usec_t user_get_stop_delay(User *u) {
assert(u);
if (u->user_record->stop_delay_usec != UINT64_MAX)
return u->user_record->stop_delay_usec;
if (user_record_removable(u->user_record) > 0)
return 0; /* For removable users lower the stop delay to zero */
return u->manager->user_stop_delay;
}
bool user_may_gc(User *u, bool drop_not_started) {
int r;
@ -579,12 +680,16 @@ bool user_may_gc(User *u, bool drop_not_started) {
return false;
if (u->last_session_timestamp != USEC_INFINITY) {
usec_t user_stop_delay;
/* All sessions have been closed. Let's see if we shall leave the user record around for a bit */
if (u->manager->user_stop_delay == USEC_INFINITY)
user_stop_delay = user_get_stop_delay(u);
if (user_stop_delay == USEC_INFINITY)
return false; /* Leave it around forever! */
if (u->manager->user_stop_delay > 0 &&
now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, u->manager->user_stop_delay))
if (user_stop_delay > 0 &&
now(CLOCK_MONOTONIC) < usec_add(u->last_session_timestamp, user_stop_delay))
return false; /* Leave it around for a bit longer. */
}
@ -712,7 +817,7 @@ void user_elect_display(User *u) {
/* This elects a primary session for each user, which we call the "display". We try to keep the assignment
* stable, but we "upgrade" to better choices. */
log_debug("Electing new display for user %s", u->name);
log_debug("Electing new display for user %s", u->user_record->user_name);
LIST_FOREACH(sessions_by_user, s, u->sessions) {
if (!elect_display_filter(s)) {
@ -737,6 +842,7 @@ static int user_stop_timeout_callback(sd_event_source *es, uint64_t usec, void *
}
void user_update_last_session_timer(User *u) {
usec_t user_stop_delay;
int r;
assert(u);
@ -755,7 +861,8 @@ void user_update_last_session_timer(User *u) {
assert(!u->timer_event_source);
if (IN_SET(u->manager->user_stop_delay, 0, USEC_INFINITY))
user_stop_delay = user_get_stop_delay(u);
if (IN_SET(user_stop_delay, 0, USEC_INFINITY))
return;
if (sd_event_get_state(u->manager->event) == SD_EVENT_FINISHED) {
@ -766,7 +873,7 @@ void user_update_last_session_timer(User *u) {
r = sd_event_add_time(u->manager->event,
&u->timer_event_source,
CLOCK_MONOTONIC,
usec_add(u->last_session_timestamp, u->manager->user_stop_delay), 0,
usec_add(u->last_session_timestamp, user_stop_delay), 0,
user_stop_timeout_callback, u);
if (r < 0)
log_warning_errno(r, "Failed to enqueue user stop event source, ignoring: %m");
@ -775,8 +882,8 @@ void user_update_last_session_timer(User *u) {
char s[FORMAT_TIMESPAN_MAX];
log_debug("Last session of user '%s' logged out, terminating user context in %s.",
u->name,
format_timespan(s, sizeof(s), u->manager->user_stop_delay, USEC_PER_MSEC));
u->user_record->user_name,
format_timespan(s, sizeof(s), user_stop_delay, USEC_PER_MSEC));
}
}

View File

@ -6,6 +6,7 @@ typedef struct User User;
#include "conf-parser.h"
#include "list.h"
#include "logind.h"
#include "user-record.h"
typedef enum UserState {
USER_OFFLINE, /* Not logged in at all */
@ -20,10 +21,9 @@ typedef enum UserState {
struct User {
Manager *manager;
uid_t uid;
gid_t gid;
char *name;
char *home;
UserRecord *user_record;
char *state_file;
char *runtime_path;
@ -50,7 +50,7 @@ struct User {
LIST_FIELDS(User, gc_queue);
};
int user_new(User **out, Manager *m, uid_t uid, gid_t gid, const char *name, const char *home);
int user_new(User **out, Manager *m, UserRecord *ur);
User *user_free(User *u);
DEFINE_TRIVIAL_CLEANUP_FUNC(User *, user_free);

View File

@ -12,6 +12,7 @@
#include "list.h"
#include "set.h"
#include "time-util.h"
#include "user-record.h"
typedef struct Manager Manager;
@ -28,7 +29,7 @@ struct Manager {
Hashmap *seats;
Hashmap *sessions;
Hashmap *sessions_by_leader;
Hashmap *users;
Hashmap *users; /* indexed by UID */
Hashmap *inhibitors;
Hashmap *buttons;
Hashmap *brightness_writers;
@ -130,7 +131,7 @@ int manager_add_device(Manager *m, const char *sysfs, bool master, Device **ret_
int manager_add_button(Manager *m, const char *name, Button **ret_button);
int manager_add_seat(Manager *m, const char *id, Seat **ret_seat);
int manager_add_session(Manager *m, const char *id, Session **ret_session);
int manager_add_user(Manager *m, uid_t uid, gid_t gid, const char *name, const char *home, User **ret_user);
int manager_add_user(Manager *m, UserRecord *ur, User **ret_user);
int manager_add_user_by_name(Manager *m, const char *name, User **ret_user);
int manager_add_user_by_uid(Manager *m, uid_t uid, User **ret_user);
int manager_add_inhibitor(Manager *m, const char* id, Inhibitor **ret_inhibitor);

View File

@ -25,16 +25,22 @@
#include "fd-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "hostname-util.h"
#include "locale-util.h"
#include "login-util.h"
#include "macro.h"
#include "pam-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "process-util.h"
#include "rlimit-util.h"
#include "socket-util.h"
#include "stdio-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "user-util.h"
#include "userdb.h"
#define LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
@ -86,18 +92,15 @@ static int parse_argv(
return 0;
}
static int get_user_data(
static int acquire_user_record(
pam_handle_t *handle,
const char **ret_username,
struct passwd **ret_pw) {
UserRecord **ret_record) {
const char *username = NULL;
struct passwd *pw = NULL;
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
const char *username = NULL, *json = NULL;
int r;
assert(handle);
assert(ret_username);
assert(ret_pw);
r = pam_get_user(handle, &username, NULL);
if (r != PAM_SUCCESS) {
@ -107,17 +110,75 @@ static int get_user_data(
if (isempty(username)) {
pam_syslog(handle, LOG_ERR, "User name not valid.");
return PAM_AUTH_ERR;
return PAM_SERVICE_ERR;
}
pw = pam_modutil_getpwnam(handle, username);
if (!pw) {
pam_syslog(handle, LOG_ERR, "Failed to get user data.");
return PAM_USER_UNKNOWN;
/* If pam_systemd_homed (or some other module) already acqired the user record we can reuse it
* here. */
r = pam_get_data(handle, "systemd-user-record", (const void**) &json);
if (r != PAM_SUCCESS || !json) {
_cleanup_free_ char *formatted = NULL;
if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
pam_syslog(handle, LOG_ERR, "Failed to get PAM user record data: %s", pam_strerror(handle, r));
return r;
}
/* Request the record ourselves */
r = userdb_by_name(username, 0, &ur);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to get user record: %s", strerror_safe(r));
return PAM_USER_UNKNOWN;
}
r = json_variant_format(ur->json, 0, &formatted);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to format user JSON: %s", strerror_safe(r));
return PAM_SERVICE_ERR;
}
/* And cache it for everyone else */
r = pam_set_data(handle, "systemd-user-record", formatted, pam_cleanup_free);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to set PAM user record data: %s", pam_strerror(handle, r));
return r;
}
TAKE_PTR(formatted);
} else {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
/* Parse cached record */
r = json_parse(json, JSON_PARSE_SENSITIVE, &v, NULL, NULL);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to parse JSON user record: %s", strerror_safe(r));
return PAM_SERVICE_ERR;
}
ur = user_record_new();
if (!ur)
return pam_log_oom(handle);
r = user_record_load(ur, v, USER_RECORD_LOAD_REFUSE_SECRET);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to load user record: %s", strerror_safe(r));
return PAM_SERVICE_ERR;
}
/* Safety check if cached record actually matches what we are looking for */
if (!streq_ptr(username, ur->user_name)) {
pam_syslog(handle, LOG_ERR, "Acquired user record does not match user name.");
return PAM_SERVICE_ERR;
}
}
*ret_pw = pw;
*ret_username = username;
if (!uid_is_valid(ur->uid)) {
pam_syslog(handle, LOG_ERR, "Acquired user record does not have a UID.");
return PAM_SERVICE_ERR;
}
if (ret_record)
*ret_record = TAKE_PTR(ur);
return PAM_SUCCESS;
}
@ -233,17 +294,15 @@ static int export_legacy_dbus_address(
return PAM_SUCCESS;
if (asprintf(&t, DEFAULT_USER_BUS_ADDRESS_FMT, runtime) < 0)
goto error;
return pam_log_oom(handle);
r = pam_misc_setenv(handle, "DBUS_SESSION_BUS_ADDRESS", t, 0);
if (r != PAM_SUCCESS)
goto error;
if (r != PAM_SUCCESS) {
pam_syslog(handle, LOG_ERR, "Failed to set bus variable: %s", pam_strerror(handle, r));
return r;
}
return PAM_SUCCESS;
error:
pam_syslog(handle, LOG_ERR, "Failed to set bus variable.");
return r;
}
static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) {
@ -251,36 +310,36 @@ static int append_session_memory_max(pam_handle_t *handle, sd_bus_message *m, co
int r;
if (isempty(limit))
return 0;
return PAM_SUCCESS;
if (streq(limit, "infinity")) {
r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", (uint64_t)-1);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return r;
}
} else {
r = parse_permille(limit);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "MemoryMaxScale", "u", (uint32_t) (((uint64_t) r * UINT32_MAX) / 1000U));
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return r;
}
} else {
r = parse_size(limit, 1024, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", val);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return r;
}
} else
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max: %s, ignoring.", limit);
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
return PAM_SUCCESS;
}
return 0;
r = parse_permille(limit);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "MemoryMaxScale", "u", (uint32_t) (((uint64_t) r * UINT32_MAX) / 1000U));
if (r < 0)
return pam_bus_log_create_error(handle, r);
return PAM_SUCCESS;
}
r = parse_size(limit, 1024, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "MemoryMax", "t", val);
if (r < 0)
return pam_bus_log_create_error(handle, r);
return PAM_SUCCESS;
}
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.memory_max, ignoring: %s", limit);
return PAM_SUCCESS;
}
static int append_session_runtime_max_sec(pam_handle_t *handle, sd_bus_message *m, const char *limit) {
@ -289,19 +348,17 @@ static int append_session_runtime_max_sec(pam_handle_t *handle, sd_bus_message *
/* No need to parse "infinity" here, it will be set by default later in scope_init() */
if (isempty(limit) || streq(limit, "infinity"))
return 0;
return PAM_SUCCESS;
r = parse_sec(limit, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "RuntimeMaxUSec", "t", (uint64_t) val);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return r;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
} else
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.runtime_max_sec: %s, ignoring.", limit);
return 0;
return PAM_SUCCESS;
}
static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, const char *limit) {
@ -310,19 +367,17 @@ static int append_session_tasks_max(pam_handle_t *handle, sd_bus_message *m, con
/* No need to parse "infinity" here, it will be set unconditionally later in manager_start_scope() */
if (isempty(limit) || streq(limit, "infinity"))
return 0;
return PAM_SUCCESS;
r = safe_atou64(limit, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", "TasksMax", "t", val);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return r;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
} else
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max: %s, ignoring.", limit);
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.tasks_max, ignoring: %s", limit);
return 0;
return PAM_SUCCESS;
}
static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, const char *limit, const char *field) {
@ -330,21 +385,19 @@ static int append_session_cg_weight(pam_handle_t *handle, sd_bus_message *m, con
int r;
if (isempty(limit))
return 0;
return PAM_SUCCESS;
r = cg_weight_parse(limit, &val);
if (r >= 0) {
r = sd_bus_message_append(m, "(sv)", field, "t", val);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return r;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
} else if (streq(field, "CPUWeight"))
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.cpu_weight: %s, ignoring.", limit);
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.cpu_weight, ignoring: %s", limit);
else
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight: %s, ignoring.", limit);
pam_syslog(handle, LOG_WARNING, "Failed to parse systemd.io_weight, ignoring: %s", limit);
return 0;
return PAM_SUCCESS;
}
static const char* getenv_harder(pam_handle_t *handle, const char *key, const char *fallback) {
@ -423,6 +476,138 @@ fail:
return false;
}
static int pam_putenv_and_log(pam_handle_t *handle, const char *e, bool debug) {
int r;
assert(handle);
assert(e);
r = pam_putenv(handle, e);
if (r != PAM_SUCCESS) {
pam_syslog(handle, LOG_ERR, "Failed to set PAM environment variable %s: %s", e, pam_strerror(handle, r));
return r;
}
if (debug)
pam_syslog(handle, LOG_DEBUG, "PAM environment variable %s set based on user record.", e);
return PAM_SUCCESS;
}
static int apply_user_record_settings(pam_handle_t *handle, UserRecord *ur, bool debug) {
char **i;
int r;
assert(handle);
assert(ur);
if (ur->umask != MODE_INVALID) {
umask(ur->umask);
if (debug)
pam_syslog(handle, LOG_DEBUG, "Set user umask to %04o based on user record.", ur->umask);
}
STRV_FOREACH(i, ur->environment) {
_cleanup_free_ char *n = NULL;
const char *e;
assert_se(e = strchr(*i, '=')); /* environment was already validated while parsing JSON record, this thus must hold */
n = strndup(*i, e - *i);
if (!n)
return pam_log_oom(handle);
if (pam_getenv(handle, n)) {
if (debug)
pam_syslog(handle, LOG_DEBUG, "PAM environment variable $%s already set, not changing based on record.", *i);
continue;
}
r = pam_putenv_and_log(handle, *i, debug);
if (r != PAM_SUCCESS)
return r;
}
if (ur->email_address) {
if (pam_getenv(handle, "EMAIL")) {
if (debug)
pam_syslog(handle, LOG_DEBUG, "PAM environment variable $EMAIL already set, not changing based on user record.");
} else {
_cleanup_free_ char *joined = NULL;
joined = strjoin("EMAIL=", ur->email_address);
if (!joined)
return pam_log_oom(handle);
r = pam_putenv_and_log(handle, joined, debug);
if (r != PAM_SUCCESS)
return r;
}
}
if (ur->time_zone) {
if (pam_getenv(handle, "TZ")) {
if (debug)
pam_syslog(handle, LOG_DEBUG, "PAM environment variable $TZ already set, not changing based on user record.");
} else if (!timezone_is_valid(ur->time_zone, LOG_DEBUG)) {
if (debug)
pam_syslog(handle, LOG_DEBUG, "Time zone specified in user record is not valid locally, not setting $TZ.");
} else {
_cleanup_free_ char *joined = NULL;
joined = strjoin("TZ=:", ur->time_zone);
if (!joined)
return pam_log_oom(handle);
r = pam_putenv_and_log(handle, joined, debug);
if (r != PAM_SUCCESS)
return r;
}
}
if (ur->preferred_language) {
if (pam_getenv(handle, "LANG")) {
if (debug)
pam_syslog(handle, LOG_DEBUG, "PAM environment variable $LANG already set, not changing based on user record.");
} else if (!locale_is_valid(ur->preferred_language)) {
if (debug)
pam_syslog(handle, LOG_DEBUG, "Preferred language specified in user record is not valid locally, not setting $LANG.");
} else {
_cleanup_free_ char *joined = NULL;
joined = strjoin("LANG=", ur->preferred_language);
if (!joined)
return pam_log_oom(handle);
r = pam_putenv_and_log(handle, joined, debug);
if (r != PAM_SUCCESS)
return r;
}
}
if (nice_is_valid(ur->nice_level)) {
if (nice(ur->nice_level) < 0)
pam_syslog(handle, LOG_ERR, "Failed to set nice level to %i, ignoring: %s", ur->nice_level, strerror_safe(errno));
else if (debug)
pam_syslog(handle, LOG_DEBUG, "Nice level set, based on user record.");
}
for (int rl = 0; rl < _RLIMIT_MAX; rl++) {
if (!ur->rlimits[rl])
continue;
r = setrlimit_closest(rl, ur->rlimits[rl]);
if (r < 0)
pam_syslog(handle, LOG_ERR, "Failed to set resource limit %s, ignoring: %s", rlimit_to_string(rl), strerror_safe(r));
else if (debug)
pam_syslog(handle, LOG_DEBUG, "Resource limit %s set, based on user record.", rlimit_to_string(rl));
}
return PAM_SUCCESS;
}
_public_ PAM_EXTERN int pam_sm_open_session(
pam_handle_t *handle,
int flags,
@ -431,7 +616,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
const char
*username, *id, *object_path, *runtime_path,
*id, *object_path, *runtime_path,
*service = NULL,
*tty = NULL, *display = NULL,
*remote_user = NULL, *remote_host = NULL,
@ -440,9 +625,9 @@ _public_ PAM_EXTERN int pam_sm_open_session(
*class_pam = NULL, *type_pam = NULL, *cvtnr = NULL, *desktop = NULL, *desktop_pam = NULL,
*memory_max = NULL, *tasks_max = NULL, *cpu_weight = NULL, *io_weight = NULL, *runtime_max_sec = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int session_fd = -1, existing, r;
bool debug = false, remote;
struct passwd *pw;
uint32_t vtnr = 0;
uid_t original_uid;
@ -463,7 +648,7 @@ _public_ PAM_EXTERN int pam_sm_open_session(
if (debug)
pam_syslog(handle, LOG_DEBUG, "pam-systemd initializing");
r = get_user_data(handle, &username, &pw);
r = acquire_user_record(handle, &ur);
if (r != PAM_SUCCESS)
return r;
@ -477,8 +662,8 @@ _public_ PAM_EXTERN int pam_sm_open_session(
if (streq_ptr(service, "systemd-user")) {
char rt[STRLEN("/run/user/") + DECIMAL_STR_MAX(uid_t)];
xsprintf(rt, "/run/user/"UID_FMT, pw->pw_uid);
if (validate_runtime_directory(handle, rt, pw->pw_uid)) {
xsprintf(rt, "/run/user/"UID_FMT, ur->uid);
if (validate_runtime_directory(handle, rt, ur->uid)) {
r = pam_misc_setenv(handle, "XDG_RUNTIME_DIR", rt, 0);
if (r != PAM_SUCCESS) {
pam_syslog(handle, LOG_ERR, "Failed to set runtime dir: %s", pam_strerror(handle, r));
@ -486,7 +671,11 @@ _public_ PAM_EXTERN int pam_sm_open_session(
}
}
r = export_legacy_dbus_address(handle, pw->pw_uid, rt);
r = export_legacy_dbus_address(handle, ur->uid, rt);
if (r != PAM_SUCCESS)
return r;
r = apply_user_record_settings(handle, ur, debug);
if (r != PAM_SUCCESS)
return r;
@ -573,16 +762,14 @@ _public_ PAM_EXTERN int pam_sm_open_session(
/* Talk to logind over the message bus */
r = sd_bus_open_system(&bus);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r));
return PAM_SESSION_ERR;
}
r = pam_acquire_bus_connection(handle, &bus);
if (r != PAM_SUCCESS)
return r;
if (debug) {
pam_syslog(handle, LOG_DEBUG, "Asking logind to create session: "
"uid="UID_FMT" pid="PID_FMT" service=%s type=%s class=%s desktop=%s seat=%s vtnr=%"PRIu32" tty=%s display=%s remote=%s remote_user=%s remote_host=%s",
pw->pw_uid, getpid_cached(),
ur->uid, getpid_cached(),
strempty(service),
type, class, strempty(desktop),
strempty(seat), vtnr, strempty(tty), strempty(display),
@ -599,13 +786,11 @@ _public_ PAM_EXTERN int pam_sm_open_session(
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
"CreateSession");
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to create CreateSession method call: %s", strerror_safe(r));
return PAM_SESSION_ERR;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
r = sd_bus_message_append(m, "uusssssussbss",
(uint32_t) pw->pw_uid,
(uint32_t) ur->uid,
0,
service,
type,
@ -618,42 +803,36 @@ _public_ PAM_EXTERN int pam_sm_open_session(
remote,
remote_user,
remote_host);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to append to bus message: %s", strerror_safe(r));
return PAM_SESSION_ERR;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
r = sd_bus_message_open_container(m, 'a', "(sv)");
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to open message container: %s", strerror_safe(r));
return PAM_SYSTEM_ERR;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
r = append_session_memory_max(handle, m, memory_max);
if (r < 0)
return PAM_SESSION_ERR;
if (r != PAM_SUCCESS)
return r;
r = append_session_runtime_max_sec(handle, m, runtime_max_sec);
if (r < 0)
return PAM_SESSION_ERR;
if (r != PAM_SUCCESS)
return r;
r = append_session_tasks_max(handle, m, tasks_max);
if (r < 0)
return PAM_SESSION_ERR;
if (r != PAM_SUCCESS)
return r;
r = append_session_cg_weight(handle, m, cpu_weight, "CPUWeight");
if (r < 0)
return PAM_SESSION_ERR;
if (r != PAM_SUCCESS)
return r;
r = append_session_cg_weight(handle, m, io_weight, "IOWeight");
if (r < 0)
return PAM_SESSION_ERR;
if (r != PAM_SUCCESS)
return r;
r = sd_bus_message_close_container(m);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to close message container: %s", strerror_safe(r));
return PAM_SYSTEM_ERR;
}
if (r < 0)
return pam_bus_log_create_error(handle, r);
r = sd_bus_call(bus, m, LOGIN_SLOW_BUS_CALL_TIMEOUT_USEC, &error, &reply);
if (r < 0) {
@ -677,10 +856,8 @@ _public_ PAM_EXTERN int pam_sm_open_session(
&seat,
&vtnr,
&existing);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to parse message: %s", strerror_safe(r));
return PAM_SESSION_ERR;
}
if (r < 0)
return pam_bus_log_parse_error(handle, r);
if (debug)
pam_syslog(handle, LOG_DEBUG, "Reply from logind: "
@ -691,20 +868,20 @@ _public_ PAM_EXTERN int pam_sm_open_session(
if (r != PAM_SUCCESS)
return r;
if (original_uid == pw->pw_uid) {
if (original_uid == ur->uid) {
/* Don't set $XDG_RUNTIME_DIR if the user we now
* authenticated for does not match the original user
* of the session. We do this in order not to result
* in privileged apps clobbering the runtime directory
* unnecessarily. */
if (validate_runtime_directory(handle, runtime_path, pw->pw_uid)) {
if (validate_runtime_directory(handle, runtime_path, ur->uid)) {
r = update_environment(handle, "XDG_RUNTIME_DIR", runtime_path);
if (r != PAM_SUCCESS)
return r;
}
r = export_legacy_dbus_address(handle, pw->pw_uid, runtime_path);
r = export_legacy_dbus_address(handle, ur->uid, runtime_path);
if (r != PAM_SUCCESS)
return r;
}
@ -759,6 +936,14 @@ _public_ PAM_EXTERN int pam_sm_open_session(
}
}
r = apply_user_record_settings(handle, ur, debug);
if (r != PAM_SUCCESS)
return r;
/* Let's release the D-Bus connection, after all the session might live quite a long time, and we are
* not going to use the bus connection in that time, so let's better close before the daemon kicks us
* off because we are not processing anything. */
(void) pam_release_bus_connection(handle);
return PAM_SUCCESS;
}
@ -767,8 +952,6 @@ _public_ PAM_EXTERN int pam_sm_close_session(
int flags,
int argc, const char **argv) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
const void *existing = NULL;
const char *id;
int r;
@ -781,17 +964,15 @@ _public_ PAM_EXTERN int pam_sm_close_session(
id = pam_getenv(handle, "XDG_SESSION_ID");
if (id && !existing) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
/* Before we go and close the FIFO we need to tell
* logind that this is a clean session shutdown, so
* that it doesn't just go and slaughter us
* immediately after closing the fd */
/* Before we go and close the FIFO we need to tell logind that this is a clean session
* shutdown, so that it doesn't just go and slaughter us immediately after closing the fd */
r = sd_bus_open_system(&bus);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r));
return PAM_SESSION_ERR;
}
r = pam_acquire_bus_connection(handle, &bus);
if (r != PAM_SUCCESS)
return r;
r = sd_bus_call_method(bus,
"org.freedesktop.login1",
@ -808,11 +989,9 @@ _public_ PAM_EXTERN int pam_sm_close_session(
}
}
/* Note that we are knowingly leaking the FIFO fd here. This
* way, logind can watch us die. If we closed it here it would
* not have any clue when that is completed. Given that one
* cannot really have multiple PAM sessions open from the same
* process this means we will leak one FD at max. */
/* Note that we are knowingly leaking the FIFO fd here. This way, logind can watch us die. If we
* closed it here it would not have any clue when that is completed. Given that one cannot really
* have multiple PAM sessions open from the same process this means we will leak one FD at max. */
return PAM_SUCCESS;
}

File diff suppressed because it is too large Load Diff

View File

@ -19,5 +19,6 @@ global:
_nss_systemd_endgrent;
_nss_systemd_setgrent;
_nss_systemd_getgrent_r;
_nss_systemd_initgroups_dyn;
local: *;
};

View File

@ -0,0 +1,344 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "env-util.h"
#include "fd-util.h"
#include "group-record-nss.h"
#include "strv.h"
#include "user-record.h"
#include "userdb-glue.h"
#include "userdb.h"
UserDBFlags nss_glue_userdb_flags(void) {
UserDBFlags flags = USERDB_AVOID_NSS;
/* Make sure that we don't go in circles when allocating a dynamic UID by checking our own database */
if (getenv_bool_secure("SYSTEMD_NSS_DYNAMIC_BYPASS") > 0)
flags |= USERDB_AVOID_DYNAMIC_USER;
return flags;
}
int nss_pack_user_record(
UserRecord *hr,
struct passwd *pwd,
char *buffer,
size_t buflen) {
const char *rn, *hd, *shell;
size_t required;
assert(hr);
assert(pwd);
assert_se(hr->user_name);
required = strlen(hr->user_name) + 1;
assert_se(rn = user_record_real_name(hr));
required += strlen(rn) + 1;
assert_se(hd = user_record_home_directory(hr));
required += strlen(hd) + 1;
assert_se(shell = user_record_shell(hr));
required += strlen(shell) + 1;
if (buflen < required)
return -ERANGE;
*pwd = (struct passwd) {
.pw_name = buffer,
.pw_uid = hr->uid,
.pw_gid = user_record_gid(hr),
.pw_passwd = (char*) "x", /* means: see shadow file */
};
assert(buffer);
pwd->pw_gecos = stpcpy(pwd->pw_name, hr->user_name) + 1;
pwd->pw_dir = stpcpy(pwd->pw_gecos, rn) + 1;
pwd->pw_shell = stpcpy(pwd->pw_dir, hd) + 1;
strcpy(pwd->pw_shell, shell);
return 0;
}
enum nss_status userdb_getpwnam(
const char *name,
struct passwd *pwd,
char *buffer, size_t buflen,
int *errnop) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
assert(pwd);
assert(errnop);
r = userdb_nss_compat_is_enabled();
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
if (!r)
return NSS_STATUS_NOTFOUND;
r = userdb_by_name(name, nss_glue_userdb_flags(), &hr);
if (r == -ESRCH)
return NSS_STATUS_NOTFOUND;
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
r = nss_pack_user_record(hr, pwd, buffer, buflen);
if (r < 0) {
*errnop = -r;
return NSS_STATUS_TRYAGAIN;
}
return NSS_STATUS_SUCCESS;
}
enum nss_status userdb_getpwuid(
uid_t uid,
struct passwd *pwd,
char *buffer,
size_t buflen,
int *errnop) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
assert(pwd);
assert(errnop);
r = userdb_nss_compat_is_enabled();
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
if (!r)
return NSS_STATUS_NOTFOUND;
r = userdb_by_uid(uid, nss_glue_userdb_flags(), &hr);
if (r == -ESRCH)
return NSS_STATUS_NOTFOUND;
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
r = nss_pack_user_record(hr, pwd, buffer, buflen);
if (r < 0) {
*errnop = -r;
return NSS_STATUS_TRYAGAIN;
}
return NSS_STATUS_SUCCESS;
}
int nss_pack_group_record(
GroupRecord *g,
char **extra_members,
struct group *gr,
char *buffer,
size_t buflen) {
char **array = NULL, *p, **m;
size_t required, n = 0, i = 0;
assert(g);
assert(gr);
assert_se(g->group_name);
required = strlen(g->group_name) + 1;
STRV_FOREACH(m, g->members) {
required += sizeof(char*); /* space for ptr array entry */
required += strlen(*m) + 1;
n++;
}
STRV_FOREACH(m, extra_members) {
if (strv_contains(g->members, *m))
continue;
required += sizeof(char*);
required += strlen(*m) + 1;
n++;
}
required += sizeof(char*); /* trailing NULL in ptr array entry */
if (buflen < required)
return -ERANGE;
array = (char**) buffer; /* place ptr array at beginning of buffer, under assumption buffer is aligned */
p = buffer + sizeof(void*) * (n + 1); /* place member strings right after the ptr array */
STRV_FOREACH(m, g->members) {
array[i++] = p;
p = stpcpy(p, *m) + 1;
}
STRV_FOREACH(m, extra_members) {
if (strv_contains(g->members, *m))
continue;
array[i++] = p;
p = stpcpy(p, *m) + 1;
}
assert_se(i == n);
array[n] = NULL;
*gr = (struct group) {
.gr_name = strcpy(p, g->group_name),
.gr_gid = g->gid,
.gr_passwd = (char*) "x", /* means: see shadow file */
.gr_mem = array,
};
return 0;
}
enum nss_status userdb_getgrnam(
const char *name,
struct group *gr,
char *buffer,
size_t buflen,
int *errnop) {
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
_cleanup_strv_free_ char **members = NULL;
int r;
assert(gr);
assert(errnop);
r = userdb_nss_compat_is_enabled();
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
if (!r)
return NSS_STATUS_NOTFOUND;
r = groupdb_by_name(name, nss_glue_userdb_flags(), &g);
if (r < 0 && r != -ESRCH) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
r = membershipdb_by_group_strv(name, nss_glue_userdb_flags(), &members);
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
if (!g) {
_cleanup_close_ int lock_fd = -1;
if (strv_isempty(members))
return NSS_STATUS_NOTFOUND;
/* Grmbl, so we are supposed to extend a group entry, but the group entry itself is not
* accessible via non-NSS. Hence let's do what we have to do, and query NSS after all to
* acquire it, so that we can extend it (that's because glibc's group merging feature will
* merge groups only if both GID and name match and thus we need to have both first). It
* sucks behaving recursively likely this, but it's apparently what everybody does. We break
* the recursion for ourselves via the userdb_nss_compat_disable() lock. */
lock_fd = userdb_nss_compat_disable();
if (lock_fd < 0 && lock_fd != -EBUSY)
return lock_fd;
r = nss_group_record_by_name(name, &g);
if (r == -ESRCH)
return NSS_STATUS_NOTFOUND;
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
}
r = nss_pack_group_record(g, members, gr, buffer, buflen);
if (r < 0) {
*errnop = -r;
return NSS_STATUS_TRYAGAIN;
}
return NSS_STATUS_SUCCESS;
}
enum nss_status userdb_getgrgid(
gid_t gid,
struct group *gr,
char *buffer,
size_t buflen,
int *errnop) {
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
_cleanup_strv_free_ char **members = NULL;
bool from_nss;
int r;
assert(gr);
assert(errnop);
r = userdb_nss_compat_is_enabled();
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
if (r)
return NSS_STATUS_NOTFOUND;
r = groupdb_by_gid(gid, nss_glue_userdb_flags(), &g);
if (r < 0 && r != -ESRCH) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
if (!g) {
_cleanup_close_ int lock_fd = -1;
/* So, quite possibly we have to extend an existing group record with additional members. But
* to do this we need to know the group name first. The group didn't exist via non-NSS
* queries though, hence let's try to acquire it here recursively via NSS. */
lock_fd = userdb_nss_compat_disable();
if (lock_fd < 0 && lock_fd != -EBUSY)
return lock_fd;
r = nss_group_record_by_gid(gid, &g);
if (r == -ESRCH)
return NSS_STATUS_NOTFOUND;
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
from_nss = true;
} else
from_nss = false;
r = membershipdb_by_group_strv(g->group_name, nss_glue_userdb_flags(), &members);
if (r < 0) {
*errnop = -r;
return NSS_STATUS_UNAVAIL;
}
/* If we acquired the record via NSS then there's no reason to respond unless we have to agument the
* list of members of the group */
if (from_nss && strv_isempty(members))
return NSS_STATUS_NOTFOUND;
r = nss_pack_group_record(g, members, gr, buffer, buflen);
if (r < 0) {
*errnop = -r;
return NSS_STATUS_TRYAGAIN;
}
return NSS_STATUS_SUCCESS;
}

View File

@ -0,0 +1,20 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <nss.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include "userdb.h"
UserDBFlags nss_glue_userdb_flags(void);
int nss_pack_user_record(UserRecord *hr, struct passwd *pwd, char *buffer, size_t buflen);
int nss_pack_group_record(GroupRecord *g, char **extra_members, struct group *gr, char *buffer, size_t buflen);
enum nss_status userdb_getpwnam(const char *name, struct passwd *pwd, char *buffer, size_t buflen, int *errnop);
enum nss_status userdb_getpwuid(uid_t uid, struct passwd *pwd, char *buffer, size_t buflen, int *errnop);
enum nss_status userdb_getgrnam(const char *name, struct group *gr, char *buffer, size_t buflen, int *errnop);
enum nss_status userdb_getgrgid(gid_t gid, struct group *gr, char *buffer, size_t buflen, int *errnop);

View File

@ -0,0 +1,203 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "errno-util.h"
#include "group-record-nss.h"
#include "libcrypt-util.h"
#include "strv.h"
int nss_group_to_group_record(
const struct group *grp,
const struct sgrp *sgrp,
GroupRecord **ret) {
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
int r;
assert(grp);
assert(ret);
if (isempty(grp->gr_name))
return -EINVAL;
if (sgrp && !streq_ptr(sgrp->sg_namp, grp->gr_name))
return -EINVAL;
g = group_record_new();
if (!g)
return -ENOMEM;
g->group_name = strdup(grp->gr_name);
if (!g->group_name)
return -ENOMEM;
g->members = strv_copy(grp->gr_mem);
if (!g->members)
return -ENOMEM;
g->gid = grp->gr_gid;
if (sgrp) {
if (hashed_password_valid(sgrp->sg_passwd)) {
g->hashed_password = strv_new(sgrp->sg_passwd);
if (!g->hashed_password)
return -ENOMEM;
}
r = strv_extend_strv(&g->members, sgrp->sg_mem, 1);
if (r < 0)
return r;
g->administrators = strv_copy(sgrp->sg_adm);
if (!g->administrators)
return -ENOMEM;
}
r = json_build(&g->json, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name)),
JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(g->gid)),
JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->members), "members", JSON_BUILD_STRV(g->members)),
JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(g->hashed_password)))),
JSON_BUILD_PAIR_CONDITION(!strv_isempty(g->administrators), "administrators", JSON_BUILD_STRV(g->administrators))));
if (r < 0)
return r;
g->mask = USER_RECORD_REGULAR |
(!strv_isempty(g->hashed_password) ? USER_RECORD_PRIVILEGED : 0);
*ret = TAKE_PTR(g);
return 0;
}
int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer) {
size_t buflen = 4096;
int r;
assert(grp);
assert(ret_sgrp);
assert(ret_buffer);
for (;;) {
_cleanup_free_ char *buf = NULL;
struct sgrp sgrp, *result;
buf = malloc(buflen);
if (!buf)
return -ENOMEM;
r = getsgnam_r(grp->gr_name, &sgrp, buf, buflen, &result);
if (r == 0) {
if (!result)
return -ESRCH;
*ret_sgrp = *result;
*ret_buffer = TAKE_PTR(buf);
return 0;
}
if (r < 0)
return -EIO; /* Weird, this should not return negative! */
if (r != ERANGE)
return -r;
if (buflen > SIZE_MAX / 2)
return -ERANGE;
buflen *= 2;
buf = mfree(buf);
}
}
int nss_group_record_by_name(const char *name, GroupRecord **ret) {
_cleanup_free_ char *buf = NULL, *sbuf = NULL;
struct group grp, *result;
bool incomplete = false;
size_t buflen = 4096;
struct sgrp sgrp;
int r;
assert(name);
assert(ret);
for (;;) {
buf = malloc(buflen);
if (!buf)
return -ENOMEM;
r = getgrnam_r(name, &grp, buf, buflen, &result);
if (r == 0) {
if (!result)
return -ESRCH;
break;
}
if (r < 0)
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrnam_r() returned a negative value");
if (r != ERANGE)
return -r;
if (buflen > SIZE_MAX / 2)
return -ERANGE;
buflen *= 2;
buf = mfree(buf);
}
r = nss_sgrp_for_group(result, &sgrp, &sbuf);
if (r < 0) {
log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name);
incomplete = ERRNO_IS_PRIVILEGE(r);
}
r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret);
if (r < 0)
return r;
(*ret)->incomplete = incomplete;
return 0;
}
int nss_group_record_by_gid(gid_t gid, GroupRecord **ret) {
_cleanup_free_ char *buf = NULL, *sbuf = NULL;
struct group grp, *result;
bool incomplete = false;
size_t buflen = 4096;
struct sgrp sgrp;
int r;
assert(ret);
for (;;) {
buf = malloc(buflen);
if (!buf)
return -ENOMEM;
r = getgrgid_r(gid, &grp, buf, buflen, &result);
if (r == 0) {
if (!result)
return -ESRCH;
break;
}
if (r < 0)
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getgrgid_r() returned a negative value");
if (r != ERANGE)
return -r;
if (buflen > SIZE_MAX / 2)
return -ERANGE;
buflen *= 2;
buf = mfree(buf);
}
r = nss_sgrp_for_group(result, &sgrp, &sbuf);
if (r < 0) {
log_debug_errno(r, "Failed to do shadow lookup for group %s, ignoring: %m", result->gr_name);
incomplete = ERRNO_IS_PRIVILEGE(r);
}
r = nss_group_to_group_record(result, r >= 0 ? &sgrp : NULL, ret);
if (r < 0)
return r;
(*ret)->incomplete = incomplete;
return 0;
}

View File

@ -0,0 +1,15 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <grp.h>
#include <gshadow.h>
#include "group-record.h"
/* Synthesize GroupRecord objects from NSS data */
int nss_group_to_group_record(const struct group *grp, const struct sgrp *sgrp, GroupRecord **ret);
int nss_sgrp_for_group(const struct group *grp, struct sgrp *ret_sgrp, char **ret_buffer);
int nss_group_record_by_name(const char *name, GroupRecord **ret);
int nss_group_record_by_gid(gid_t gid, GroupRecord **ret);

View File

@ -0,0 +1,76 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "format-util.h"
#include "group-record-show.h"
#include "strv.h"
#include "user-util.h"
#include "userdb.h"
void group_record_show(GroupRecord *gr, bool show_full_user_info) {
int r;
printf(" Group name: %s\n",
group_record_group_name_and_realm(gr));
printf(" Disposition: %s\n", user_disposition_to_string(group_record_disposition(gr)));
if (gr->last_change_usec != USEC_INFINITY) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), gr->last_change_usec));
}
if (gid_is_valid(gr->gid))
printf(" GID: " GID_FMT "\n", gr->gid);
if (show_full_user_info) {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
r = membershipdb_by_group(gr->group_name, 0, &iterator);
if (r < 0) {
errno = -r;
printf(" Members: (can't acquire: %m)");
} else {
const char *prefix = " Members:";
for (;;) {
_cleanup_free_ char *user = NULL;
r = membershipdb_iterator_get(iterator, &user, NULL);
if (r == -ESRCH)
break;
if (r < 0) {
errno = -r;
printf("%s (can't iterate: %m\n", prefix);
break;
}
printf("%s %s\n", prefix, user);
prefix = " ";
}
}
} else {
const char *prefix = " Members:";
char **i;
STRV_FOREACH(i, gr->members) {
printf("%s %s\n", prefix, *i);
prefix = " ";
}
}
if (!strv_isempty(gr->administrators)) {
const char *prefix = " Admins:";
char **i;
STRV_FOREACH(i, gr->administrators) {
printf("%s %s\n", prefix, *i);
prefix = " ";
}
}
if (!strv_isempty(gr->hashed_password))
printf(" Passwords: %zu\n", strv_length(gr->hashed_password));
if (gr->service)
printf(" Service: %s\n", gr->service);
}

View File

@ -0,0 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include "group-record.h"
void group_record_show(GroupRecord *gr, bool show_full_user_info);

346
src/shared/group-record.c Normal file
View File

@ -0,0 +1,346 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "group-record.h"
#include "strv.h"
#include "user-util.h"
GroupRecord* group_record_new(void) {
GroupRecord *h;
h = new(GroupRecord, 1);
if (!h)
return NULL;
*h = (GroupRecord) {
.n_ref = 1,
.disposition = _USER_DISPOSITION_INVALID,
.last_change_usec = UINT64_MAX,
.gid = GID_INVALID,
};
return h;
}
static GroupRecord *group_record_free(GroupRecord *g) {
if (!g)
return NULL;
free(g->group_name);
free(g->realm);
free(g->group_name_and_realm_auto);
strv_free(g->members);
free(g->service);
strv_free(g->administrators);
strv_free_erase(g->hashed_password);
json_variant_unref(g->json);
return mfree(g);
}
DEFINE_TRIVIAL_REF_UNREF_FUNC(GroupRecord, group_record, group_record_free);
static int dispatch_privileged(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch privileged_dispatch_table[] = {
{ "hashedPassword", _JSON_VARIANT_TYPE_INVALID, json_dispatch_strv, offsetof(GroupRecord, hashed_password), JSON_SAFE },
{},
};
return json_dispatch(variant, privileged_dispatch_table, NULL, flags, userdata);
}
static int dispatch_binding(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch binding_dispatch_table[] = {
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 },
{},
};
char smid[SD_ID128_STRING_MAX];
JsonVariant *m;
sd_id128_t mid;
int r;
if (!variant)
return 0;
if (!json_variant_is_object(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
r = sd_id128_get_machine(&mid);
if (r < 0)
return json_log(variant, flags, r, "Failed to determine machine ID: %m");
m = json_variant_by_key(variant, sd_id128_to_string(mid, smid));
if (!m)
return 0;
return json_dispatch(m, binding_dispatch_table, NULL, flags, userdata);
}
static int dispatch_per_machine(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch per_machine_dispatch_table[] = {
{ "matchMachineId", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
{ "matchHostname", _JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 },
{ "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 },
{ "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 },
{},
};
JsonVariant *e;
int r;
if (!variant)
return 0;
if (!json_variant_is_array(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
JSON_VARIANT_ARRAY_FOREACH(e, variant) {
bool matching = false;
JsonVariant *m;
if (!json_variant_is_object(e))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of objects.", strna(name));
m = json_variant_by_key(e, "matchMachineId");
if (m) {
r = per_machine_id_match(m, flags);
if (r < 0)
return r;
matching = r > 0;
}
if (!matching) {
m = json_variant_by_key(e, "matchHostname");
if (m) {
r = per_machine_hostname_match(m, flags);
if (r < 0)
return r;
matching = r > 0;
}
}
if (!matching)
continue;
r = json_dispatch(e, per_machine_dispatch_table, NULL, flags, userdata);
if (r < 0)
return r;
}
return 0;
}
static int dispatch_status(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
static const JsonDispatch status_dispatch_table[] = {
{ "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE },
{},
};
char smid[SD_ID128_STRING_MAX];
JsonVariant *m;
sd_id128_t mid;
int r;
if (!variant)
return 0;
if (!json_variant_is_object(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an object.", strna(name));
r = sd_id128_get_machine(&mid);
if (r < 0)
return json_log(variant, flags, r, "Failed to determine machine ID: %m");
m = json_variant_by_key(variant, sd_id128_to_string(mid, smid));
if (!m)
return 0;
return json_dispatch(m, status_dispatch_table, NULL, flags, userdata);
}
static int group_record_augment(GroupRecord *h, JsonDispatchFlags json_flags) {
assert(h);
if (!FLAGS_SET(h->mask, USER_RECORD_REGULAR))
return 0;
assert(h->group_name);
if (!h->group_name_and_realm_auto && h->realm) {
h->group_name_and_realm_auto = strjoin(h->group_name, "@", h->realm);
if (!h->group_name_and_realm_auto)
return json_log_oom(h->json, json_flags);
}
return 0;
}
int group_record_load(
GroupRecord *h,
JsonVariant *v,
UserRecordLoadFlags load_flags) {
static const JsonDispatch group_dispatch_table[] = {
{ "groupName", JSON_VARIANT_STRING, json_dispatch_user_group_name, offsetof(GroupRecord, group_name), 0 },
{ "realm", JSON_VARIANT_STRING, json_dispatch_realm, offsetof(GroupRecord, realm), 0 },
{ "disposition", JSON_VARIANT_STRING, json_dispatch_user_disposition, offsetof(GroupRecord, disposition), 0 },
{ "service", JSON_VARIANT_STRING, json_dispatch_string, offsetof(GroupRecord, service), JSON_SAFE },
{ "lastChangeUSec", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(GroupRecord, last_change_usec), 0 },
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(GroupRecord, gid), 0 },
{ "members", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, members), 0 },
{ "administrators", JSON_VARIANT_ARRAY, json_dispatch_user_group_list, offsetof(GroupRecord, administrators), 0 },
{ "privileged", JSON_VARIANT_OBJECT, dispatch_privileged, 0, 0 },
/* Not defined for now, for groups, but let's at least generate sensible errors about it */
{ "secret", JSON_VARIANT_OBJECT, json_dispatch_unsupported, 0, 0 },
/* Ignore the perMachine, binding and status stuff here, and process it later, so that it overrides whatever is set above */
{ "perMachine", JSON_VARIANT_ARRAY, NULL, 0, 0 },
{ "binding", JSON_VARIANT_OBJECT, NULL, 0, 0 },
{ "status", JSON_VARIANT_OBJECT, NULL, 0, 0 },
/* Ignore 'signature', we check it with explicit accessors instead */
{ "signature", JSON_VARIANT_ARRAY, NULL, 0, 0 },
{},
};
JsonDispatchFlags json_flags = USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(load_flags);
int r;
assert(h);
assert(!h->json);
/* Note that this call will leave a half-initialized record around on failure! */
if ((USER_RECORD_REQUIRE_MASK(load_flags) & (USER_RECORD_SECRET|USER_RECORD_PRIVILEGED)))
return json_log(v, json_flags, SYNTHETIC_ERRNO(EINVAL), "Secret and privileged section currently not available for groups, refusing.");
r = user_group_record_mangle(v, load_flags, &h->json, &h->mask);
if (r < 0)
return r;
r = json_dispatch(h->json, group_dispatch_table, NULL, json_flags, h);
if (r < 0)
return r;
/* During the parsing operation above we ignored the 'perMachine', 'binding' and 'status' fields, since we want
* them to override the global options. Let's process them now. */
r = dispatch_per_machine("perMachine", json_variant_by_key(h->json, "perMachine"), json_flags, h);
if (r < 0)
return r;
r = dispatch_binding("binding", json_variant_by_key(h->json, "binding"), json_flags, h);
if (r < 0)
return r;
r = dispatch_status("status", json_variant_by_key(h->json, "status"), json_flags, h);
if (r < 0)
return r;
if (FLAGS_SET(h->mask, USER_RECORD_REGULAR) && !h->group_name)
return json_log(h->json, json_flags, SYNTHETIC_ERRNO(EINVAL), "Group name field missing, refusing.");
r = group_record_augment(h, json_flags);
if (r < 0)
return r;
return 0;
}
int group_record_build(GroupRecord **ret, ...) {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
va_list ap;
int r;
assert(ret);
va_start(ap, ret);
r = json_buildv(&v, ap);
va_end(ap);
if (r < 0)
return r;
g = group_record_new();
if (!g)
return -ENOMEM;
r = group_record_load(g, v, USER_RECORD_LOAD_FULL);
if (r < 0)
return r;
*ret = TAKE_PTR(g);
return 0;
}
const char *group_record_group_name_and_realm(GroupRecord *h) {
assert(h);
/* Return the pre-initialized joined string if it is defined */
if (h->group_name_and_realm_auto)
return h->group_name_and_realm_auto;
/* If it's not defined then we cannot have a realm */
assert(!h->realm);
return h->group_name;
}
UserDisposition group_record_disposition(GroupRecord *h) {
assert(h);
if (h->disposition >= 0)
return h->disposition;
/* If not declared, derive from GID */
if (!gid_is_valid(h->gid))
return _USER_DISPOSITION_INVALID;
if (h->gid == 0 || h->gid == GID_NOBODY)
return USER_INTRINSIC;
if (gid_is_system(h->gid))
return USER_SYSTEM;
if (gid_is_dynamic(h->gid))
return USER_DYNAMIC;
if (gid_is_container(h->gid))
return USER_CONTAINER;
if (h->gid > INT32_MAX)
return USER_RESERVED;
return USER_REGULAR;
}
int group_record_clone(GroupRecord *h, UserRecordLoadFlags flags, GroupRecord **ret) {
_cleanup_(group_record_unrefp) GroupRecord *c = NULL;
int r;
assert(h);
assert(ret);
c = group_record_new();
if (!c)
return -ENOMEM;
r = group_record_load(c, h->json, flags);
if (r < 0)
return r;
*ret = TAKE_PTR(c);
return 0;
}

44
src/shared/group-record.h Normal file
View File

@ -0,0 +1,44 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include "json.h"
#include "user-record.h"
typedef struct GroupRecord {
unsigned n_ref;
UserRecordMask mask;
bool incomplete;
char *group_name;
char *realm;
char *group_name_and_realm_auto;
UserDisposition disposition;
uint64_t last_change_usec;
gid_t gid;
char **members;
char *service;
/* The following exist mostly so that we can cover the full /etc/gshadow set of fields, we currently
* do not actually make use of these */
char **administrators; /* maps to 'struct sgrp' .sg_adm field */
char **hashed_password; /* maps to 'struct sgrp' .sg_passwd field */
JsonVariant *json;
} GroupRecord;
GroupRecord* group_record_new(void);
GroupRecord* group_record_ref(GroupRecord *g);
GroupRecord* group_record_unref(GroupRecord *g);
DEFINE_TRIVIAL_CLEANUP_FUNC(GroupRecord*, group_record_unref);
int group_record_load(GroupRecord *h, JsonVariant *v, UserRecordLoadFlags flags);
int group_record_build(GroupRecord **ret, ...);
int group_record_clone(GroupRecord *g, UserRecordLoadFlags flags, GroupRecord **ret);
const char *group_record_group_name_and_realm(GroupRecord *h);
UserDisposition group_record_disposition(GroupRecord *h);

View File

@ -0,0 +1,86 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include <errno.h>
#include <stdlib.h>
#include "alloc-util.h"
#include "libcrypt-util.h"
#include "log.h"
#include "macro.h"
#include "missing_stdlib.h"
#include "random-util.h"
#include "string-util.h"
#include "strv.h"
int make_salt(char **ret) {
#ifdef XCRYPT_VERSION_MAJOR
const char *e;
char *salt;
/* If we have libxcrypt we default to the "preferred method" (i.e. usually yescrypt), and generate it
* with crypt_gensalt_ra(). */
e = secure_getenv("SYSTEMD_CRYPT_PREFIX");
if (!e)
e = crypt_preferred_method();
log_debug("Generating salt for hash prefix: %s", e);
salt = crypt_gensalt_ra(e, 0, NULL, 0);
if (!salt)
return -errno;
*ret = salt;
return 0;
#else
/* If libxcrypt is not used, we use SHA512 and generate the salt on our own since crypt_gensalt_ra()
* is not available. */
static const char table[] =
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"0123456789"
"./";
uint8_t raw[16];
char *salt, *j;
size_t i;
int r;
/* This is a bit like crypt_gensalt_ra(), but doesn't require libcrypt, and doesn't do anything but
* SHA512, i.e. is legacy-free and minimizes our deps. */
assert_cc(sizeof(table) == 64U + 1U);
/* Insist on the best randomness by setting RANDOM_BLOCK, this is about keeping passwords secret after all. */
r = genuine_random_bytes(raw, sizeof(raw), RANDOM_BLOCK);
if (r < 0)
return r;
salt = new(char, 3+sizeof(raw)+1+1);
if (!salt)
return -ENOMEM;
/* We only bother with SHA512 hashed passwords, the rest is legacy, and we don't do legacy. */
j = stpcpy(salt, "$6$");
for (i = 0; i < sizeof(raw); i++)
j[i] = table[raw[i] & 63];
j[i++] = '$';
j[i] = 0;
*ret = salt;
return 0;
#endif
}
bool hashed_password_valid(const char *s) {
/* Returns true if the specified string is a 'valid' hashed UNIX password, i.e. if starts with '$' or
* with '!$' (the latter being a valid, yet locked password). */
if (isempty(s))
return false;
return STARTSWITH_SET(s, "$", "!$");
}

View File

@ -0,0 +1,22 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#if HAVE_CRYPT_H
/* libxcrypt is a replacement for glibc's libcrypt, and libcrypt might be
* removed from glibc at some point. As part of the removal, defines for
* crypt(3) are dropped from unistd.h, and we must include crypt.h instead.
*
* Newer versions of glibc (v2.0+) already ship crypt.h with a definition
* of crypt(3) as well, so we simply include it if it is present. MariaDB,
* MySQL, PostgreSQL, Perl and some other wide-spread packages do it the
* same way since ages without any problems.
*/
#include <crypt.h>
#endif
#include <stdbool.h>
#include <stdlib.h>
int make_salt(char **ret);
bool hashed_password_valid(const char *s);

View File

@ -86,6 +86,12 @@ shared_sources = files('''
generator.c
generator.h
gpt.h
group-record-nss.c
group-record-nss.h
group-record-show.c
group-record-show.h
group-record.c
group-record.h
id128-print.c
id128-print.h
ima-util.c
@ -106,6 +112,8 @@ shared_sources = files('''
json-internal.h
json.c
json.h
libcrypt-util.c
libcrypt-util.h
libmount-util.h
linux/auto_dev-ioctl.h
linux/bpf.h
@ -187,6 +195,14 @@ shared_sources = files('''
uid-range.h
unit-file.c
unit-file.h
user-record-nss.c
user-record-nss.h
user-record-show.c
user-record-show.h
user-record.c
user-record.h
userdb.c
userdb.h
utmp-wtmp.h
varlink.c
varlink.h
@ -233,6 +249,13 @@ if conf.get('HAVE_KMOD') == 1
shared_sources += files('module-util.c')
endif
if conf.get('HAVE_PAM') == 1
shared_sources += files('''
pam-util.c
pam-util.h
'''.split())
endif
generate_ip_protocol_list = find_program('generate-ip-protocol-list.sh')
ip_protocol_list_txt = custom_target(
'ip-protocol-list.txt',
@ -279,6 +302,7 @@ libshared_deps = [threads,
libacl,
libblkid,
libcap,
libcrypt,
libcryptsetup,
libgcrypt,
libidn,
@ -288,6 +312,7 @@ libshared_deps = [threads,
libmount,
libopenssl,
libp11kit,
libpam,
librt,
libseccomp,
libselinux,

81
src/shared/pam-util.c Normal file
View File

@ -0,0 +1,81 @@
#include <security/pam_ext.h>
#include <syslog.h>
#include <stdlib.h>
#include "alloc-util.h"
#include "errno-util.h"
#include "macro.h"
#include "pam-util.h"
int pam_log_oom(pam_handle_t *handle) {
/* This is like log_oom(), but uses PAM logging */
pam_syslog(handle, LOG_ERR, "Out of memory.");
return PAM_BUF_ERR;
}
int pam_bus_log_create_error(pam_handle_t *handle, int r) {
/* This is like bus_log_create_error(), but uses PAM logging */
pam_syslog(handle, LOG_ERR, "Failed to create bus message: %s", strerror_safe(r));
return PAM_BUF_ERR;
}
int pam_bus_log_parse_error(pam_handle_t *handle, int r) {
/* This is like bus_log_parse_error(), but uses PAM logging */
pam_syslog(handle, LOG_ERR, "Failed to parse bus message: %s", strerror_safe(r));
return PAM_BUF_ERR;
}
static void cleanup_system_bus(pam_handle_t *handle, void *data, int error_status) {
sd_bus_flush_close_unref(data);
}
int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret) {
_cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
int r;
assert(handle);
assert(ret);
/* We cache the bus connection so that we can share it between the session and the authentication hooks */
r = pam_get_data(handle, "systemd-system-bus", (const void**) &bus);
if (r == PAM_SUCCESS && bus) {
*ret = sd_bus_ref(TAKE_PTR(bus)); /* Increase the reference counter, so that the PAM data stays valid */
return PAM_SUCCESS;
}
if (!IN_SET(r, PAM_SUCCESS, PAM_NO_MODULE_DATA)) {
pam_syslog(handle, LOG_ERR, "Failed to get bus connection: %s", pam_strerror(handle, r));
return r;
}
r = sd_bus_open_system(&bus);
if (r < 0) {
pam_syslog(handle, LOG_ERR, "Failed to connect to system bus: %s", strerror_safe(r));
return PAM_SERVICE_ERR;
}
r = pam_set_data(handle, "systemd-system-bus", bus, cleanup_system_bus);
if (r != PAM_SUCCESS) {
pam_syslog(handle, LOG_ERR, "Failed to set PAM bus data: %s", pam_strerror(handle, r));
return r;
}
sd_bus_ref(bus);
*ret = TAKE_PTR(bus);
return PAM_SUCCESS;
}
int pam_release_bus_connection(pam_handle_t *handle) {
int r;
r = pam_set_data(handle, "systemd-system-bus", NULL, NULL);
if (r != PAM_SUCCESS)
pam_syslog(handle, LOG_ERR, "Failed to release PAM user record data: %s", pam_strerror(handle, r));
return r;
}
void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status) {
/* A generic destructor for pam_set_data() that just frees the specified data */
free(data);
}

15
src/shared/pam-util.h Normal file
View File

@ -0,0 +1,15 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <security/pam_modules.h>
#include "sd-bus.h"
int pam_log_oom(pam_handle_t *handle);
int pam_bus_log_create_error(pam_handle_t *handle, int r);
int pam_bus_log_parse_error(pam_handle_t *handle, int r);
int pam_acquire_bus_connection(pam_handle_t *handle, sd_bus **ret);
int pam_release_bus_connection(pam_handle_t *handle);
void pam_cleanup_free(pam_handle_t *handle, void *data, int error_status);

View File

@ -0,0 +1,288 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "errno-util.h"
#include "format-util.h"
#include "libcrypt-util.h"
#include "strv.h"
#include "user-record-nss.h"
int nss_passwd_to_user_record(
const struct passwd *pwd,
const struct spwd *spwd,
UserRecord **ret) {
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
int r;
assert(pwd);
assert(ret);
if (isempty(pwd->pw_name))
return -EINVAL;
if (spwd && !streq_ptr(spwd->sp_namp, pwd->pw_name))
return -EINVAL;
hr = user_record_new();
if (!hr)
return -ENOMEM;
r = free_and_strdup(&hr->user_name, pwd->pw_name);
if (r < 0)
return r;
if (isempty(pwd->pw_gecos) || streq_ptr(pwd->pw_gecos, hr->user_name))
hr->real_name = mfree(hr->real_name);
else {
r = free_and_strdup(&hr->real_name, pwd->pw_gecos);
if (r < 0)
return r;
}
if (isempty(pwd->pw_dir))
hr->home_directory = mfree(hr->home_directory);
else {
r = free_and_strdup(&hr->home_directory, pwd->pw_dir);
if (r < 0)
return r;
}
if (isempty(pwd->pw_shell))
hr->shell = mfree(hr->shell);
else {
r = free_and_strdup(&hr->shell, pwd->pw_shell);
if (r < 0)
return r;
}
hr->uid = pwd->pw_uid;
hr->gid = pwd->pw_gid;
if (spwd) {
if (hashed_password_valid(spwd->sp_pwdp)) {
strv_free_erase(hr->hashed_password);
hr->hashed_password = strv_new(spwd->sp_pwdp);
if (!hr->hashed_password)
return -ENOMEM;
} else
hr->hashed_password = strv_free_erase(hr->hashed_password);
/* shadow-utils suggests using "chage -E 0" (or -E 1, depending on which man page you check)
* for locking a whole account, hence check for that. Note that it also defines a way to lock
* just a password instead of the whole account, but that's mostly pointless in times of
* password-less authorization, hence let's not bother. */
if (spwd->sp_expire >= 0)
hr->locked = spwd->sp_expire <= 1;
else
hr->locked = -1;
if (spwd->sp_expire > 1 && (uint64_t) spwd->sp_expire < (UINT64_MAX-1)/USEC_PER_DAY)
hr->not_after_usec = spwd->sp_expire * USEC_PER_DAY;
else
hr->not_after_usec = UINT64_MAX;
if (spwd->sp_lstchg >= 0)
hr->password_change_now = spwd->sp_lstchg == 0;
else
hr->password_change_now = -1;
if (spwd->sp_lstchg > 0 && (uint64_t) spwd->sp_lstchg <= (UINT64_MAX-1)/USEC_PER_DAY)
hr->last_password_change_usec = spwd->sp_lstchg * USEC_PER_DAY;
else
hr->last_password_change_usec = UINT64_MAX;
if (spwd->sp_min > 0 && (uint64_t) spwd->sp_min <= (UINT64_MAX-1)/USEC_PER_DAY)
hr->password_change_min_usec = spwd->sp_min * USEC_PER_DAY;
else
hr->password_change_min_usec = UINT64_MAX;
if (spwd->sp_max > 0 && (uint64_t) spwd->sp_max <= (UINT64_MAX-1)/USEC_PER_DAY)
hr->password_change_max_usec = spwd->sp_max * USEC_PER_DAY;
else
hr->password_change_max_usec = UINT64_MAX;
if (spwd->sp_warn > 0 && (uint64_t) spwd->sp_warn <= (UINT64_MAX-1)/USEC_PER_DAY)
hr->password_change_warn_usec = spwd->sp_warn * USEC_PER_DAY;
else
hr->password_change_warn_usec = UINT64_MAX;
if (spwd->sp_inact > 0 && (uint64_t) spwd->sp_inact <= (UINT64_MAX-1)/USEC_PER_DAY)
hr->password_change_inactive_usec = spwd->sp_inact * USEC_PER_DAY;
else
hr->password_change_inactive_usec = UINT64_MAX;
} else {
hr->hashed_password = strv_free_erase(hr->hashed_password);
hr->locked = -1;
hr->not_after_usec = UINT64_MAX;
hr->password_change_now = -1,
hr->last_password_change_usec = UINT64_MAX;
hr->password_change_min_usec = UINT64_MAX;
hr->password_change_max_usec = UINT64_MAX;
hr->password_change_warn_usec = UINT64_MAX;
hr->password_change_inactive_usec = UINT64_MAX;
}
hr->json = json_variant_unref(hr->json);
r = json_build(&hr->json, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(hr->user_name)),
JSON_BUILD_PAIR("uid", JSON_BUILD_UNSIGNED(hr->uid)),
JSON_BUILD_PAIR("gid", JSON_BUILD_UNSIGNED(hr->gid)),
JSON_BUILD_PAIR_CONDITION(hr->real_name, "realName", JSON_BUILD_STRING(hr->real_name)),
JSON_BUILD_PAIR_CONDITION(hr->home_directory, "homeDirectory", JSON_BUILD_STRING(hr->home_directory)),
JSON_BUILD_PAIR_CONDITION(hr->shell, "shell", JSON_BUILD_STRING(hr->shell)),
JSON_BUILD_PAIR_CONDITION(!strv_isempty(hr->hashed_password), "privileged", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("hashedPassword", JSON_BUILD_STRV(hr->hashed_password)))),
JSON_BUILD_PAIR_CONDITION(hr->locked >= 0, "locked", JSON_BUILD_BOOLEAN(hr->locked)),
JSON_BUILD_PAIR_CONDITION(hr->not_after_usec != UINT64_MAX, "notAfterUSec", JSON_BUILD_UNSIGNED(hr->not_after_usec)),
JSON_BUILD_PAIR_CONDITION(hr->password_change_now >= 0, "passwordChangeNow", JSON_BUILD_BOOLEAN(hr->password_change_now)),
JSON_BUILD_PAIR_CONDITION(hr->last_password_change_usec != UINT64_MAX, "lastPasswordChangeUSec", JSON_BUILD_UNSIGNED(hr->last_password_change_usec)),
JSON_BUILD_PAIR_CONDITION(hr->password_change_min_usec != UINT64_MAX, "passwordChangeMinUSec", JSON_BUILD_UNSIGNED(hr->password_change_min_usec)),
JSON_BUILD_PAIR_CONDITION(hr->password_change_max_usec != UINT64_MAX, "passwordChangeMaxUSec", JSON_BUILD_UNSIGNED(hr->password_change_max_usec)),
JSON_BUILD_PAIR_CONDITION(hr->password_change_warn_usec != UINT64_MAX, "passwordChangeWarnUSec", JSON_BUILD_UNSIGNED(hr->password_change_warn_usec)),
JSON_BUILD_PAIR_CONDITION(hr->password_change_inactive_usec != UINT64_MAX, "passwordChangeInactiveUSec", JSON_BUILD_UNSIGNED(hr->password_change_inactive_usec))));
if (r < 0)
return r;
hr->mask = USER_RECORD_REGULAR |
(!strv_isempty(hr->hashed_password) ? USER_RECORD_PRIVILEGED : 0);
*ret = TAKE_PTR(hr);
return 0;
}
int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer) {
size_t buflen = 4096;
int r;
assert(pwd);
assert(ret_spwd);
assert(ret_buffer);
for (;;) {
_cleanup_free_ char *buf = NULL;
struct spwd spwd, *result;
buf = malloc(buflen);
if (!buf)
return -ENOMEM;
r = getspnam_r(pwd->pw_name, &spwd, buf, buflen, &result);
if (r == 0) {
if (!result)
return -ESRCH;
*ret_spwd = *result;
*ret_buffer = TAKE_PTR(buf);
return 0;
}
if (r < 0)
return -EIO; /* Weird, this should not return negative! */
if (r != ERANGE)
return -r;
if (buflen > SIZE_MAX / 2)
return -ERANGE;
buflen *= 2;
buf = mfree(buf);
}
}
int nss_user_record_by_name(const char *name, UserRecord **ret) {
_cleanup_free_ char *buf = NULL, *sbuf = NULL;
struct passwd pwd, *result;
bool incomplete = false;
size_t buflen = 4096;
struct spwd spwd;
int r;
assert(name);
assert(ret);
for (;;) {
buf = malloc(buflen);
if (!buf)
return -ENOMEM;
r = getpwnam_r(name, &pwd, buf, buflen, &result);
if (r == 0) {
if (!result)
return -ESRCH;
break;
}
if (r < 0)
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwnam_r() returned a negative value");
if (r != ERANGE)
return -r;
if (buflen > SIZE_MAX / 2)
return -ERANGE;
buflen *= 2;
buf = mfree(buf);
}
r = nss_spwd_for_passwd(result, &spwd, &sbuf);
if (r < 0) {
log_debug_errno(r, "Failed to do shadow lookup for user %s, ignoring: %m", name);
incomplete = ERRNO_IS_PRIVILEGE(r);
}
r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret);
if (r < 0)
return r;
(*ret)->incomplete = incomplete;
return 0;
}
int nss_user_record_by_uid(uid_t uid, UserRecord **ret) {
_cleanup_free_ char *buf = NULL, *sbuf = NULL;
struct passwd pwd, *result;
bool incomplete = false;
size_t buflen = 4096;
struct spwd spwd;
int r;
assert(ret);
for (;;) {
buf = malloc(buflen);
if (!buf)
return -ENOMEM;
r = getpwuid_r(uid, &pwd, buf, buflen, &result);
if (r == 0) {
if (!result)
return -ESRCH;
break;
}
if (r < 0)
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "getpwuid_r() returned a negative value");
if (r != ERANGE)
return -r;
if (buflen > SIZE_MAX / 2)
return -ERANGE;
buflen *= 2;
buf = mfree(buf);
}
r = nss_spwd_for_passwd(result, &spwd, &sbuf);
if (r < 0) {
log_debug_errno(r, "Failed to do shadow lookup for UID " UID_FMT ", ignoring: %m", uid);
incomplete = ERRNO_IS_PRIVILEGE(r);
}
r = nss_passwd_to_user_record(result, r >= 0 ? &spwd : NULL, ret);
if (r < 0)
return r;
(*ret)->incomplete = incomplete;
return 0;
}

View File

@ -0,0 +1,15 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <pwd.h>
#include <shadow.h>
#include "user-record.h"
/* Synthesizes a UserRecord object from NSS data */
int nss_passwd_to_user_record(const struct passwd *pwd, const struct spwd *spwd, UserRecord **ret);
int nss_spwd_for_passwd(const struct passwd *pwd, struct spwd *ret_spwd, char **ret_buffer);
int nss_user_record_by_name(const char *name, UserRecord **ret);
int nss_user_record_by_uid(uid_t uid, UserRecord **ret);

View File

@ -0,0 +1,466 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include "format-util.h"
#include "fs-util.h"
#include "group-record.h"
#include "process-util.h"
#include "rlimit-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "user-record-show.h"
#include "user-util.h"
#include "userdb.h"
const char *user_record_state_color(const char *state) {
if (STR_IN_SET(state, "unfixated", "absent"))
return ansi_grey();
else if (streq(state, "active"))
return ansi_highlight_green();
else if (streq(state, "locked"))
return ansi_highlight_yellow();
return NULL;
}
void user_record_show(UserRecord *hr, bool show_full_group_info) {
const char *hd, *ip, *shell;
UserStorage storage;
usec_t t;
size_t k;
int r, b;
printf(" User name: %s\n",
user_record_user_name_and_realm(hr));
if (hr->state) {
const char *color;
color = user_record_state_color(hr->state);
printf(" State: %s%s%s\n",
strempty(color), hr->state, color ? ansi_normal() : "");
}
printf(" Disposition: %s\n", user_disposition_to_string(user_record_disposition(hr)));
if (hr->last_change_usec != USEC_INFINITY) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Last Change: %s\n", format_timestamp(buf, sizeof(buf), hr->last_change_usec));
}
if (hr->last_password_change_usec != USEC_INFINITY &&
hr->last_password_change_usec != hr->last_change_usec) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Last Passw.: %s\n", format_timestamp(buf, sizeof(buf), hr->last_password_change_usec));
}
r = user_record_test_blocked(hr);
switch (r) {
case -ESTALE:
printf(" Login OK: %sno%s (last change time is in the future)\n", ansi_highlight_red(), ansi_normal());
break;
case -ENOLCK:
printf(" Login OK: %sno%s (record is locked)\n", ansi_highlight_red(), ansi_normal());
break;
case -EL2HLT:
printf(" Login OK: %sno%s (record not valid yet))\n", ansi_highlight_red(), ansi_normal());
break;
case -EL3HLT:
printf(" Login OK: %sno%s (record not valid anymore))\n", ansi_highlight_red(), ansi_normal());
break;
default: {
usec_t y;
if (r < 0) {
errno = -r;
printf(" Login OK: %sno%s (%m)\n", ansi_highlight_red(), ansi_normal());
break;
}
if (is_nologin_shell(user_record_shell(hr))) {
printf(" Login OK: %sno%s (nologin shell)\n", ansi_highlight_red(), ansi_normal());
break;
}
y = user_record_ratelimit_next_try(hr);
if (y != USEC_INFINITY && y > now(CLOCK_REALTIME)) {
printf(" Login OK: %sno%s (ratelimit)\n", ansi_highlight_red(), ansi_normal());
break;
}
printf(" Login OK: %syes%s\n", ansi_highlight_green(), ansi_normal());
break;
}}
r = user_record_test_password_change_required(hr);
switch (r) {
case -EKEYREVOKED:
printf(" Password OK: %schange now%s\n", ansi_highlight_yellow(), ansi_normal());
break;
case -EOWNERDEAD:
printf(" Password OK: %sexpired%s (change now!)\n", ansi_highlight_yellow(), ansi_normal());
break;
case -EKEYREJECTED:
printf(" Password OK: %sexpired%s (for good)\n", ansi_highlight_red(), ansi_normal());
break;
case -EKEYEXPIRED:
printf(" Password OK: %sexpires soon%s\n", ansi_highlight_yellow(), ansi_normal());
break;
case -ENETDOWN:
printf(" Password OK: %sno timestamp%s\n", ansi_highlight_red(), ansi_normal());
break;
case -EROFS:
printf(" Password OK: %schange not permitted%s\n", ansi_highlight_yellow(), ansi_normal());
break;
default:
if (r < 0) {
errno = -r;
printf(" Password OK: %sno%s (%m)\n", ansi_highlight_yellow(), ansi_normal());
break;
}
printf(" Password OK: %syes%s\n", ansi_highlight_green(), ansi_normal());
break;
}
if (uid_is_valid(hr->uid))
printf(" UID: " UID_FMT "\n", hr->uid);
if (gid_is_valid(hr->gid)) {
if (show_full_group_info) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
r = groupdb_by_gid(hr->gid, 0, &gr);
if (r < 0) {
errno = -r;
printf(" GID: " GID_FMT " (unresolvable: %m)\n", hr->gid);
} else
printf(" GID: " GID_FMT " (%s)\n", hr->gid, gr->group_name);
} else
printf(" GID: " GID_FMT "\n", hr->gid);
} else if (uid_is_valid(hr->uid)) /* Show UID as GID if not separately configured */
printf(" GID: " GID_FMT "\n", (gid_t) hr->uid);
if (show_full_group_info) {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
r = membershipdb_by_user(hr->user_name, 0, &iterator);
if (r < 0) {
errno = -r;
printf(" Aux. Groups: (can't acquire: %m)\n");
} else {
const char *prefix = " Aux. Groups:";
for (;;) {
_cleanup_free_ char *group = NULL;
r = membershipdb_iterator_get(iterator, NULL, &group);
if (r == -ESRCH)
break;
if (r < 0) {
errno = -r;
printf("%s (can't iterate: %m)\n", prefix);
break;
}
printf("%s %s\n", prefix, group);
prefix = " ";
}
}
}
if (hr->real_name && !streq(hr->real_name, hr->user_name))
printf(" Real Name: %s\n", hr->real_name);
hd = user_record_home_directory(hr);
if (hd)
printf(" Directory: %s\n", hd);
storage = user_record_storage(hr);
if (storage >= 0) /* Let's be political, and clarify which storage we like, and which we don't. About CIFS we don't complain. */
printf(" Storage: %s%s\n", user_storage_to_string(storage),
storage == USER_LUKS ? " (strong encryption)" :
storage == USER_FSCRYPT ? " (weak encryption)" :
IN_SET(storage, USER_DIRECTORY, USER_SUBVOLUME) ? " (no encryption)" : "");
ip = user_record_image_path(hr);
if (ip && !streq_ptr(ip, hd))
printf(" Image Path: %s\n", ip);
b = user_record_removable(hr);
if (b >= 0)
printf(" Removable: %s\n", yes_no(b));
shell = user_record_shell(hr);
if (shell)
printf(" Shell: %s\n", shell);
if (hr->email_address)
printf(" Email: %s\n", hr->email_address);
if (hr->location)
printf(" Location: %s\n", hr->location);
if (hr->password_hint)
printf(" Passw. Hint: %s\n", hr->password_hint);
if (hr->icon_name)
printf(" Icon Name: %s\n", hr->icon_name);
if (hr->time_zone)
printf(" Time Zone: %s\n", hr->time_zone);
if (hr->preferred_language)
printf(" Language: %s\n", hr->preferred_language);
if (!strv_isempty(hr->environment)) {
char **i;
STRV_FOREACH(i, hr->environment) {
printf(i == hr->environment ?
" Environment: %s\n" :
" %s\n", *i);
}
}
if (hr->locked >= 0)
printf(" Locked: %s\n", yes_no(hr->locked));
if (hr->not_before_usec != UINT64_MAX) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Not Before: %s\n", format_timestamp(buf, sizeof(buf), hr->not_before_usec));
}
if (hr->not_after_usec != UINT64_MAX) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Not After: %s\n", format_timestamp(buf, sizeof(buf), hr->not_after_usec));
}
if (hr->umask != MODE_INVALID)
printf(" UMask: 0%03o\n", hr->umask);
if (nice_is_valid(hr->nice_level))
printf(" Nice: %i\n", hr->nice_level);
for (int j = 0; j < _RLIMIT_MAX; j++) {
if (hr->rlimits[j])
printf(" Limit: RLIMIT_%s=%" PRIu64 ":%" PRIu64 "\n",
rlimit_to_string(j), (uint64_t) hr->rlimits[j]->rlim_cur, (uint64_t) hr->rlimits[j]->rlim_max);
}
if (hr->tasks_max != UINT64_MAX)
printf(" Tasks Max: %" PRIu64 "\n", hr->tasks_max);
if (hr->memory_high != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" Memory High: %s\n", format_bytes(buf, sizeof(buf), hr->memory_high));
}
if (hr->memory_max != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" Memory Max: %s\n", format_bytes(buf, sizeof(buf), hr->memory_max));
}
if (hr->cpu_weight != UINT64_MAX)
printf(" CPU Weight: %" PRIu64 "\n", hr->cpu_weight);
if (hr->io_weight != UINT64_MAX)
printf(" IO Weight: %" PRIu64 "\n", hr->io_weight);
if (hr->access_mode != MODE_INVALID)
printf(" Access Mode: 0%03oo\n", user_record_access_mode(hr));
if (storage == USER_LUKS) {
printf("LUKS Discard: %s\n", yes_no(user_record_luks_discard(hr)));
if (!sd_id128_is_null(hr->luks_uuid))
printf(" LUKS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->luks_uuid));
if (!sd_id128_is_null(hr->partition_uuid))
printf(" Part UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->partition_uuid));
if (!sd_id128_is_null(hr->file_system_uuid))
printf(" FS UUID: " SD_ID128_FORMAT_STR "\n", SD_ID128_FORMAT_VAL(hr->file_system_uuid));
if (hr->file_system_type)
printf(" File System: %s\n", user_record_file_system_type(hr));
if (hr->luks_cipher)
printf(" LUKS Cipher: %s\n", hr->luks_cipher);
if (hr->luks_cipher_mode)
printf(" Cipher Mode: %s\n", hr->luks_cipher_mode);
if (hr->luks_volume_key_size != UINT64_MAX)
printf(" Volume Key: %" PRIu64 "bit\n", hr->luks_volume_key_size * 8);
if (hr->luks_pbkdf_type)
printf(" PBKDF Type: %s\n", hr->luks_pbkdf_type);
if (hr->luks_pbkdf_hash_algorithm)
printf(" PBKDF Hash: %s\n", hr->luks_pbkdf_hash_algorithm);
if (hr->luks_pbkdf_time_cost_usec != UINT64_MAX) {
char buf[FORMAT_TIMESPAN_MAX];
printf(" PBKDF Time: %s\n", format_timespan(buf, sizeof(buf), hr->luks_pbkdf_time_cost_usec, 0));
}
if (hr->luks_pbkdf_memory_cost != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" PBKDF Bytes: %s\n", format_bytes(buf, sizeof(buf), hr->luks_pbkdf_memory_cost));
}
if (hr->luks_pbkdf_parallel_threads != UINT64_MAX)
printf("PBKDF Thread: %" PRIu64 "\n", hr->luks_pbkdf_parallel_threads);
} else if (storage == USER_CIFS) {
if (hr->cifs_service)
printf("CIFS Service: %s\n", hr->cifs_service);
}
if (hr->cifs_user_name)
printf(" CIFS User: %s\n", user_record_cifs_user_name(hr));
if (hr->cifs_domain)
printf(" CIFS Domain: %s\n", hr->cifs_domain);
if (storage != USER_CLASSIC)
printf(" Mount Flags: %s %s %s\n",
hr->nosuid ? "nosuid" : "suid",
hr->nodev ? "nodev" : "dev",
hr->noexec ? "noexec" : "exec");
if (hr->skeleton_directory)
printf(" Skel. Dir.: %s\n", user_record_skeleton_directory(hr));
if (hr->disk_size != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" Disk Size: %s\n", format_bytes(buf, sizeof(buf), hr->disk_size));
}
if (hr->disk_usage != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" Disk Usage: %s\n", format_bytes(buf, sizeof(buf), hr->disk_usage));
}
if (hr->disk_free != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" Disk Free: %s\n", format_bytes(buf, sizeof(buf), hr->disk_free));
}
if (hr->disk_floor != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf(" Disk Floor: %s\n", format_bytes(buf, sizeof(buf), hr->disk_floor));
}
if (hr->disk_ceiling != UINT64_MAX) {
char buf[FORMAT_BYTES_MAX];
printf("Disk Ceiling: %s\n", format_bytes(buf, sizeof(buf), hr->disk_ceiling));
}
if (hr->good_authentication_counter != UINT64_MAX)
printf(" Good Auth.: %" PRIu64 "\n", hr->good_authentication_counter);
if (hr->last_good_authentication_usec != UINT64_MAX) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Last Good: %s\n", format_timestamp(buf, sizeof(buf), hr->last_good_authentication_usec));
}
if (hr->bad_authentication_counter != UINT64_MAX)
printf(" Bad Auth.: %" PRIu64 "\n", hr->bad_authentication_counter);
if (hr->last_bad_authentication_usec != UINT64_MAX) {
char buf[FORMAT_TIMESTAMP_MAX];
printf(" Last Bad: %s\n", format_timestamp(buf, sizeof(buf), hr->last_bad_authentication_usec));
}
t = user_record_ratelimit_next_try(hr);
if (t != USEC_INFINITY) {
usec_t n = now(CLOCK_REALTIME);
if (t <= n)
printf(" Next Try: anytime\n");
else {
char buf[FORMAT_TIMESPAN_MAX];
printf(" Next Try: %sin %s%s\n",
ansi_highlight_red(),
format_timespan(buf, sizeof(buf), t - n, USEC_PER_SEC),
ansi_normal());
}
}
if (storage != USER_CLASSIC) {
char buf[FORMAT_TIMESPAN_MAX];
printf(" Auth. Limit: %" PRIu64 " attempts per %s\n", user_record_ratelimit_burst(hr),
format_timespan(buf, sizeof(buf), user_record_ratelimit_interval_usec(hr), 0));
}
if (hr->enforce_password_policy >= 0)
printf(" Passwd Pol.: %s\n", yes_no(hr->enforce_password_policy));
if (hr->password_change_min_usec != UINT64_MAX ||
hr->password_change_max_usec != UINT64_MAX ||
hr->password_change_warn_usec != UINT64_MAX ||
hr->password_change_inactive_usec != UINT64_MAX) {
char buf[FORMAT_TIMESPAN_MAX];
printf(" Passwd Chg.:");
if (hr->password_change_min_usec != UINT64_MAX) {
printf(" min %s", format_timespan(buf, sizeof(buf), hr->password_change_min_usec, 0));
if (hr->password_change_max_usec != UINT64_MAX)
printf("");
}
if (hr->password_change_max_usec != UINT64_MAX)
printf(" max %s", format_timespan(buf, sizeof(buf), hr->password_change_max_usec, 0));
if (hr->password_change_warn_usec != UINT64_MAX)
printf("/warn %s", format_timespan(buf, sizeof(buf), hr->password_change_warn_usec, 0));
if (hr->password_change_inactive_usec != UINT64_MAX)
printf("/inactive %s", format_timespan(buf, sizeof(buf), hr->password_change_inactive_usec, 0));
printf("\n");
}
if (hr->password_change_now >= 0)
printf("Pas. Ch. Now: %s\n", yes_no(hr->password_change_now));
if (!strv_isempty(hr->ssh_authorized_keys))
printf("SSH Pub. Key: %zu\n", strv_length(hr->ssh_authorized_keys));
if (!strv_isempty(hr->pkcs11_token_uri)) {
char **i;
STRV_FOREACH(i, hr->pkcs11_token_uri)
printf(i == hr->pkcs11_token_uri ?
" Sec. Token: %s\n" :
" %s\n", *i);
}
k = strv_length(hr->hashed_password);
if (k == 0)
printf(" Passwords: %snone%s\n",
user_record_disposition(hr) == USER_REGULAR ? ansi_highlight_yellow() : ansi_normal(), ansi_normal());
else
printf(" Passwords: %zu\n", k);
if (hr->signed_locally >= 0)
printf(" Local Sig.: %s\n", yes_no(hr->signed_locally));
if (hr->stop_delay_usec != UINT64_MAX) {
char buf[FORMAT_TIMESPAN_MAX];
printf(" Stop Delay: %s\n", format_timespan(buf, sizeof(buf), hr->stop_delay_usec, 0));
}
if (hr->auto_login >= 0)
printf("Autom. Login: %s\n", yes_no(hr->auto_login));
if (hr->kill_processes >= 0)
printf(" Kill Proc.: %s\n", yes_no(hr->kill_processes));
if (hr->service)
printf(" Service: %s\n", hr->service);
}

View File

@ -0,0 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include "user-record.h"
const char *user_record_state_color(const char *state);
void user_record_show(UserRecord *hr, bool show_full_group_info);

1883
src/shared/user-record.c Normal file

File diff suppressed because it is too large Load Diff

375
src/shared/user-record.h Normal file
View File

@ -0,0 +1,375 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <inttypes.h>
#include <sys/types.h>
#include "sd-id128.h"
#include "json.h"
#include "missing_resource.h"
#include "time-util.h"
/* But some limits on disk sizes: not less than 5M, not more than 5T */
#define USER_DISK_SIZE_MIN (UINT64_C(5)*1024*1024)
#define USER_DISK_SIZE_MAX (UINT64_C(5)*1024*1024*1024*1024)
/* The default disk size to use when nothing else is specified, relative to free disk space */
#define USER_DISK_SIZE_DEFAULT_PERCENT 85
typedef enum UserDisposition {
USER_INTRINSIC, /* root and nobody */
USER_SYSTEM, /* statically allocated users for system services */
USER_DYNAMIC, /* dynamically allocated users for system services */
USER_REGULAR, /* regular (typically human users) */
USER_CONTAINER, /* UID ranges allocated for container uses */
USER_RESERVED, /* Range above 2^31 */
_USER_DISPOSITION_MAX,
_USER_DISPOSITION_INVALID = -1,
} UserDisposition;
typedef enum UserHomeStorage {
USER_CLASSIC,
USER_LUKS,
USER_DIRECTORY, /* A directory, and a .identity file in it, which USER_CLASSIC lacks */
USER_SUBVOLUME,
USER_FSCRYPT,
USER_CIFS,
_USER_STORAGE_MAX,
_USER_STORAGE_INVALID = -1
} UserStorage;
typedef enum UserRecordMask {
/* The various sections an identity record may have, as bit mask */
USER_RECORD_REGULAR = 1U << 0,
USER_RECORD_SECRET = 1U << 1,
USER_RECORD_PRIVILEGED = 1U << 2,
USER_RECORD_PER_MACHINE = 1U << 3,
USER_RECORD_BINDING = 1U << 4,
USER_RECORD_STATUS = 1U << 5,
USER_RECORD_SIGNATURE = 1U << 6,
_USER_RECORD_MASK_MAX = (1U << 7)-1
} UserRecordMask;
typedef enum UserRecordLoadFlags {
/* A set of flags used while loading a user record from JSON data. We leave the lower 6 bits free,
* just as a safety precaution so that we can detect borked conversions between UserRecordMask and
* UserRecordLoadFlags. */
/* What to require */
USER_RECORD_REQUIRE_REGULAR = USER_RECORD_REGULAR << 7,
USER_RECORD_REQUIRE_SECRET = USER_RECORD_SECRET << 7,
USER_RECORD_REQUIRE_PRIVILEGED = USER_RECORD_PRIVILEGED << 7,
USER_RECORD_REQUIRE_PER_MACHINE = USER_RECORD_PER_MACHINE << 7,
USER_RECORD_REQUIRE_BINDING = USER_RECORD_BINDING << 7,
USER_RECORD_REQUIRE_STATUS = USER_RECORD_STATUS << 7,
USER_RECORD_REQUIRE_SIGNATURE = USER_RECORD_SIGNATURE << 7,
/* What to allow */
USER_RECORD_ALLOW_REGULAR = USER_RECORD_REGULAR << 14,
USER_RECORD_ALLOW_SECRET = USER_RECORD_SECRET << 14,
USER_RECORD_ALLOW_PRIVILEGED = USER_RECORD_PRIVILEGED << 14,
USER_RECORD_ALLOW_PER_MACHINE = USER_RECORD_PER_MACHINE << 14,
USER_RECORD_ALLOW_BINDING = USER_RECORD_BINDING << 14,
USER_RECORD_ALLOW_STATUS = USER_RECORD_STATUS << 14,
USER_RECORD_ALLOW_SIGNATURE = USER_RECORD_SIGNATURE << 14,
/* What to strip */
USER_RECORD_STRIP_REGULAR = USER_RECORD_REGULAR << 21,
USER_RECORD_STRIP_SECRET = USER_RECORD_SECRET << 21,
USER_RECORD_STRIP_PRIVILEGED = USER_RECORD_PRIVILEGED << 21,
USER_RECORD_STRIP_PER_MACHINE = USER_RECORD_PER_MACHINE << 21,
USER_RECORD_STRIP_BINDING = USER_RECORD_BINDING << 21,
USER_RECORD_STRIP_STATUS = USER_RECORD_STATUS << 21,
USER_RECORD_STRIP_SIGNATURE = USER_RECORD_SIGNATURE << 21,
/* Some special combinations that deserve explicit names */
USER_RECORD_LOAD_FULL = USER_RECORD_REQUIRE_REGULAR |
USER_RECORD_ALLOW_SECRET |
USER_RECORD_ALLOW_PRIVILEGED |
USER_RECORD_ALLOW_PER_MACHINE |
USER_RECORD_ALLOW_BINDING |
USER_RECORD_ALLOW_STATUS |
USER_RECORD_ALLOW_SIGNATURE,
USER_RECORD_LOAD_REFUSE_SECRET = USER_RECORD_REQUIRE_REGULAR |
USER_RECORD_ALLOW_PRIVILEGED |
USER_RECORD_ALLOW_PER_MACHINE |
USER_RECORD_ALLOW_BINDING |
USER_RECORD_ALLOW_STATUS |
USER_RECORD_ALLOW_SIGNATURE,
USER_RECORD_LOAD_MASK_SECRET = USER_RECORD_REQUIRE_REGULAR |
USER_RECORD_ALLOW_PRIVILEGED |
USER_RECORD_ALLOW_PER_MACHINE |
USER_RECORD_ALLOW_BINDING |
USER_RECORD_ALLOW_STATUS |
USER_RECORD_ALLOW_SIGNATURE |
USER_RECORD_STRIP_SECRET,
USER_RECORD_EXTRACT_SECRET = USER_RECORD_REQUIRE_SECRET |
USER_RECORD_STRIP_REGULAR |
USER_RECORD_STRIP_PRIVILEGED |
USER_RECORD_STRIP_PER_MACHINE |
USER_RECORD_STRIP_BINDING |
USER_RECORD_STRIP_STATUS |
USER_RECORD_STRIP_SIGNATURE,
USER_RECORD_LOAD_SIGNABLE = USER_RECORD_REQUIRE_REGULAR |
USER_RECORD_ALLOW_PRIVILEGED |
USER_RECORD_ALLOW_PER_MACHINE,
USER_RECORD_EXTRACT_SIGNABLE = USER_RECORD_LOAD_SIGNABLE |
USER_RECORD_STRIP_SECRET |
USER_RECORD_STRIP_BINDING |
USER_RECORD_STRIP_STATUS |
USER_RECORD_STRIP_SIGNATURE,
USER_RECORD_LOAD_EMBEDDED = USER_RECORD_REQUIRE_REGULAR |
USER_RECORD_ALLOW_PRIVILEGED |
USER_RECORD_ALLOW_PER_MACHINE |
USER_RECORD_ALLOW_SIGNATURE,
USER_RECORD_EXTRACT_EMBEDDED = USER_RECORD_LOAD_EMBEDDED |
USER_RECORD_STRIP_SECRET |
USER_RECORD_STRIP_BINDING |
USER_RECORD_STRIP_STATUS,
/* Whether to log about loader errors beyond LOG_DEBUG */
USER_RECORD_LOG = 1U << 28,
/* Whether to ignore errors and load what we can */
USER_RECORD_PERMISSIVE = 1U << 29,
} UserRecordLoadFlags;
static inline UserRecordLoadFlags USER_RECORD_REQUIRE(UserRecordMask m) {
assert((m & ~_USER_RECORD_MASK_MAX) == 0);
return m << 7;
}
static inline UserRecordLoadFlags USER_RECORD_ALLOW(UserRecordMask m) {
assert((m & ~_USER_RECORD_MASK_MAX) == 0);
return m << 14;
}
static inline UserRecordLoadFlags USER_RECORD_STRIP(UserRecordMask m) {
assert((m & ~_USER_RECORD_MASK_MAX) == 0);
return m << 21;
}
static inline UserRecordMask USER_RECORD_REQUIRE_MASK(UserRecordLoadFlags f) {
return (f >> 7) & _USER_RECORD_MASK_MAX;
}
static inline UserRecordMask USER_RECORD_ALLOW_MASK(UserRecordLoadFlags f) {
return ((f >> 14) & _USER_RECORD_MASK_MAX) | USER_RECORD_REQUIRE_MASK(f);
}
static inline UserRecordMask USER_RECORD_STRIP_MASK(UserRecordLoadFlags f) {
return (f >> 21) & _USER_RECORD_MASK_MAX;
}
static inline JsonDispatchFlags USER_RECORD_LOAD_FLAGS_TO_JSON_DISPATCH_FLAGS(UserRecordLoadFlags flags) {
return (FLAGS_SET(flags, USER_RECORD_LOG) ? JSON_LOG : 0) |
(FLAGS_SET(flags, USER_RECORD_PERMISSIVE) ? JSON_PERMISSIVE : 0);
}
typedef struct Pkcs11EncryptedKey {
/* The encrypted passphrase, which can be decrypted with the private key indicated below */
void *data;
size_t size;
/* Where to find the private key to decrypt the encrypted passphrase above */
char *uri;
/* What to test the decrypted passphrase against to allow access (classic UNIX password hash). Note
* that the decrypted passphrase is also used for unlocking LUKS and fscrypt, and if the account is
* backed by LUKS or fscrypt the hashed password is only an additional layer of authentication, not
* the only. */
char *hashed_password;
} Pkcs11EncryptedKey;
typedef struct UserRecord {
/* The following three fields are not part of the JSON record */
unsigned n_ref;
UserRecordMask mask;
bool incomplete; /* incomplete due to security restrictions. */
char *user_name;
char *realm;
char *user_name_and_realm_auto; /* the user_name field concatenated with '@' and the realm, if the latter is defined */
char *real_name;
char *email_address;
char *password_hint;
char *icon_name;
char *location;
UserDisposition disposition;
uint64_t last_change_usec;
uint64_t last_password_change_usec;
char *shell;
mode_t umask;
char **environment;
char *time_zone;
char *preferred_language;
int nice_level;
struct rlimit *rlimits[_RLIMIT_MAX];
int locked; /* prohibit activation in general */
uint64_t not_before_usec; /* prohibit activation before this unix time */
uint64_t not_after_usec; /* prohibit activation after this unix time */
UserStorage storage;
uint64_t disk_size;
uint64_t disk_size_relative; /* Disk size, relative to the free bytes of the medium, normalized to UINT32_MAX = 100% */
char *skeleton_directory;
mode_t access_mode;
uint64_t tasks_max;
uint64_t memory_high;
uint64_t memory_max;
uint64_t cpu_weight;
uint64_t io_weight;
bool nosuid;
bool nodev;
bool noexec;
char **hashed_password;
char **ssh_authorized_keys;
char **password;
char **pkcs11_pin;
char *cifs_domain;
char *cifs_user_name;
char *cifs_service;
char *image_path;
char *image_path_auto; /* when none is configured explicitly, this is where we place the implicit image */
char *home_directory;
char *home_directory_auto; /* when none is set explicitly, this is where we place the implicit home directory */
uid_t uid;
gid_t gid;
char **member_of;
char *file_system_type;
sd_id128_t partition_uuid;
sd_id128_t luks_uuid;
sd_id128_t file_system_uuid;
int luks_discard;
char *luks_cipher;
char *luks_cipher_mode;
uint64_t luks_volume_key_size;
char *luks_pbkdf_hash_algorithm;
char *luks_pbkdf_type;
uint64_t luks_pbkdf_time_cost_usec;
uint64_t luks_pbkdf_memory_cost;
uint64_t luks_pbkdf_parallel_threads;
uint64_t disk_usage;
uint64_t disk_free;
uint64_t disk_ceiling;
uint64_t disk_floor;
char *state;
char *service;
int signed_locally;
uint64_t good_authentication_counter;
uint64_t bad_authentication_counter;
uint64_t last_good_authentication_usec;
uint64_t last_bad_authentication_usec;
uint64_t ratelimit_begin_usec;
uint64_t ratelimit_count;
uint64_t ratelimit_interval_usec;
uint64_t ratelimit_burst;
int removable;
int enforce_password_policy;
int auto_login;
uint64_t stop_delay_usec; /* How long to leave systemd --user around on log-out */
int kill_processes; /* Whether to kill user processes forcibly on log-out */
/* The following exist mostly so that we can cover the full /etc/shadow set of fields */
uint64_t password_change_min_usec; /* maps to .sp_min */
uint64_t password_change_max_usec; /* maps to .sp_max */
uint64_t password_change_warn_usec; /* maps to .sp_warn */
uint64_t password_change_inactive_usec; /* maps to .sp_inact */
int password_change_now; /* Require a password change immediately on next login (.sp_lstchg = 0) */
char **pkcs11_token_uri;
Pkcs11EncryptedKey *pkcs11_encrypted_key;
size_t n_pkcs11_encrypted_key;
int pkcs11_protected_authentication_path_permitted;
JsonVariant *json;
} UserRecord;
UserRecord* user_record_new(void);
UserRecord* user_record_ref(UserRecord *h);
UserRecord* user_record_unref(UserRecord *h);
DEFINE_TRIVIAL_CLEANUP_FUNC(UserRecord*, user_record_unref);
int user_record_load(UserRecord *h, JsonVariant *v, UserRecordLoadFlags flags);
int user_record_build(UserRecord **ret, ...);
const char *user_record_user_name_and_realm(UserRecord *h);
UserStorage user_record_storage(UserRecord *h);
const char *user_record_file_system_type(UserRecord *h);
const char *user_record_skeleton_directory(UserRecord *h);
mode_t user_record_access_mode(UserRecord *h);
const char *user_record_home_directory(UserRecord *h);
const char *user_record_image_path(UserRecord *h);
unsigned long user_record_mount_flags(UserRecord *h);
const char *user_record_cifs_user_name(UserRecord *h);
const char *user_record_shell(UserRecord *h);
const char *user_record_real_name(UserRecord *h);
bool user_record_luks_discard(UserRecord *h);
const char *user_record_luks_cipher(UserRecord *h);
const char *user_record_luks_cipher_mode(UserRecord *h);
uint64_t user_record_luks_volume_key_size(UserRecord *h);
const char* user_record_luks_pbkdf_type(UserRecord *h);
usec_t user_record_luks_pbkdf_time_cost_usec(UserRecord *h);
uint64_t user_record_luks_pbkdf_memory_cost(UserRecord *h);
uint64_t user_record_luks_pbkdf_parallel_threads(UserRecord *h);
const char *user_record_luks_pbkdf_hash_algorithm(UserRecord *h);
gid_t user_record_gid(UserRecord *h);
UserDisposition user_record_disposition(UserRecord *h);
int user_record_removable(UserRecord *h);
usec_t user_record_ratelimit_interval_usec(UserRecord *h);
uint64_t user_record_ratelimit_burst(UserRecord *h);
bool user_record_can_authenticate(UserRecord *h);
bool user_record_equal(UserRecord *a, UserRecord *b);
bool user_record_compatible(UserRecord *a, UserRecord *b);
int user_record_compare_last_change(UserRecord *a, UserRecord *b);
usec_t user_record_ratelimit_next_try(UserRecord *h);
int user_record_clone(UserRecord *h, UserRecordLoadFlags flags, UserRecord **ret);
int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask);
int user_record_test_blocked(UserRecord *h);
int user_record_test_password_change_required(UserRecord *h);
/* The following six are user by group-record.c, that's why we export them here */
int json_dispatch_realm(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_user_group_list(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int json_dispatch_user_disposition(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata);
int per_machine_id_match(JsonVariant *ids, JsonDispatchFlags flags);
int per_machine_hostname_match(JsonVariant *hns, JsonDispatchFlags flags);
int user_group_record_mangle(JsonVariant *v, UserRecordLoadFlags load_flags, JsonVariant **ret_variant, UserRecordMask *ret_mask);
const char* user_storage_to_string(UserStorage t) _const_;
UserStorage user_storage_from_string(const char *s) _pure_;
const char* user_disposition_to_string(UserDisposition t) _const_;
UserDisposition user_disposition_from_string(const char *s) _pure_;

1347
src/shared/userdb.c Normal file

File diff suppressed because it is too large Load Diff

41
src/shared/userdb.h Normal file
View File

@ -0,0 +1,41 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include <sys/socket.h>
#include <sys/un.h>
#include "group-record.h"
#include "user-record.h"
/* Inquire local services for user/group records */
typedef struct UserDBIterator UserDBIterator;
UserDBIterator *userdb_iterator_free(UserDBIterator *iterator);
DEFINE_TRIVIAL_CLEANUP_FUNC(UserDBIterator*, userdb_iterator_free);
typedef enum UserDBFlags {
USERDB_AVOID_NSS = 1 << 0, /* don't do client-side nor server-side NSS */
USERDB_AVOID_DYNAMIC_USER = 1 << 1, /* exclude looking up in io.systemd.DynamicUser */
USERDB_AVOID_MULTIPLEXER = 1 << 2, /* exclude looking up via io.systemd.Multiplexer */
USERDB_DONT_SYNTHESIZE = 1 << 3, /* don't synthesize root/nobody */
} UserDBFlags;
int userdb_by_name(const char *name, UserDBFlags flags, UserRecord **ret);
int userdb_by_uid(uid_t uid, UserDBFlags flags, UserRecord **ret);
int userdb_all(UserDBFlags flags, UserDBIterator **ret);
int userdb_iterator_get(UserDBIterator *iterator, UserRecord **ret);
int groupdb_by_name(const char *name, UserDBFlags flags, GroupRecord **ret);
int groupdb_by_gid(gid_t gid, UserDBFlags flags, GroupRecord **ret);
int groupdb_all(UserDBFlags flags, UserDBIterator **ret);
int groupdb_iterator_get(UserDBIterator *iterator, GroupRecord **ret);
int membershipdb_by_user(const char *name, UserDBFlags flags, UserDBIterator **ret);
int membershipdb_by_group(const char *name, UserDBFlags flags, UserDBIterator **ret);
int membershipdb_all(UserDBFlags flags, UserDBIterator **ret);
int membershipdb_iterator_get(UserDBIterator *iterator, char **user, char **group);
int membershipdb_by_group_strv(const char *name, UserDBFlags flags, char ***ret);
int userdb_nss_compat_is_enabled(void);
int userdb_nss_compat_disable(void);

View File

@ -2,6 +2,7 @@
#include "alloc-util.h"
#include "format-util.h"
#include "libcrypt-util.h"
#include "log.h"
#include "macro.h"
#include "memory-util.h"

15
src/userdb/meson.build Normal file
View File

@ -0,0 +1,15 @@
# SPDX-License-Identifier: LGPL-2.1+
systemd_userwork_sources = files('''
userwork.c
'''.split())
systemd_userdbd_sources = files('''
userdbd-manager.c
userdbd-manager.h
userdbd.c
'''.split())
userdbctl_sources = files('''
userdbctl.c
'''.split())

790
src/userdb/userdbctl.c Normal file
View File

@ -0,0 +1,790 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include <getopt.h>
#include <utmp.h>
#include "dirent-util.h"
#include "errno-list.h"
#include "fd-util.h"
#include "format-table.h"
#include "format-util.h"
#include "group-record-show.h"
#include "main-func.h"
#include "pager.h"
#include "parse-util.h"
#include "pretty-print.h"
#include "socket-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "user-record-show.h"
#include "user-util.h"
#include "userdb.h"
#include "verbs.h"
static enum {
OUTPUT_CLASSIC,
OUTPUT_TABLE,
OUTPUT_FRIENDLY,
OUTPUT_JSON,
_OUTPUT_INVALID = -1
} arg_output = _OUTPUT_INVALID;
static PagerFlags arg_pager_flags = 0;
static bool arg_legend = true;
static char** arg_services = NULL;
static UserDBFlags arg_userdb_flags = 0;
STATIC_DESTRUCTOR_REGISTER(arg_services, strv_freep);
static int show_user(UserRecord *ur, Table *table) {
int r;
assert(ur);
switch (arg_output) {
case OUTPUT_CLASSIC:
if (!uid_is_valid(ur->uid))
break;
printf("%s:x:" UID_FMT ":" GID_FMT ":%s:%s:%s\n",
ur->user_name,
ur->uid,
user_record_gid(ur),
strempty(user_record_real_name(ur)),
user_record_home_directory(ur),
user_record_shell(ur));
break;
case OUTPUT_JSON:
json_variant_dump(ur->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0);
break;
case OUTPUT_FRIENDLY:
user_record_show(ur, true);
if (ur->incomplete) {
fflush(stdout);
log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
}
break;
case OUTPUT_TABLE:
assert(table);
r = table_add_many(
table,
TABLE_STRING, ur->user_name,
TABLE_STRING, user_disposition_to_string(user_record_disposition(ur)),
TABLE_UID, ur->uid,
TABLE_GID, user_record_gid(ur),
TABLE_STRING, empty_to_null(ur->real_name),
TABLE_STRING, user_record_home_directory(ur),
TABLE_STRING, user_record_shell(ur),
TABLE_INT, (int) user_record_disposition(ur));
if (r < 0)
return log_error_errno(r, "Failed to add row to table: %m");
break;
default:
assert_not_reached("Unexpected output mode");
}
return 0;
}
static int display_user(int argc, char *argv[], void *userdata) {
_cleanup_(table_unrefp) Table *table = NULL;
bool draw_separator = false;
int ret = 0, r;
if (arg_output < 0)
arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
if (arg_output == OUTPUT_TABLE) {
table = table_new("name", "disposition", "uid", "gid", "realname", "home", "shell", "disposition-numeric");
if (!table)
return log_oom();
(void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100);
(void) table_set_align_percent(table, table_get_cell(table, 0, 3), 100);
(void) table_set_empty_string(table, "-");
(void) table_set_sort(table, 7, 2, (size_t) -1);
(void) table_set_display(table, 0, 1, 2, 3, 4, 5, 6, (size_t) -1);
}
if (argc > 1) {
char **i;
STRV_FOREACH(i, argv + 1) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
uid_t uid;
if (parse_uid(*i, &uid) >= 0)
r = userdb_by_uid(uid, arg_userdb_flags, &ur);
else
r = userdb_by_name(*i, arg_userdb_flags, &ur);
if (r < 0) {
if (r == -ESRCH)
log_error_errno(r, "User %s does not exist.", *i);
else if (r == -EHOSTDOWN)
log_error_errno(r, "Selected user database service is not available for this request.");
else
log_error_errno(r, "Failed to find user %s: %m", *i);
if (ret >= 0)
ret = r;
} else {
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
r = show_user(ur, table);
if (r < 0)
return r;
draw_separator = true;
}
}
} else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
r = userdb_all(arg_userdb_flags, &iterator);
if (r < 0)
return log_error_errno(r, "Failed to enumerate users: %m");
for (;;) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
r = userdb_iterator_get(iterator, &ur);
if (r == -ESRCH)
break;
if (r == -EHOSTDOWN)
return log_error_errno(r, "Selected user database service is not available for this request.");
if (r < 0)
return log_error_errno(r, "Failed acquire next user: %m");
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
r = show_user(ur, table);
if (r < 0)
return r;
draw_separator = true;
}
}
if (table) {
r = table_print(table, NULL);
if (r < 0)
return log_error_errno(r, "Failed to show table: %m");
}
return ret;
}
static int show_group(GroupRecord *gr, Table *table) {
int r;
assert(gr);
switch (arg_output) {
case OUTPUT_CLASSIC: {
_cleanup_free_ char *m = NULL;
if (!gid_is_valid(gr->gid))
break;
m = strv_join(gr->members, ",");
if (!m)
return log_oom();
printf("%s:x:" GID_FMT ":%s\n",
gr->group_name,
gr->gid,
m);
break;
}
case OUTPUT_JSON:
json_variant_dump(gr->json, JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_PRETTY, NULL, 0);
break;
case OUTPUT_FRIENDLY:
group_record_show(gr, true);
if (gr->incomplete) {
fflush(stdout);
log_warning("Warning: lacking rights to acquire privileged fields of group record of '%s', output incomplete.", gr->group_name);
}
break;
case OUTPUT_TABLE:
assert(table);
r = table_add_many(
table,
TABLE_STRING, gr->group_name,
TABLE_STRING, user_disposition_to_string(group_record_disposition(gr)),
TABLE_GID, gr->gid,
TABLE_INT, (int) group_record_disposition(gr));
if (r < 0)
return log_error_errno(r, "Failed to add row to table: %m");
break;
default:
assert_not_reached("Unexpected disply mode");
}
return 0;
}
static int display_group(int argc, char *argv[], void *userdata) {
_cleanup_(table_unrefp) Table *table = NULL;
bool draw_separator = false;
int ret = 0, r;
if (arg_output < 0)
arg_output = argc > 1 ? OUTPUT_FRIENDLY : OUTPUT_TABLE;
if (arg_output == OUTPUT_TABLE) {
table = table_new("name", "disposition", "gid", "disposition-numeric");
if (!table)
return log_oom();
(void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100);
(void) table_set_sort(table, 3, 2, (size_t) -1);
(void) table_set_display(table, 0, 1, 2, (size_t) -1);
}
if (argc > 1) {
char **i;
STRV_FOREACH(i, argv + 1) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
gid_t gid;
if (parse_gid(*i, &gid) >= 0)
r = groupdb_by_gid(gid, arg_userdb_flags, &gr);
else
r = groupdb_by_name(*i, arg_userdb_flags, &gr);
if (r < 0) {
if (r == -ESRCH)
log_error_errno(r, "Group %s does not exist.", *i);
else if (r == -EHOSTDOWN)
log_error_errno(r, "Selected group database service is not available for this request.");
else
log_error_errno(r, "Failed to find group %s: %m", *i);
if (ret >= 0)
ret = r;
} else {
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
r = show_group(gr, table);
if (r < 0)
return r;
draw_separator = true;
}
}
} else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
r = groupdb_all(arg_userdb_flags, &iterator);
if (r < 0)
return log_error_errno(r, "Failed to enumerate groups: %m");
for (;;) {
_cleanup_(group_record_unrefp) GroupRecord *gr = NULL;
r = groupdb_iterator_get(iterator, &gr);
if (r == -ESRCH)
break;
if (r == -EHOSTDOWN)
return log_error_errno(r, "Selected group database service is not available for this request.");
if (r < 0)
return log_error_errno(r, "Failed acquire next group: %m");
if (draw_separator && arg_output == OUTPUT_FRIENDLY)
putchar('\n');
r = show_group(gr, table);
if (r < 0)
return r;
draw_separator = true;
}
}
if (table) {
r = table_print(table, NULL);
if (r < 0)
return log_error_errno(r, "Failed to show table: %m");
}
return ret;
}
static int show_membership(const char *user, const char *group, Table *table) {
int r;
assert(user);
assert(group);
switch (arg_output) {
case OUTPUT_CLASSIC:
/* Strictly speaking there's no 'classic' output for this concept, but let's output it in
* similar style to the classic output for user/group info */
printf("%s:%s\n", user, group);
break;
case OUTPUT_JSON: {
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
r = json_build(&v, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("user", JSON_BUILD_STRING(user)),
JSON_BUILD_PAIR("group", JSON_BUILD_STRING(group))));
if (r < 0)
return log_error_errno(r, "Failed to build JSON object: %m");
json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO, NULL, NULL);
break;
}
case OUTPUT_FRIENDLY:
/* Hmm, this is not particularly friendly, but not sure how we could do this better */
printf("%s: %s\n", group, user);
break;
case OUTPUT_TABLE:
assert(table);
r = table_add_many(
table,
TABLE_STRING, user,
TABLE_STRING, group);
if (r < 0)
return log_error_errno(r, "Failed to add row to table: %m");
break;
default:
assert_not_reached("Unexpected output mode");
}
return 0;
}
static int display_memberships(int argc, char *argv[], void *userdata) {
_cleanup_(table_unrefp) Table *table = NULL;
int ret = 0, r;
if (arg_output < 0)
arg_output = OUTPUT_TABLE;
if (arg_output == OUTPUT_TABLE) {
table = table_new("user", "group");
if (!table)
return log_oom();
(void) table_set_sort(table, 0, 1, (size_t) -1);
}
if (argc > 1) {
char **i;
STRV_FOREACH(i, argv + 1) {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
if (streq(argv[0], "users-in-group")) {
r = membershipdb_by_group(*i, arg_userdb_flags, &iterator);
if (r < 0)
return log_error_errno(r, "Failed to enumerate users in group: %m");
} else if (streq(argv[0], "groups-of-user")) {
r = membershipdb_by_user(*i, arg_userdb_flags, &iterator);
if (r < 0)
return log_error_errno(r, "Failed to enumerate groups of user: %m");
} else
assert_not_reached("Unexpected verb");
for (;;) {
_cleanup_free_ char *user = NULL, *group = NULL;
r = membershipdb_iterator_get(iterator, &user, &group);
if (r == -ESRCH)
break;
if (r == -EHOSTDOWN)
return log_error_errno(r, "Selected membership database service is not available for this request.");
if (r < 0)
return log_error_errno(r, "Failed acquire next membership: %m");
r = show_membership(user, group, table);
if (r < 0)
return r;
}
}
} else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
r = membershipdb_all(arg_userdb_flags, &iterator);
if (r < 0)
return log_error_errno(r, "Failed to enumerate memberships: %m");
for (;;) {
_cleanup_free_ char *user = NULL, *group = NULL;
r = membershipdb_iterator_get(iterator, &user, &group);
if (r == -ESRCH)
break;
if (r == -EHOSTDOWN)
return log_error_errno(r, "Selected membership database service is not available for this request.");
if (r < 0)
return log_error_errno(r, "Failed acquire next membership: %m");
r = show_membership(user, group, table);
if (r < 0)
return r;
}
}
if (table) {
r = table_print(table, NULL);
if (r < 0)
return log_error_errno(r, "Failed to show table: %m");
}
return ret;
}
static int display_services(int argc, char *argv[], void *userdata) {
_cleanup_(table_unrefp) Table *t = NULL;
_cleanup_(closedirp) DIR *d = NULL;
struct dirent *de;
int r;
d = opendir("/run/systemd/userdb/");
if (!d) {
if (errno == ENOENT) {
log_info("No services.");
return 0;
}
return log_error_errno(errno, "Failed to open /run/systemd/userdb/: %m");
}
t = table_new("service", "listening");
if (!t)
return log_oom();
(void) table_set_sort(t, 0, (size_t) -1);
FOREACH_DIRENT(de, d, return -errno) {
_cleanup_free_ char *j = NULL, *no = NULL;
union sockaddr_union sockaddr;
_cleanup_close_ int fd = -1;
j = path_join("/run/systemd/userdb/", de->d_name);
if (!j)
return log_oom();
r = sockaddr_un_set_path(&sockaddr.un, j);
if (r < 0)
return log_error_errno(r, "Path %s does not fit in AF_UNIX socket address: %m", j);
fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if (fd < 0)
return log_error_errno(r, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
if (connect(fd, &sockaddr.un, SOCKADDR_UN_LEN(sockaddr.un)) < 0) {
no = strjoin("No (", errno_to_name(errno), ")");
if (!no)
return log_oom();
}
r = table_add_many(t,
TABLE_STRING, de->d_name,
TABLE_STRING, no ?: "yes",
TABLE_SET_COLOR, no ? ansi_highlight_red() : ansi_highlight_green());
if (r < 0)
return log_error_errno(r, "Failed to add table row: %m");
}
if (table_get_rows(t) <= 0) {
log_info("No services.");
return 0;
}
if (arg_output == OUTPUT_JSON)
table_print_json(t, NULL, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR_AUTO);
else
table_print(t, NULL);
return 0;
}
static int ssh_authorized_keys(int argc, char *argv[], void *userdata) {
_cleanup_(user_record_unrefp) UserRecord *ur = NULL;
int r;
if (!valid_user_group_name(argv[1]))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid user name '%s'.", argv[1]);
r = userdb_by_name(argv[1], arg_userdb_flags, &ur);
if (r == -ESRCH)
log_error_errno(r, "User %s does not exist.", argv[1]);
else if (r == -EHOSTDOWN)
log_error_errno(r, "Selected user database service is not available for this request.");
else if (r < 0)
log_error_errno(r, "Failed to find user %s: %m", argv[1]);
if (strv_isempty(ur->ssh_authorized_keys))
log_debug("User record for %s has no public SSH keys.", argv[1]);
else {
char **i;
STRV_FOREACH(i, ur->ssh_authorized_keys)
printf("%s\n", *i);
}
if (ur->incomplete) {
fflush(stdout);
log_warning("Warning: lacking rights to acquire privileged fields of user record of '%s', output incomplete.", ur->user_name);
}
return EXIT_SUCCESS;
}
static int help(int argc, char *argv[], void *userdata) {
_cleanup_free_ char *link = NULL;
int r;
(void) pager_open(arg_pager_flags);
r = terminal_urlify_man("userdbctl", "1", &link);
if (r < 0)
return log_oom();
printf("%s [OPTIONS...] COMMAND ...\n\n"
"%sShow user and group information.%s\n"
"\nCommands:\n"
" user [USER…] Inspect user\n"
" group [GROUP…] Inspect group\n"
" users-in-group [GROUP…] Show users that are members of specified group(s)\n"
" groups-of-user [USER…] Show groups the specified user(s) is a member of\n"
" services Show enabled database services\n"
"\nOptions:\n"
" -h --help Show this help\n"
" --version Show package version\n"
" --no-pager Do not pipe output into a pager\n"
" --no-legend Do not show the headers and footers\n"
" --output=MODE Select output mode (classic, friendly, table, json)\n"
" -j Equivalent to --output=json\n"
" -s --service=SERVICE[:SERVICE…]\n"
" Query the specified service\n"
" --with-nss=BOOL Control whether to include glibc NSS data\n"
" -N Disable inclusion of glibc NSS data and disable synthesizing\n"
" (Same as --with-nss=no --synthesize=no)\n"
" --synthesize=BOOL Synthesize root/nobody user\n"
"\nSee the %s for details.\n"
, program_invocation_short_name
, ansi_highlight(), ansi_normal()
, link
);
return 0;
}
static int parse_argv(int argc, char *argv[]) {
enum {
ARG_VERSION = 0x100,
ARG_NO_PAGER,
ARG_NO_LEGEND,
ARG_OUTPUT,
ARG_WITH_NSS,
ARG_SYNTHESIZE,
};
static const struct option options[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, ARG_VERSION },
{ "no-pager", no_argument, NULL, ARG_NO_PAGER },
{ "no-legend", no_argument, NULL, ARG_NO_LEGEND },
{ "output", required_argument, NULL, ARG_OUTPUT },
{ "service", required_argument, NULL, 's' },
{ "with-nss", required_argument, NULL, ARG_WITH_NSS },
{ "synthesize", required_argument, NULL, ARG_SYNTHESIZE },
{}
};
const char *e;
int r;
assert(argc >= 0);
assert(argv);
/* We are going to update this environment variable with our own, hence let's first read what is already set */
e = getenv("SYSTEMD_ONLY_USERDB");
if (e) {
char **l;
l = strv_split(e, ":");
if (!l)
return log_oom();
strv_free(arg_services);
arg_services = l;
}
for (;;) {
int c;
c = getopt_long(argc, argv, "hjs:N", options, NULL);
if (c < 0)
break;
switch (c) {
case 'h':
return help(0, NULL, NULL);
case ARG_VERSION:
return version();
case ARG_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
case ARG_NO_LEGEND:
arg_legend = false;
break;
case ARG_OUTPUT:
if (streq(optarg, "classic"))
arg_output = OUTPUT_CLASSIC;
else if (streq(optarg, "friendly"))
arg_output = OUTPUT_FRIENDLY;
else if (streq(optarg, "json"))
arg_output = OUTPUT_JSON;
else if (streq(optarg, "table"))
arg_output = OUTPUT_TABLE;
else if (streq(optarg, "help")) {
puts("classic\n"
"friendly\n"
"json");
return 0;
} else
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid --output= mode: %s", optarg);
break;
case 'j':
arg_output = OUTPUT_JSON;
break;
case 's':
if (isempty(optarg))
arg_services = strv_free(arg_services);
else {
char **l;
l = strv_split(optarg, ":");
if (!l)
return log_oom();
r = strv_extend_strv(&arg_services, l, true);
if (r < 0)
return log_oom();
}
break;
case 'N':
arg_userdb_flags |= USERDB_AVOID_NSS|USERDB_DONT_SYNTHESIZE;
break;
case ARG_WITH_NSS:
r = parse_boolean(optarg);
if (r < 0)
return log_error_errno(r, "Failed to parse --with-nss= parameter: %s", optarg);
SET_FLAG(arg_userdb_flags, USERDB_AVOID_NSS, !r);
break;
case ARG_SYNTHESIZE:
r = parse_boolean(optarg);
if (r < 0)
return log_error_errno(r, "Failed to parse --synthesize= parameter: %s", optarg);
SET_FLAG(arg_userdb_flags, USERDB_DONT_SYNTHESIZE, !r);
break;
case '?':
return -EINVAL;
default:
assert_not_reached("Unhandled option");
}
}
return 1;
}
static int run(int argc, char *argv[]) {
static const Verb verbs[] = {
{ "help", VERB_ANY, VERB_ANY, 0, help },
{ "user", VERB_ANY, VERB_ANY, VERB_DEFAULT, display_user },
{ "group", VERB_ANY, VERB_ANY, 0, display_group },
{ "users-in-group", VERB_ANY, VERB_ANY, 0, display_memberships },
{ "groups-of-user", VERB_ANY, VERB_ANY, 0, display_memberships },
{ "services", VERB_ANY, 1, 0, display_services },
/* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a
* user-facing verb and thus should not appear in man pages or --help texts. */
{ "ssh-authorized-keys", 2, 2, 0, ssh_authorized_keys },
{}
};
int r;
log_show_color(true);
log_parse_environment();
log_open();
r = parse_argv(argc, argv);
if (r <= 0)
return r;
if (arg_services) {
_cleanup_free_ char *e = NULL;
e = strv_join(arg_services, ":");
if (!e)
return log_oom();
if (setenv("SYSTEMD_ONLY_USERDB", e, true) < 0)
return log_error_errno(r, "Failed to set $SYSTEMD_ONLY_USERDB: %m");
log_info("Enabled services: %s", e);
} else {
if (unsetenv("SYSTEMD_ONLY_USERDB") < 0)
return log_error_errno(r, "Failed to unset $SYSTEMD_ONLY_USERDB: %m");
}
return dispatch_verb(argc, argv, verbs, NULL);
}
DEFINE_MAIN_FUNCTION(run);

View File

@ -0,0 +1,302 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include <sys/wait.h>
#include "sd-daemon.h"
#include "fd-util.h"
#include "fs-util.h"
#include "mkdir.h"
#include "process-util.h"
#include "set.h"
#include "signal-util.h"
#include "socket-util.h"
#include "stdio-util.h"
#include "umask-util.h"
#include "userdbd-manager.h"
#define LISTEN_TIMEOUT_USEC (25 * USEC_PER_SEC)
static int start_workers(Manager *m, bool explicit_request);
static int on_sigchld(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
Manager *m = userdata;
assert(s);
assert(m);
for (;;) {
siginfo_t siginfo = {};
bool removed = false;
if (waitid(P_ALL, 0, &siginfo, WNOHANG|WEXITED) < 0) {
if (errno == ECHILD)
break;
log_warning_errno(errno, "Failed to invoke waitid(): %m");
break;
}
if (siginfo.si_pid == 0)
break;
if (set_remove(m->workers_dynamic, PID_TO_PTR(siginfo.si_pid)))
removed = true;
if (set_remove(m->workers_fixed, PID_TO_PTR(siginfo.si_pid)))
removed = true;
if (!removed) {
log_warning("Weird, got SIGCHLD for unknown child " PID_FMT ", ignoring.", siginfo.si_pid);
continue;
}
if (siginfo.si_code == CLD_EXITED) {
if (siginfo.si_status == EXIT_SUCCESS)
log_debug("Worker " PID_FMT " exited successfully.", siginfo.si_pid);
else
log_warning("Worker " PID_FMT " died with a failure exit status %i, ignoring.", siginfo.si_pid, siginfo.si_status);
} else if (siginfo.si_code == CLD_KILLED)
log_warning("Worker " PID_FMT " was killed by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status));
else if (siginfo.si_code == CLD_DUMPED)
log_warning("Worker " PID_FMT " dumped core by signal %s, ignoring.", siginfo.si_pid, signal_to_string(siginfo.si_status));
else
log_warning("Can't handle SIGCHLD of this type");
}
(void) start_workers(m, false); /* Fill up workers again if we fell below the low watermark */
return 0;
}
static int on_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
Manager *m = userdata;
assert(s);
assert(m);
(void) start_workers(m, true); /* Workers told us there's more work, let's add one more worker as long as we are below the high watermark */
return 0;
}
int manager_new(Manager **ret) {
Manager *m;
int r;
m = new(Manager, 1);
if (!m)
return -ENOMEM;
*m = (Manager) {
.listen_fd = -1,
.worker_ratelimit = {
.interval = 5 * USEC_PER_SEC,
.burst = 50,
},
};
r = sd_event_new(&m->event);
if (r < 0)
return r;
r = sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
if (r < 0)
return r;
r = sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL);
if (r < 0)
return r;
(void) sd_event_set_watchdog(m->event, true);
m->workers_fixed = set_new(NULL);
m->workers_dynamic = set_new(NULL);
if (!m->workers_fixed || !m->workers_dynamic)
return -ENOMEM;
r = sd_event_add_signal(m->event, &m->sigusr2_event_source, SIGUSR2, on_sigusr2, m);
if (r < 0)
return r;
r = sd_event_add_signal(m->event, &m->sigchld_event_source, SIGCHLD, on_sigchld, m);
if (r < 0)
return r;
*ret = TAKE_PTR(m);
return 0;
}
Manager* manager_free(Manager *m) {
if (!m)
return NULL;
set_free(m->workers_fixed);
set_free(m->workers_dynamic);
sd_event_source_disable_unref(m->sigusr2_event_source);
sd_event_source_disable_unref(m->sigchld_event_source);
sd_event_unref(m->event);
return mfree(m);
}
static size_t manager_current_workers(Manager *m) {
assert(m);
return set_size(m->workers_fixed) + set_size(m->workers_dynamic);
}
static int start_one_worker(Manager *m) {
bool fixed;
pid_t pid;
int r;
assert(m);
fixed = set_size(m->workers_fixed) < USERDB_WORKERS_MIN;
r = safe_fork("(sd-worker)", FORK_RESET_SIGNALS|FORK_DEATHSIG|FORK_LOG, &pid);
if (r < 0)
return log_error_errno(r, "Failed to fork new worker child: %m");
if (r == 0) {
char pids[DECIMAL_STR_MAX(pid_t)];
/* Child */
log_close();
r = close_all_fds(&m->listen_fd, 1);
if (r < 0) {
log_error_errno(r, "Failed to close fds in child: %m");
_exit(EXIT_FAILURE);
}
log_open();
if (m->listen_fd == 3) {
r = fd_cloexec(3, false);
if (r < 0) {
log_error_errno(r, "Failed to turn off O_CLOEXEC for fd 3: %m");
_exit(EXIT_FAILURE);
}
} else {
if (dup2(m->listen_fd, 3) < 0) { /* dup2() creates with O_CLOEXEC off */
log_error_errno(errno, "Failed to move listen fd to 3: %m");
_exit(EXIT_FAILURE);
}
safe_close(m->listen_fd);
}
xsprintf(pids, PID_FMT, pid);
if (setenv("LISTEN_PID", pids, 1) < 0) {
log_error_errno(errno, "Failed to set $LISTEN_PID: %m");
_exit(EXIT_FAILURE);
}
if (setenv("LISTEN_FDS", "1", 1) < 0) {
log_error_errno(errno, "Failed to set $LISTEN_FDS: %m");
_exit(EXIT_FAILURE);
}
if (setenv("USERDB_FIXED_WORKER", one_zero(fixed), 1) < 0) {
log_error_errno(errno, "Failed to set $USERDB_FIXED_WORKER: %m");
_exit(EXIT_FAILURE);
}
/* execl("/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /\* With some extra space rename_process() can make use of *\/ */
/* execl("/usr/bin/valgrind", "valgrind", "/home/lennart/projects/systemd/build/systemd-userwork", "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /\* With some extra space rename_process() can make use of *\/ */
execl(SYSTEMD_USERWORK_PATH, "systemd-userwork", "xxxxxxxxxxxxxxxx", NULL); /* With some extra space rename_process() can make use of */
log_error_errno(errno, "Failed start worker process: %m");
_exit(EXIT_FAILURE);
}
if (fixed)
r = set_put(m->workers_fixed, PID_TO_PTR(pid));
else
r = set_put(m->workers_dynamic, PID_TO_PTR(pid));
if (r < 0)
return log_error_errno(r, "Failed to add child process to set: %m");
return 0;
}
static int start_workers(Manager *m, bool explicit_request) {
int r;
assert(m);
for (;;) {
size_t n;
n = manager_current_workers(m);
if (n >= USERDB_WORKERS_MIN && (!explicit_request || n >= USERDB_WORKERS_MAX))
break;
if (!ratelimit_below(&m->worker_ratelimit)) {
/* If we keep starting workers too often, let's fail the whole daemon, something is wrong */
sd_event_exit(m->event, EXIT_FAILURE);
return log_error_errno(SYNTHETIC_ERRNO(EUCLEAN), "Worker threads requested too frequently, something is wrong.");
}
r = start_one_worker(m);
if (r < 0)
return r;
explicit_request = false;
}
return 0;
}
int manager_startup(Manager *m) {
struct timeval ts;
int n, r;
assert(m);
assert(m->listen_fd < 0);
n = sd_listen_fds(false);
if (n < 0)
return log_error_errno(n, "Failed to determine number of passed file descriptors: %m");
if (n > 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected one listening fd, got %i.", n);
if (n == 1)
m->listen_fd = SD_LISTEN_FDS_START;
else {
union sockaddr_union sockaddr;
r = sockaddr_un_set_path(&sockaddr.un, "/run/systemd/userdb/io.systemd.NameServiceSwitch");
if (r < 0)
return log_error_errno(r, "Cannot assign socket path to socket address: %m");
r = mkdir_p("/run/systemd/userdb", 0755);
if (r < 0)
return log_error_errno(r, "Failed to create /run/systemd/userdb: %m");
m->listen_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
if (m->listen_fd < 0)
return log_error_errno(errno, "Failed to bind on socket: %m");
(void) sockaddr_un_unlink(&sockaddr.un);
RUN_WITH_UMASK(0000)
if (bind(m->listen_fd, &sockaddr.sa, SOCKADDR_UN_LEN(sockaddr.un)) < 0)
return log_error_errno(errno, "Failed to bind socket: %m");
r = symlink_idempotent("io.systemd.NameServiceSwitch", "/run/systemd/userdb/io.systemd.Multiplexer", false);
if (r < 0)
return log_error_errno(r, "Failed to bind io.systemd.Multiplexer: %m");
if (listen(m->listen_fd, SOMAXCONN) < 0)
return log_error_errno(errno, "Failed to listen on socket: %m");
}
/* Let's make sure every accept() call on this socket times out after 25s. This allows workers to be
* GC'ed on idle */
if (setsockopt(m->listen_fd, SOL_SOCKET, SO_RCVTIMEO, timeval_store(&ts, LISTEN_TIMEOUT_USEC), sizeof(ts)) < 0)
return log_error_errno(errno, "Failed to se SO_RCVTIMEO: %m");
return start_workers(m, false);
}

View File

@ -0,0 +1,34 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#pragma once
#include "sd-bus.h"
#include "sd-event.h"
typedef struct Manager Manager;
#include "hashmap.h"
#include "varlink.h"
#include "ratelimit.h"
#define USERDB_WORKERS_MIN 3
#define USERDB_WORKERS_MAX 4096
struct Manager {
sd_event *event;
Set *workers_fixed; /* Workers 0…USERDB_WORKERS_MIN */
Set *workers_dynamic; /* Workers USERD_WORKERS_MIN+1…USERDB_WORKERS_MAX */
sd_event_source *sigusr2_event_source;
sd_event_source *sigchld_event_source;
int listen_fd;
RateLimit worker_ratelimit;
};
int manager_new(Manager **ret);
Manager* manager_free(Manager *m);
DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
int manager_startup(Manager *m);

56
src/userdb/userdbd.c Normal file
View File

@ -0,0 +1,56 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include <sys/stat.h>
#include <sys/types.h>
#include "daemon-util.h"
#include "userdbd-manager.h"
#include "log.h"
#include "main-func.h"
#include "signal-util.h"
/* This service offers two Varlink services, both implementing io.systemd.UserDatabase:
*
* io.systemd.NameServiceSwitch: this is a compatibility interface for glibc NSS: it response to
* name lookups by checking the classic NSS interfaces and responding that.
*
* io.systemd.Multiplexer: this multiplexes lookup requests to all Varlink services that have a
* socket in /run/systemd/userdb/. It's supposed to simplify clients that don't want to implement
* the full iterative logic on their own.
*/
static int run(int argc, char *argv[]) {
_cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
_cleanup_(manager_freep) Manager *m = NULL;
int r;
log_setup_service();
umask(0022);
if (argc != 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
if (setenv("SYSTEMD_BYPASS_USERDB", "io.systemd.NameServiceSwitch:io.systemd.Multiplexer", 1) < 0)
return log_error_errno(errno, "Failed to se $SYSTEMD_BYPASS_USERDB: %m");
assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, SIGTERM, SIGINT, SIGUSR2, -1) >= 0);
r = manager_new(&m);
if (r < 0)
return log_error_errno(r, "Could not create manager: %m");
r = manager_startup(m);
if (r < 0)
return log_error_errno(r, "Failed to start up daemon: %m");
notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING);
r = sd_event_loop(m->event);
if (r < 0)
return log_error_errno(r, "Event loop failed: %m");
return 0;
}
DEFINE_MAIN_FUNCTION(run);

778
src/userdb/userwork.c Normal file
View File

@ -0,0 +1,778 @@
/* SPDX-License-Identifier: LGPL-2.1+ */
#include <poll.h>
#include <sys/wait.h>
#include "sd-daemon.h"
#include "env-util.h"
#include "fd-util.h"
#include "group-record-nss.h"
#include "group-record.h"
#include "main-func.h"
#include "process-util.h"
#include "strv.h"
#include "time-util.h"
#include "user-record-nss.h"
#include "user-record.h"
#include "user-util.h"
#include "userdb.h"
#include "varlink.h"
#define ITERATIONS_MAX 64U
#define RUNTIME_MAX_USEC (5 * USEC_PER_MINUTE)
#define PRESSURE_SLEEP_TIME_USEC (50 * USEC_PER_MSEC)
#define CONNECTION_IDLE_USEC (15 * USEC_PER_SEC)
#define LISTEN_IDLE_USEC (90 * USEC_PER_SEC)
typedef struct LookupParameters {
const char *user_name;
const char *group_name;
union {
uid_t uid;
gid_t gid;
};
const char *service;
} LookupParameters;
static int add_nss_service(JsonVariant **v) {
_cleanup_(json_variant_unrefp) JsonVariant *status = NULL, *z = NULL;
char buf[SD_ID128_STRING_MAX];
sd_id128_t mid;
int r;
assert(v);
/* Patch in service field if it's missing. The assumption here is that this field is unset only for
* NSS records */
if (json_variant_by_key(*v, "service"))
return 0;
r = sd_id128_get_machine(&mid);
if (r < 0)
return r;
status = json_variant_ref(json_variant_by_key(*v, "status"));
z = json_variant_ref(json_variant_by_key(status, sd_id128_to_string(mid, buf)));
if (json_variant_by_key(z, "service"))
return 0;
r = json_variant_set_field_string(&z, "service", "io.systemd.NameServiceSwitch");
if (r < 0)
return r;
r = json_variant_set_field(&status, buf, z);
if (r < 0)
return r;
return json_variant_set_field(v, "status", status);
}
static int build_user_json(Varlink *link, UserRecord *ur, JsonVariant **ret) {
_cleanup_(user_record_unrefp) UserRecord *stripped = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
UserRecordLoadFlags flags;
uid_t peer_uid;
bool trusted;
int r;
assert(ur);
assert(ret);
r = varlink_get_peer_uid(link, &peer_uid);
if (r < 0) {
log_debug_errno(r, "Unable to query peer UID, ignoring: %m");
trusted = false;
} else
trusted = peer_uid == 0 || peer_uid == ur->uid;
flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
if (trusted)
flags |= USER_RECORD_ALLOW_PRIVILEGED;
else
flags |= USER_RECORD_STRIP_PRIVILEGED;
r = user_record_clone(ur, flags, &stripped);
if (r < 0)
return r;
stripped->incomplete =
ur->incomplete ||
(FLAGS_SET(ur->mask, USER_RECORD_PRIVILEGED) &&
!FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED));
v = json_variant_ref(stripped->json);
r = add_nss_service(&v);
if (r < 0)
return r;
return json_build(ret, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)),
JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete))));
}
static int vl_method_get_user_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "uid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, uid), 0 },
{ "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 },
{ "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
{}
};
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(user_record_unrefp) UserRecord *hr = NULL;
LookupParameters p = {
.uid = UID_INVALID,
};
int r;
assert(parameters);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) {
if (uid_is_valid(p.uid))
r = nss_user_record_by_uid(p.uid, &hr);
else if (p.user_name)
r = nss_user_record_by_name(p.user_name, &hr);
else {
_cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
setpwent();
for (;;) {
_cleanup_(user_record_unrefp) UserRecord *z = NULL;
_cleanup_free_ char *sbuf = NULL;
struct passwd *pw;
struct spwd spwd;
errno = 0;
pw = getpwent();
if (!pw) {
if (errno != 0)
log_debug_errno(errno, "Failure while iterating through NSS user database, ignoring: %m");
break;
}
r = nss_spwd_for_passwd(pw, &spwd, &sbuf);
if (r < 0)
log_debug_errno(r, "Failed to acquire shadow entry for user %s, ignoring: %m", pw->pw_name);
r = nss_passwd_to_user_record(pw, NULL, &z);
if (r < 0) {
endpwent();
return r;
}
if (last) {
r = varlink_notify(link, last);
if (r < 0) {
endpwent();
return r;
}
last = json_variant_unref(last);
}
r = build_user_json(link, z, &last);
if (r < 0) {
endpwent();
return r;
}
}
endpwent();
if (!last)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_reply(link, last);
}
} else if (streq_ptr(p.service, "io.systemd.Multiplexer")) {
if (uid_is_valid(p.uid))
r = userdb_by_uid(p.uid, USERDB_AVOID_MULTIPLEXER, &hr);
else if (p.user_name)
r = userdb_by_name(p.user_name, USERDB_AVOID_MULTIPLEXER, &hr);
else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
r = userdb_all(USERDB_AVOID_MULTIPLEXER, &iterator);
if (r < 0)
return r;
for (;;) {
_cleanup_(user_record_unrefp) UserRecord *z = NULL;
r = userdb_iterator_get(iterator, &z);
if (r == -ESRCH)
break;
if (r < 0)
return r;
if (last) {
r = varlink_notify(link, last);
if (r < 0)
return r;
last = json_variant_unref(last);
}
r = build_user_json(link, z, &last);
if (r < 0)
return r;
}
if (!last)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_reply(link, last);
}
} else
return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
if (r == -ESRCH)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
if (r < 0) {
log_debug_errno(r, "User lookup failed abnormally: %m");
return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL);
}
if ((uid_is_valid(p.uid) && hr->uid != p.uid) ||
(p.user_name && !streq(hr->user_name, p.user_name)))
return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
r = build_user_json(link, hr, &v);
if (r < 0)
return r;
return varlink_reply(link, v);
}
static int build_group_json(Varlink *link, GroupRecord *gr, JsonVariant **ret) {
_cleanup_(group_record_unrefp) GroupRecord *stripped = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
UserRecordLoadFlags flags;
uid_t peer_uid;
bool trusted;
int r;
assert(gr);
assert(ret);
r = varlink_get_peer_uid(link, &peer_uid);
if (r < 0) {
log_debug_errno(r, "Unable to query peer UID, ignoring: %m");
trusted = false;
} else
trusted = peer_uid == 0;
flags = USER_RECORD_REQUIRE_REGULAR|USER_RECORD_ALLOW_PER_MACHINE|USER_RECORD_ALLOW_BINDING|USER_RECORD_STRIP_SECRET|USER_RECORD_ALLOW_STATUS|USER_RECORD_ALLOW_SIGNATURE;
if (trusted)
flags |= USER_RECORD_ALLOW_PRIVILEGED;
else
flags |= USER_RECORD_STRIP_PRIVILEGED;
r = group_record_clone(gr, flags, &stripped);
if (r < 0)
return r;
stripped->incomplete =
gr->incomplete ||
(FLAGS_SET(gr->mask, USER_RECORD_PRIVILEGED) &&
!FLAGS_SET(stripped->mask, USER_RECORD_PRIVILEGED));
v = json_variant_ref(gr->json);
r = add_nss_service(&v);
if (r < 0)
return r;
return json_build(ret, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("record", JSON_BUILD_VARIANT(v)),
JSON_BUILD_PAIR("incomplete", JSON_BUILD_BOOLEAN(stripped->incomplete))));
}
static int vl_method_get_group_record(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "gid", JSON_VARIANT_UNSIGNED, json_dispatch_uid_gid, offsetof(LookupParameters, gid), 0 },
{ "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 },
{ "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
{}
};
_cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
LookupParameters p = {
.gid = GID_INVALID,
};
int r;
assert(parameters);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) {
if (gid_is_valid(p.gid))
r = nss_group_record_by_gid(p.gid, &g);
else if (p.group_name)
r = nss_group_record_by_name(p.group_name, &g);
else {
_cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
setgrent();
for (;;) {
_cleanup_(group_record_unrefp) GroupRecord *z = NULL;
_cleanup_free_ char *sbuf = NULL;
struct group *grp;
struct sgrp sgrp;
errno = 0;
grp = getgrent();
if (!grp) {
if (errno != 0)
log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m");
break;
}
r = nss_sgrp_for_group(grp, &sgrp, &sbuf);
if (r < 0)
log_debug_errno(r, "Failed to acquire shadow entry for group %s, ignoring: %m", grp->gr_name);
r = nss_group_to_group_record(grp, r >= 0 ? &sgrp : NULL, &z);
if (r < 0) {
endgrent();
return r;
}
if (last) {
r = varlink_notify(link, last);
if (r < 0) {
endgrent();
return r;
}
last = json_variant_unref(last);
}
r = build_group_json(link, z, &last);
if (r < 0) {
endgrent();
return r;
}
}
endgrent();
if (!last)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_reply(link, last);
}
} else if (streq_ptr(p.service, "io.systemd.Multiplexer")) {
if (gid_is_valid(p.gid))
r = groupdb_by_gid(p.gid, USERDB_AVOID_MULTIPLEXER, &g);
else if (p.group_name)
r = groupdb_by_name(p.group_name, USERDB_AVOID_MULTIPLEXER, &g);
else {
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *last = NULL;
r = groupdb_all(USERDB_AVOID_MULTIPLEXER, &iterator);
if (r < 0)
return r;
for (;;) {
_cleanup_(group_record_unrefp) GroupRecord *z = NULL;
r = groupdb_iterator_get(iterator, &z);
if (r == -ESRCH)
break;
if (r < 0)
return r;
if (last) {
r = varlink_notify(link, last);
if (r < 0)
return r;
last = json_variant_unref(last);
}
r = build_group_json(link, z, &last);
if (r < 0)
return r;
}
if (!last)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_reply(link, last);
}
} else
return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
if (r == -ESRCH)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
if (r < 0) {
log_debug_errno(r, "Group lookup failed abnormally: %m");
return varlink_error(link, "io.systemd.UserDatabase.ServiceNotAvailable", NULL);
}
if ((uid_is_valid(p.gid) && g->gid != p.gid) ||
(p.group_name && !streq(g->group_name, p.group_name)))
return varlink_error(link, "io.systemd.UserDatabase.ConflictingRecordFound", NULL);
r = build_group_json(link, g, &v);
if (r < 0)
return r;
return varlink_reply(link, v);
}
static int vl_method_get_memberships(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
static const JsonDispatch dispatch_table[] = {
{ "userName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, user_name), 0 },
{ "groupName", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, group_name), 0 },
{ "service", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParameters, service), 0 },
{}
};
LookupParameters p = {};
int r;
assert(parameters);
r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
if (r < 0)
return r;
if (streq_ptr(p.service, "io.systemd.NameServiceSwitch")) {
if (p.group_name) {
_cleanup_(group_record_unrefp) GroupRecord *g = NULL;
const char *last = NULL;
char **i;
r = nss_group_record_by_name(p.group_name, &g);
if (r == -ESRCH)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
if (r < 0)
return r;
STRV_FOREACH(i, g->members) {
if (p.user_name && !streq_ptr(p.user_name, *i))
continue;
if (last) {
r = varlink_notifyb(link, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name))));
if (r < 0)
return r;
}
last = *i;
}
if (!last)
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
return varlink_replyb(link, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last)),
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(g->group_name))));
} else {
_cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL;
setgrent();
for (;;) {
struct group *grp;
const char* two[2], **users, **i;
errno = 0;
grp = getgrent();
if (!grp) {
if (errno != 0)
log_debug_errno(errno, "Failure while iterating through NSS group database, ignoring: %m");
break;
}
if (p.user_name) {
if (!strv_contains(grp->gr_mem, p.user_name))
continue;
two[0] = p.user_name;
two[1] = NULL;
users = two;
} else
users = (const char**) grp->gr_mem;
STRV_FOREACH(i, users) {
if (last_user_name) {
assert(last_group_name);
r = varlink_notifyb(link, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
if (r < 0) {
endgrent();
return r;
}
free(last_user_name);
free(last_group_name);
}
last_user_name = strdup(*i);
last_group_name = strdup(grp->gr_name);
if (!last_user_name || !last_group_name) {
endgrent();
return -ENOMEM;
}
}
}
endgrent();
if (!last_user_name) {
assert(!last_group_name);
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
}
assert(last_group_name);
return varlink_replyb(link, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
}
} else if (streq_ptr(p.service, "io.systemd.Multiplexer")) {
_cleanup_free_ char *last_user_name = NULL, *last_group_name = NULL;
_cleanup_(userdb_iterator_freep) UserDBIterator *iterator = NULL;
if (p.group_name)
r = membershipdb_by_group(p.group_name, USERDB_AVOID_MULTIPLEXER, &iterator);
else if (p.user_name)
r = membershipdb_by_user(p.user_name, USERDB_AVOID_MULTIPLEXER, &iterator);
else
r = membershipdb_all(USERDB_AVOID_MULTIPLEXER, &iterator);
if (r < 0)
return r;
for (;;) {
_cleanup_free_ char *user_name = NULL, *group_name = NULL;
r = membershipdb_iterator_get(iterator, &user_name, &group_name);
if (r == -ESRCH)
break;
if (r < 0)
return r;
/* If both group + user are specified do a-posteriori filtering */
if (p.group_name && p.user_name && !streq(group_name, p.group_name))
continue;
if (last_user_name) {
assert(last_group_name);
r = varlink_notifyb(link, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
if (r < 0)
return r;
free(last_user_name);
free(last_group_name);
}
last_user_name = TAKE_PTR(user_name);
last_group_name = TAKE_PTR(group_name);
}
if (!last_user_name) {
assert(!last_group_name);
return varlink_error(link, "io.systemd.UserDatabase.NoRecordFound", NULL);
}
assert(last_group_name);
return varlink_replyb(link, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("userName", JSON_BUILD_STRING(last_user_name)),
JSON_BUILD_PAIR("groupName", JSON_BUILD_STRING(last_group_name))));
}
return varlink_error(link, "io.systemd.UserDatabase.BadService", NULL);
}
static int process_connection(VarlinkServer *server, int fd) {
_cleanup_(varlink_close_unrefp) Varlink *vl = NULL;
int r;
r = varlink_server_add_connection(server, fd, &vl);
if (r < 0) {
fd = safe_close(fd);
return log_error_errno(r, "Failed to add connection: %m");
}
vl = varlink_ref(vl);
for (;;) {
r = varlink_process(vl);
if (r == -ENOTCONN) {
log_debug("Connection terminated.");
break;
}
if (r < 0)
return log_error_errno(r, "Failed to process connection: %m");
if (r > 0)
continue;
r = varlink_wait(vl, CONNECTION_IDLE_USEC);
if (r < 0)
return log_error_errno(r, "Failed to wait for connection events: %m");
if (r == 0)
break;
}
return 0;
}
static int run(int argc, char *argv[]) {
usec_t start_time, listen_idle_usec, last_busy_usec = USEC_INFINITY;
_cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL;
_cleanup_close_ int lock = -1;
unsigned n_iterations = 0;
int m, listen_fd, r;
log_setup_service();
m = sd_listen_fds(false);
if (m < 0)
return log_error_errno(m, "Failed to determine number of listening fds: %m");
if (m == 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No socket to listen on received.");
if (m > 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Worker can only listen on a single socket at a time.");
listen_fd = SD_LISTEN_FDS_START;
r = fd_nonblock(listen_fd, false);
if (r < 0)
return log_error_errno(r, "Failed to turn off non-blocking mode for listening socket: %m");
r = varlink_server_new(&server, 0);
if (r < 0)
return log_error_errno(r, "Failed to allocate server: %m");
r = varlink_server_bind_method_many(
server,
"io.systemd.UserDatabase.GetUserRecord", vl_method_get_user_record,
"io.systemd.UserDatabase.GetGroupRecord", vl_method_get_group_record,
"io.systemd.UserDatabase.GetMemberships", vl_method_get_memberships);
if (r < 0)
return log_error_errno(r, "Failed to bind methods: %m");
r = getenv_bool("USERDB_FIXED_WORKER");
if (r < 0)
return log_error_errno(r, "Failed to parse USERDB_FIXED_WORKER: %m");
listen_idle_usec = r ? USEC_INFINITY : LISTEN_IDLE_USEC;
lock = userdb_nss_compat_disable();
if (lock < 0)
return log_error_errno(r, "Failed to disable userdb NSS compatibility: %m");
start_time = now(CLOCK_MONOTONIC);
for (;;) {
_cleanup_close_ int fd = -1;
usec_t n;
/* Exit the worker in regular intervals, to flush out all memory use */
if (n_iterations++ > ITERATIONS_MAX) {
log_debug("Exiting worker, processed %u iterations, that's enough.", n_iterations);
break;
}
n = now(CLOCK_MONOTONIC);
if (n >= usec_add(start_time, RUNTIME_MAX_USEC)) {
char buf[FORMAT_TIMESPAN_MAX];
log_debug("Exiting worker, ran for %s, that's enough.", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, start_time), 0));
break;
}
if (last_busy_usec == USEC_INFINITY)
last_busy_usec = n;
else if (listen_idle_usec != USEC_INFINITY && n >= usec_add(last_busy_usec, listen_idle_usec)) {
char buf[FORMAT_TIMESPAN_MAX];
log_debug("Exiting worker, been idle for %s, .", format_timespan(buf, sizeof(buf), usec_sub_unsigned(n, last_busy_usec), 0));
break;
}
(void) rename_process("systemd-userwork: waiting...");
fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC);
if (fd < 0)
fd = -errno;
(void) rename_process("systemd-userwork: processing...");
if (fd == -EAGAIN)
continue; /* The listening socket as SO_RECVTIMEO set, hence a time-out is expected
* after a while, let's check if it's time to exit though. */
if (fd == -EINTR)
continue; /* Might be that somebody attached via strace, let's just continue in that
* case */
if (fd < 0)
return log_error_errno(fd, "Failed to accept() from listening socket: %m");
if (now(CLOCK_MONOTONIC) <= usec_add(n, PRESSURE_SLEEP_TIME_USEC)) {
struct pollfd pfd = {
.fd = listen_fd,
.events = POLLIN,
};
/* We only slept a very short time? If so, let's see if there are more sockets
* pending, and if so, let's ask our parent for more workers */
if (poll(&pfd, 1, 0) < 0)
return log_error_errno(errno, "Failed to test for POLLIN on listening socket: %m");
if (FLAGS_SET(pfd.revents, POLLIN)) {
pid_t parent;
parent = getppid();
if (parent <= 1)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Parent already died?");
if (kill(parent, SIGUSR1) < 0)
return log_error_errno(errno, "Failed to kill our own parent.");
}
}
(void) process_connection(server, TAKE_FD(fd));
last_busy_usec = USEC_INFINITY;
}
return 0;
}
DEFINE_MAIN_FUNCTION(run);

View File

@ -96,6 +96,8 @@ units = [
'sockets.target.wants/'],
['systemd-journald.socket', '',
'sockets.target.wants/'],
['systemd-userdbd.socket', 'ENABLE_USERDB',
'sockets.target.wants/'],
['systemd-networkd.socket', 'ENABLE_NETWORKD'],
['systemd-poweroff.service', ''],
['systemd-reboot.service', ''],
@ -182,6 +184,7 @@ in_units = [
['systemd-nspawn@.service', ''],
['systemd-portabled.service', 'ENABLE_PORTABLED',
'dbus-org.freedesktop.portable1.service'],
['systemd-userdbd.service', 'ENABLE_USERDB'],
['systemd-quotacheck.service', 'ENABLE_QUOTACHECK'],
['systemd-random-seed.service', 'ENABLE_RANDOMSEED',
'sysinit.target.wants/'],

View File

@ -0,0 +1,41 @@
# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=User Database Manager
Documentation=man:systemd-userdbd.service(8)
Requires=systemd-userdbd.socket
After=systemd-userdbd.socket
Before=sysinit.target
DefaultDependencies=no
[Service]
CapabilityBoundingSet=CAP_DAC_READ_SEARCH
ExecStart=@rootlibexecdir@/systemd-userdbd
IPAddressDeny=any
LimitNOFILE=@HIGH_RLIMIT_NOFILE@
LockPersonality=yes
MemoryDenyWriteExecute=yes
NoNewPrivileges=yes
PrivateDevices=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectHostname=yes
ProtectKernelLogs=yes
ProtectKernelModules=yes
ProtectSystem=strict
RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
SystemCallFilter=@system-service
Type=notify
@SERVICE_WATCHDOG@

View File

@ -0,0 +1,19 @@
# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=User Database Manager Socket
Documentation=man:systemd-userdbd.service(8)
DefaultDependencies=no
Before=sockets.target
[Socket]
ListenStream=/run/systemd/userdb/io.systemd.Multiplexer
Symlinks=/run/systemd/userdb/io.systemd.NameServiceSwitch
SocketMode=0666